From 7faf94cfab97f5d7be3877f5aba7e837344efcf6 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 3 Dec 2024 15:58:08 -0500 Subject: [PATCH] feat(ytm): Re-implement auth with cookie and oauth Youtube TV seems no longer have scope for reading history or account details? Cookie may work and custom oauth seems most stable based on reporting from LuanRT/YouTube.js#803 --- package-lock.json | 200 +++++++++++++++++- package.json | 3 +- .../infrastructure/config/source/ytmusic.ts | 19 ++ src/backend/server/auth.ts | 18 +- src/backend/sources/YTMusicSource.ts | 105 +++++++-- 5 files changed, 321 insertions(+), 24 deletions(-) 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;