Skip to content

Commit

Permalink
feat(youtube): Sync cookies with those returned from YTM responses #158
Browse files Browse the repository at this point in the history
* Patch youtube-music-ts-api to use updated cookies from response and provide a callback on update
* Implement currentCreds/build init data and read from MS-updated creds if available
* Write to currentCreds when ytm-ts-api invokes auth update callback and optionally log what parts changed based on config options
  • Loading branch information
FoxxMD committed Jun 14, 2024
1 parent 4494675 commit 1c8efba
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 28 deletions.
52 changes: 48 additions & 4 deletions patches/youtube-music-ts-api+1.7.0.patch

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions src/backend/common/infrastructure/config/source/ytmusic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PollingOptions } from "../common.js";
import { CommonSourceConfig, CommonSourceData } from "./index.js";
import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./index.js";

export interface YTMusicData extends CommonSourceData, PollingOptions {
export interface YTMusicCredentials {
/**
* The cookie retrieved from the Request Headers of music.youtube.com after logging in.
*
Expand All @@ -17,8 +17,17 @@ export interface YTMusicData extends CommonSourceData, PollingOptions {
* */
authUser?: number
}

export interface YTMusicData extends YTMusicCredentials, CommonSourceData, PollingOptions {
}
export interface YTMusicSourceConfig extends CommonSourceConfig {
data: YTMusicData
options?: CommonSourceOptions & {
/**
* When true MS will log to DEBUG what parts of the cookie are updated by YTM
* */
logAuthUpdateChanges?: boolean
}
}

export interface YTMusicSourceAIOConfig extends YTMusicSourceConfig {
Expand Down
16 changes: 15 additions & 1 deletion src/backend/common/schema/aio-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -1955,7 +1955,21 @@
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonSourceOptions",
"allOf": [
{
"$ref": "#/definitions/CommonSourceOptions"
},
{
"properties": {
"logAuthUpdateChanges": {
"description": "When true MS will log to DEBUG what parts of the cookie are updated by YTM",
"title": "logAuthUpdateChanges",
"type": "boolean"
}
},
"type": "object"
}
],
"title": "options"
},
"type": {
Expand Down
16 changes: 15 additions & 1 deletion src/backend/common/schema/aio.json
Original file line number Diff line number Diff line change
Expand Up @@ -2865,7 +2865,21 @@
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonSourceOptions",
"allOf": [
{
"$ref": "#/definitions/CommonSourceOptions"
},
{
"properties": {
"logAuthUpdateChanges": {
"description": "When true MS will log to DEBUG what parts of the cookie are updated by YTM",
"title": "logAuthUpdateChanges",
"type": "boolean"
}
},
"type": "object"
}
],
"title": "options"
},
"type": {
Expand Down
16 changes: 15 additions & 1 deletion src/backend/common/schema/source.json
Original file line number Diff line number Diff line change
Expand Up @@ -1797,7 +1797,21 @@
"type": "string"
},
"options": {
"$ref": "#/definitions/CommonSourceOptions",
"allOf": [
{
"$ref": "#/definitions/CommonSourceOptions"
},
{
"properties": {
"logAuthUpdateChanges": {
"description": "When true MS will log to DEBUG what parts of the cookie are updated by YTM",
"title": "logAuthUpdateChanges",
"type": "boolean"
}
},
"type": "object"
}
],
"title": "options"
}
},
Expand Down
98 changes: 79 additions & 19 deletions src/backend/sources/YTMusicSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { IYouTubeMusicAuthenticated } from "youtube-music-ts-api/interfaces-prim
import { IPlaylistDetail, ITrackDetail } from "youtube-music-ts-api/interfaces-supplementary";
import { PlayObject } from "../../core/Atomic.js";
import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js";
import { YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmusic.js";
import { parseDurationFromTimestamp, playObjDataMatch } from "../utils.js";
import { YTMusicCredentials, YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmusic.js";
import { parseDurationFromTimestamp, playObjDataMatch, readJson, writeFile } from "../utils.js";
import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js";

export default class YTMusicSource extends AbstractSource {
Expand All @@ -18,10 +18,57 @@ export default class YTMusicSource extends AbstractSource {

recentlyPlayed: PlayObject[] = [];

workingCredsPath: string;
currentCreds!: YTMusicCredentials;

constructor(name: string, config: YTMusicSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
super('ytmusic', name, config, internal, emitter);
this.canPoll = true;
this.supportsUpstreamRecentlyPlayed = true;
this.workingCredsPath = `${this.configDir}/currentAuth-ytm-${name}.json`;
}

protected writeCurrentAuth = async (cookie: string, authUser: number) => {
await writeFile(this.workingCredsPath, JSON.stringify({
cookie,
authUser
}));
}

protected async doBuildInitData(): Promise<true | string | undefined> {
let creds: YTMusicCredentials;
try {
creds = await readJson(this.workingCredsPath, {throwOnNotFound: false}) as YTMusicCredentials;
if(creds !== undefined) {
this.currentCreds = creds;
return `Read updated credentials from file currentAuth-ytm-${this.name}.json`;
}
} catch (e) {
this.logger.warn('Current YTMusic credentials file exists but could not be parsed', { path: this.workingCredsPath });
}
if(creds === undefined) {
if(this.config.data.cookie === undefined) {
throw new Error('No YTM cookies were found in configuration');
}
this.currentCreds = this.config.data;
return 'Read initial credentials from config';
}
}

doAuthentication = async () => {
try {
await this.getRecentlyPlayed();
return true;
} catch (e) {
if(e.message.includes('Status code: 401')) {
let hint = 'Verify your cookie and authUser are correct.';
if(this.currentCreds.authUser === undefined) {
hint = `${hint} TIP: 'authUser' is not defined your credentials. If you are using Chrome to retrieve credentials from music.youtube.com make sure the value from the 'X-Goog-AuthUser' is used as 'authUser'.`;
}
this.logger.error(`Authentication failed with the given credentials. ${hint} | Error => ${e.message}`);
}
throw e;
}
}

static formatPlayObj(obj: ITrackDetail, options: FormatPlayObjectOptions = {}): PlayObject {
Expand Down Expand Up @@ -71,14 +118,43 @@ export default class YTMusicSource extends AbstractSource {

recentlyPlayedTrackIsValid = (playObj: PlayObject) => playObj.meta.newFromSource

protected onAuthUpdate = (cookieStr: string, authUser: number, updated: Map<string, {new: string, old: string}>) => {
const {
options: {
logAuthUpdateChanges = false
} = {}
} = this.config;

if(logAuthUpdateChanges) {
const parts: string[] = [];
if(authUser !== this.currentCreds.authUser) {
parts.push(`X-Goog-Authuser: ${authUser}`);
}
for(const [k,v] of updated) {
parts.push(`Cookie ${k}: Old => ${v.old} | New => ${v.new}`);
}
this.logger.debug(`Updated Auth -->\n${parts.join('\n')}`);
} else {
this.logger.debug(`Updated Auth`);
}

this.currentCreds = {
cookie: cookieStr,
authUser
};


this.writeCurrentAuth(cookieStr, authUser).then(() => {});
}

api = async (): Promise<IYouTubeMusicAuthenticated> => {
if(this.apiInstance !== undefined) {
return this.apiInstance;
}
// @ts-expect-error default does exist
const ytm = new YouTubeMusic.default() as YouTubeMusic;
try {
this.apiInstance = await ytm.authenticate(this.config.data.cookie, this.config.data.authUser);
this.apiInstance = await ytm.authenticate(this.config.data.cookie, this.config.data.authUser, this.onAuthUpdate);
} catch (e: any) {
this.logger.error('Failed to authenticate', e);
throw e;
Expand Down Expand Up @@ -197,22 +273,6 @@ export default class YTMusicSource extends AbstractSource {

}

doAuthentication = async () => {
try {
await this.getRecentlyPlayed();
return true;
} catch (e) {
if(e.message.includes('Status code: 401')) {
let hint = 'Verify your cookie and authUser are correct.';
if(this.config.data.authUser === undefined) {
hint = `${hint} TIP: 'authUser' is not defined your credentials. If you are using Chrome to retrieve credentials from music.youtube.com make sure the value from the 'X-Goog-AuthUser' is used as 'authUser'.`;
}
this.logger.error(`Authentication failed with the given credentials. ${hint} | Error => ${e.message}`);
}
throw e;
}
}

onPollPostAuthCheck = async () => {
if(!this.polling) {
this.logger.verbose('Hydrating initial recently played tracks for reference.');
Expand Down

0 comments on commit 1c8efba

Please sign in to comment.