diff --git a/package-lock.json b/package-lock.json index cb82234b..2921e3cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.1", - "youtubei.js": "^13.1.0" + "youtubei.js": "^15.0.1" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", @@ -1049,15 +1049,6 @@ "npm": ">=9.0.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", @@ -6914,12 +6905,13 @@ } }, "node_modules/jintr": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.2.1.tgz", - "integrity": "sha512-yjKUBuwTTg4nc4izMysxuIk0BKh45hnbc1KnXE6LxagIGZn5od+I2elpuRY9IIm3EiKiUZxhxV89a0iX+xoEZg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.3.1.tgz", + "integrity": "sha512-nnOzyhf0SLpbWuZ270Omwbj5LcXUkTcZkVnK8/veJXtSZOiATM5gMZMdmzN75FmTyj+NVgrGaPdH12zIJ24oIA==", "funding": [ "https://github.com/sponsors/LuanRT" ], + "license": "MIT", "dependencies": { "acorn": "^8.8.0" } @@ -12048,29 +12040,26 @@ } }, "node_modules/youtubei.js": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-13.1.0.tgz", - "integrity": "sha512-uL4TyojAYET0c5NGFD7+ScCod/k8Pc/B+D5tLrunFcz1GaBjRMOGRPcNGaRmnhwisegU7ibtw0iUxCN+BZ0ang==", + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-15.0.1.tgz", + "integrity": "sha512-2slapqJS5NuXKHvcACEknyVz0AjH/TrXaOhDM0q2twQKa54kCmfj+7B/2nGfd20uzAe29zW1ejk2qOc4ABuGkg==", "funding": [ "https://github.com/sponsors/LuanRT" ], + "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^3.2.1", - "tslib": "^2.5.0", - "undici": "^5.19.1" + "jintr": "^3.3.1", + "undici": "^6.21.3" } }, "node_modules/youtubei.js/node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=18.17" } }, "node_modules/zod": { diff --git a/package.json b/package.json index 610c17b8..8e14b457 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.1", - "youtubei.js": "^13.1.0" + "youtubei.js": "^15.0.1" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", -- 2.51.2 From 3704fba1f83104e702f9bf39c64261edd38e2b11 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 2 Sep 2025 12:18:21 +0000 Subject: [PATCH 2/7] feat(ytm): Add debug logging #345 --- src/backend/sources/YTMusicSource.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index b32a53ef..bab612de 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -162,6 +162,9 @@ export default class YTMusicSource extends AbstractSource { this.authed = true; await this.yti.session.oauth.cacheCredentials(); }); + if(isDebugMode()) { + Log.setLevel(Log.Level.DEBUG); + } } protected configureCustomOauth() { -- 2.51.2 From 7068da5fc283fa892265d19ee5a8c71890cff93e Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 2 Sep 2025 12:24:33 +0000 Subject: [PATCH 3/7] test(ytm): Fix failing type assertion --- src/backend/tests/ytm/ytm.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/tests/ytm/ytm.test.ts b/src/backend/tests/ytm/ytm.test.ts index 7d8d5d25..9830136d 100644 --- a/src/backend/tests/ytm/ytm.test.ts +++ b/src/backend/tests/ytm/ytm.test.ts @@ -4,12 +4,13 @@ import chai, { assert, expect } from 'chai'; import asPromised from 'chai-as-promised'; import clone from "clone"; import YTMusicSource, { ytiHistoryResponseFromShelfToPlays, ytiHistoryResponseToListItems } from "../../sources/YTMusicSource.js"; -import ytHistoryRes from './ytres.json' assert {type: 'json'}; +import ytHistoryRes from './ytres.json' with {type: 'json'}; import EventEmitter from "events"; import { generatePlay, generatePlays, normalizePlays } from '../utils/PlayTestUtils.js'; import { YTMusicSourceConfig } from '../../common/infrastructure/config/source/ytmusic.js'; import { sleep } from '../../utils.js'; import dayjs from 'dayjs'; +import { ApiResponse } from 'youtubei.js'; chai.use(asPromised); @@ -33,12 +34,12 @@ const createYtSource = (opts?: { describe('Parses History', function () { it(`Parses a history response to tracks`, async function () { - const items = ytiHistoryResponseToListItems(ytHistoryRes); + const items = ytiHistoryResponseToListItems(ytHistoryRes as ApiResponse); expect(items).length(10); }); it(`Parses a history response plays with shelf name`, async function () { - const items = ytiHistoryResponseFromShelfToPlays(ytHistoryRes); + const items = ytiHistoryResponseFromShelfToPlays(ytHistoryRes as ApiResponse); expect(items[0]?.meta?.comment).to.eq('March 2023'); }); }); -- 2.51.2 From 869c7dcfe14d802959848deb636e2b1224b4e818 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 2 Sep 2025 13:07:46 +0000 Subject: [PATCH 4/7] fea(ytm): Allow user-defined oauth scopes --- src/backend/sources/YTMusicSource.ts | 57 ++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index bab612de..d7e8a849 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -19,10 +19,8 @@ import { import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js"; import { buildTrackString, truncateStringToLength } from "../../core/StringUtils.js"; import { joinedUrl } from "../utils/NetworkUtils.js"; -import { FixedSizeList } from "fixed-size-list"; import { todayAwareFormat } from "../utils/TimeUtils.js"; -import { RestType } from "ts-json-schema-generator"; -import { parseArtistCredits, parseCredits } from "../utils/StringUtils.js"; +import { parseArrayFromMaybeString, parseArtistCredits, parseCredits } from "../utils/StringUtils.js"; export interface HistoryIngressResult { plays: PlayObject[], @@ -72,18 +70,43 @@ export const ytiHistoryResponseFromShelfToPlays = (res: ApiResponse, options: {n return items; } -const GOOGLE_OAUTH_OPTS: GenerateAuthUrlOpts = { - access_type: 'offline', - scope: [ - "http://gdata.youtube.com", - "https://www.googleapis.com/auth/youtube", - "https://www.googleapis.com/auth/youtube.force-ssl", - "https://www.googleapis.com/auth/youtube-paid-content", - "https://www.googleapis.com/auth/accounts.reauth", - ], - include_granted_scopes: true, - prompt: 'consent', -}; +const DEFAULT_SCOPES = [ + "http://gdata.youtube.com", + "https://www.googleapis.com/auth/youtube", + "https://www.googleapis.com/auth/youtube.force-ssl", + "https://www.googleapis.com/auth/youtube-paid-content", + "https://www.googleapis.com/auth/accounts.reauth", +]; + +const VALID_SCOPES = [ + "https://www.googleapis.com/auth/youtube", + "https://www.googleapis.com/auth/youtube.force-ssl", + "https://www.googleapis.com/auth/youtube-paid-content", +] + +const getGoogleOauthOpts = (): GenerateAuthUrlOpts => { + let scopes: string[]; + const userInput = parseArrayFromMaybeString(process.env.YTM_SCOPES); + if (userInput.length > 0) { + scopes = userInput.map(x => { + if (x.toLocaleLowerCase() === 'default') { + return DEFAULT_SCOPES; + } else if (x.toLocaleLowerCase() === 'valid') { + return VALID_SCOPES; + } + return x; + }).flat(1); + } else { + scopes = VALID_SCOPES; + } + + return { + access_type: 'offline', + scope: scopes, + include_granted_scopes: true, + prompt: 'consent', + }; +} export default class YTMusicSource extends AbstractSource { @@ -206,7 +229,9 @@ export default class YTMusicSource extends AbstractSource { redirectUri: this.redirectUri, }); - const authorizationUrl = this.oauthClient.generateAuthUrl(GOOGLE_OAUTH_OPTS); + const scopeOpts = getGoogleOauthOpts(); + this.logger.debug(`Using scopes:\n${(scopeOpts.scope as string[]).join('\n')}`) + const authorizationUrl = this.oauthClient.generateAuthUrl(getGoogleOauthOpts()); this.verificationUrl = authorizationUrl; } -- 2.51.2 From 8dd9958b304cdce0e2abd2818eb3507ddcc04caa Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 5 Sep 2025 18:55:53 +0000 Subject: [PATCH 5/7] Use default scopes --- src/backend/sources/YTMusicSource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index d7e8a849..20163441 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -97,7 +97,7 @@ const getGoogleOauthOpts = (): GenerateAuthUrlOpts => { return x; }).flat(1); } else { - scopes = VALID_SCOPES; + scopes = DEFAULT_SCOPES; } return { -- 2.51.2 From e86945355c23fee2f9d20053ca50dde398de5d94 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 5 Sep 2025 18:56:16 +0000 Subject: [PATCH 6/7] fix: Add additional logging for ytm history request --- src/backend/sources/YTMusicSource.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index 20163441..e771b409 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -448,6 +448,10 @@ Redirect URI : ${this.redirectUri}`); }); return res; } catch (e) { + const info = loggedErrorExtra(e); + if(info !== undefined) { + this.logger.error(info, 'Additional API response details') + } throw e; } } -- 2.51.2 From b3a6beb8f457ecbe6739a9a0674e965d2ebc2558 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 10 Sep 2025 12:57:42 +0000 Subject: [PATCH 7/7] docs(ytm): Add oauth warning --- docsite/docs/configuration/configuration.mdx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docsite/docs/configuration/configuration.mdx b/docsite/docs/configuration/configuration.mdx index f0bbbebe..fd29b3e6 100644 --- a/docsite/docs/configuration/configuration.mdx +++ b/docsite/docs/configuration/configuration.mdx @@ -2358,17 +2358,12 @@ Due to monitoring being unofficial, listening history from YTM can be inconsiste ::: -#### Authentication +#### Authentication {#ytm-auth} -Only one of these methods needs to be used. **Cookies** are easier but **OAuth Client** may be more stable. +Only one of these methods needs to be used. - :::info - - If cookies stop working for you or are being invalidated often try switching to **OAuth Client** authentication. - - ::: Use instructions from @@ -2400,10 +2395,14 @@ Only one of these methods needs to be used. **Cookies** are easier but **OAuth C - :::note + :::warning + + As of Sept 25' OAuth clients no longer seem to have permission to access the YTM service, as reported by the upstream YTM library developer and [several other users.](https://github.com/FoxxMD/multi-scrobbler/issues/345#issuecomment-3258518769) + + If you have existing, working OAuth credentials keep using them but there is no gaurantee they will continue to work. + + If you are setting up the YTM Source for the first time you should first try to use [Cookies](./?ytmAuth=cookie#ytm-auth). - This is likely to be the most stable method and least likely to be blocked or have authentication invalidated after an extended period. It requires more setup but is worth the effort. - ::: [Based on the instructions from here...](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2479689924)