diff --git a/package-lock.json b/package-lock.json index 12ed8880..db865546 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "fixed-size-list": "^0.3.0", "formidable": "^3.5", "glob": "^11.0.0", + "google-auth-library": "^9.15.0", "gotify": "^1.1.0", "iso-websocket": "^0.3.0", "iti": "^0.6.0", @@ -81,7 +82,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.1", - "youtubei.js": "^10.5.0" + "youtubei.js": "^11.0.1" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", @@ -3241,6 +3242,17 @@ "node": ">= 10.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3587,6 +3599,14 @@ "pnpm": ">=6" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3714,6 +3734,11 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4599,6 +4624,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5096,6 +5129,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -5493,6 +5531,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5671,6 +5748,22 @@ "node": ">=8" } }, + "node_modules/google-auth-library": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz", + "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -5738,6 +5831,18 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -5909,6 +6014,18 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6298,8 +6415,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "peer": true, "engines": { "node": ">=8" }, @@ -6695,9 +6810,9 @@ } }, "node_modules/jintr": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.1.1.tgz", - "integrity": "sha512-89cwX4ouogeDGOBsEVsVYsnWWvWjchmwXBB4kiBhmjOKw19FiOKhNhMhpxhTlK2ctl7DS+d/ethfmuBpzoNNgA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-3.0.2.tgz", + "integrity": "sha512-5g2EBudeJFOopjAX4exAv5OCCW1DgUISfoioCsm1h9Q9HJ41LmnZ6J52PCsqBlQihsmp0VDuxreAVzM7yk5nFA==", "funding": [ "https://github.com/sponsors/LuanRT" ], @@ -6750,6 +6865,14 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6849,6 +6972,25 @@ "node": "*" } }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7513,6 +7655,25 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -10444,6 +10605,11 @@ "node": ">= 4.0.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts_lru_map": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ts_lru_map/-/ts_lru_map-1.0.2.tgz", @@ -11348,6 +11514,20 @@ "phin": "^3.6.1" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11742,15 +11922,15 @@ } }, "node_modules/youtubei.js": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.5.0.tgz", - "integrity": "sha512-iyA+VF28c15tCCKH9ExM2RKC3zYiHzA/eixGlJ3vERANkuI+xYKzAZ4vtOhmyqwrAddu88R/DkzEsmpph5NWjg==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-11.0.1.tgz", + "integrity": "sha512-ZsbOd+5XF2Ofi3FrLMfYd+f9g9H8xswlouFhjhOqbwT68dMJtX6CRGsHNj5VTFCR/+L/865x1lnUlllB2dDDTA==", "funding": [ "https://github.com/sponsors/LuanRT" ], "dependencies": { "@bufbuild/protobuf": "^2.0.0", - "jintr": "^2.1.1", + "jintr": "^3.0.2", "tslib": "^2.5.0", "undici": "^5.19.1" } diff --git a/package.json b/package.json index 726d52a2..4d3d2549 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "fixed-size-list": "^0.3.0", "formidable": "^3.5", "glob": "^11.0.0", + "google-auth-library": "^9.15.0", "gotify": "^1.1.0", "iso-websocket": "^0.3.0", "iti": "^0.6.0", @@ -111,7 +112,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.1", - "youtubei.js": "^10.5.0" + "youtubei.js": "^11.0.1" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", diff --git a/src/backend/common/infrastructure/config/source/ytmusic.ts b/src/backend/common/infrastructure/config/source/ytmusic.ts index 36802dcb..513c4cc1 100644 --- a/src/backend/common/infrastructure/config/source/ytmusic.ts +++ b/src/backend/common/infrastructure/config/source/ytmusic.ts @@ -1,8 +1,27 @@ import { PollingOptions } from "../common.js"; import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./index.js"; +import { Innertube } from 'youtubei.js'; + +//type InnertubeOptions = Omit[0], 'cookie' | 'cache' | 'fetch'>; export interface YTMusicData extends CommonSourceData, PollingOptions { + /** + * The cookie retrieved from the Request Headers of music.youtube.com after logging in. + * + * See https://ytmusicapi.readthedocs.io/en/stable/setup/browser.html#copy-authentication-headers for how to retrieve this value. + * + * @examples ["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."] + * */ + cookie?: string + + clientId?: string + + clientSecret?: string + + redirectUri?: string } +//export type YTMusicData = YTMusicDataCommon & InnertubeOptions; + export interface YTMusicSourceConfig extends CommonSourceConfig { data?: YTMusicData options?: CommonSourceOptions & { diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index dc41fa0c..8f4605bf 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -8,6 +8,8 @@ import LastfmSource from "../sources/LastfmSource.js"; import ScrobbleSources from "../sources/ScrobbleSources.js"; import SpotifySource from "../sources/SpotifySource.js"; import YTMusicSource from "../sources/YTMusicSource.js"; +import { sortAndDeduplicateDiagnostics } from "typescript"; +import { source } from "common-tags"; export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMiddle: ExpressHandler, clientMiddle: ExpressHandler, scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => { app.use('/api/client/auth', clientMiddle); @@ -65,7 +67,8 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid } const { query: { - state + state, + name } = {} } = req; if (req.url.includes('lastfm')) { @@ -86,6 +89,19 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid } catch (e) { return res.send(e.message); } + } else if(req.url.includes('ytmusic')) { + const entity: YTMusicSource | undefined = scrobbleSources.getByName(name) as (YTMusicSource | undefined); + if(entity === undefined) { + logger.error(`No YTMUsic source with name ${state} was found`); + } + const result = await entity.handleAuthCodeCallback(req.query); + let responseContent = 'OK'; + if(result === true) { + entity.poll(); + } else { + responseContent = result; + } + return res.send(responseContent); } else { // TODO right now all sources requiring source interaction are covered by logic branches (deezer above and spotify here) // but eventually should update all source callbacks to url specific URLS to avoid ambiguity... diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index e137c42a..6a2c47ba 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -3,9 +3,10 @@ import EventEmitter from "events"; import { PlayObject } from "../../core/Atomic.js"; import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js"; import { YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmusic.js"; -import { Innertube, UniversalCache, Parser, YTNodes, ApiResponse, IBrowseResponse } from 'youtubei.js'; +import { Innertube, UniversalCache, Parser, YTNodes, ApiResponse, IBrowseResponse, Log } from 'youtubei.js'; +import { OAuth2Client } from 'google-auth-library'; import {resolve} from 'path'; -import { sleep } from "../utils.js"; +import { joinedUrl, sleep } from "../utils.js"; import { getPlaysDiff, humanReadableDiff, @@ -14,7 +15,6 @@ import { playsAreSortConsistent } from "../utils/PlayComparisonUtils.js"; import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js"; -import { ListDiff } from "@donedeal0/superdiff"; export const ytiHistoryResponseToListItems = (res: ApiResponse): YTNodes.MusicResponsiveListItem[] => { const page = Parser.parseResponse(res.data); @@ -61,6 +61,8 @@ export default class YTMusicSource extends AbstractSource { requiresAuth = true; requiresAuthInteraction = true; + cookieBased: boolean = false; + declare config: YTMusicSourceConfig recentlyPlayed: PlayObject[] = []; @@ -68,12 +70,15 @@ export default class YTMusicSource extends AbstractSource { yti: Innertube; userCode?: string; verificationUrl?: string; + oauthClient?: OAuth2Client; workingCredsPath: string; constructor(name: string, config: YTMusicSourceConfig, internal: InternalConfig, emitter: EventEmitter) { super('ytmusic', name, config, internal, emitter); this.canPoll = true; + Log.setLevel(Log.Level.ERROR); + this.cookieBased = this.config.data?.cookie !== undefined; this.supportsUpstreamRecentlyPlayed = true; this.workingCredsPath = resolve(this.configDir, `yti-${this.name}`); } @@ -88,6 +93,7 @@ export default class YTMusicSource extends AbstractSource { protected async doBuildInitData(): Promise { this.yti = await Innertube.create({ + ...this.config.data, cache: new UniversalCache(true, this.workingCredsPath) }); this.yti.session.on('update-credentials', async ({ credentials }) => { @@ -111,10 +117,11 @@ export default class YTMusicSource extends AbstractSource { } else { this.logger.debug('Auth success'); } - await this.yti.session.oauth.cacheCredentials(); this.userCode = undefined; this.verificationUrl = undefined; this.authed = true; + await this.yti.session.oauth.cacheCredentials(); + const f =1; }); return true; } @@ -127,33 +134,107 @@ export default class YTMusicSource extends AbstractSource { } clearCredentials = async () => { - if(this.yti.session.logged_in) { + if(this.yti.session.logged_in && !this.cookieBased) { await this.yti.session.signOut(); } } + async handleAuthCodeCallback(obj: Record): Promise { + if (obj.code === undefined) { + this.logger.error(`Authorization callback did not contain 'code' in URL`); + return false; + } + + const { tokens } = await this.oauthClient.getToken(obj.code as string); + + if (tokens.access_token && tokens.refresh_token && tokens.expiry_date) { + await this.yti.session.signIn({ + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + expiry_date: new Date(tokens.expiry_date).toISOString(), + client: { + client_id: this.config.data.clientId, + client_secret: this.config.data.clientSecret + } + }); + this.authed = true; + this.verificationUrl = undefined; + this.userCode = undefined; + await this.yti.session.oauth.cacheCredentials(); + Log.setLevel(Log.Level.ERROR); + return true; + } else { + this.logger.error(`Token data did not return all required properties.`); + return tokens; + } + } + doAuthentication = async () => { try { + if (this.cookieBased) { + try { + await this.yti.account.getInfo() + this.authed = true; + } catch (e) { + const info = loggedErrorExtra(e); + if (info !== undefined) { + this.logger.error(info, 'Additional API response details') + } + this.logger.error(new Error('Cookie-based authentication failed. Try recreating cookie or using custom OAuth Client', { cause: e })); + } + } + await Promise.race([ - sleep(300), + sleep(1000), this.yti.session.signIn() ]); - if(this.authed === false && this.userCode !== undefined) { - if(this.userCode !== undefined) { - throw new Error(`Sign in with the code '${this.userCode}' using the authentication link on the dashboard or ${this.verificationUrl}`) + if (this.authed === false) { + + if (this.config.data.clientId !== undefined) { + const redirectUri = this.config.data?.redirectUri ?? joinedUrl(this.localUrl, `ytmusic/callback?name=${this.name}`).toString(); + + this.logger.info(`Using Custom OAuth Client with Redirect URI: ${redirectUri}`); + this.oauthClient = new OAuth2Client({ + clientId: this.config.data.clientId, + clientSecret: this.config.data.clientSecret, + redirectUri + }); + + const authorizationUrl = this.oauthClient.generateAuthUrl({ + 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', + }); + + this.verificationUrl = authorizationUrl; + this.userCode = undefined; + throw new Error(`Sign in using ${authorizationUrl}`); } else { - throw new Error('Waited too long for auth response from YTM!'); + if (this.userCode !== undefined) { + this.logger.warn('Logging in with YoutubeTV Oauth will likely NOT provide access to Youtube Music history!! You should try to use either cookies or a custom OAuth Client ID/Secret'); + throw new Error(`Sign in with the code '${this.userCode}' using the authentication link on the dashboard or ${this.verificationUrl}`) + } else { + throw new Error('Waited too long for auth response from YTM!'); + } } } try { await this.yti.account.getInfo() } catch (e) { const info = loggedErrorExtra(e); - if(info !== undefined) { + if (info !== undefined) { this.logger.error(info, 'Additional API response details') } - throw new Error('Credentials exist but API calls are failing. Try re-authenticating?', {cause: e}); + throw new Error('Credentials exist but API calls are failing. Try re-authenticating?', { cause: e }); } + Log.setLevel(Log.Level.ERROR); return true; } catch (e) { throw e;