[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

fea(ytm): Allow user-defined oauth scopes

FoxxMD (Sep 2, 2025, 1:07 PM UTC) 869c7dcf 7068da5f

+41 -16
+41 -16
src/backend/sources/YTMusicSource.ts
··· 19 19 import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js"; 20 20 import { buildTrackString, truncateStringToLength } from "../../core/StringUtils.js"; 21 21 import { joinedUrl } from "../utils/NetworkUtils.js"; 22 - import { FixedSizeList } from "fixed-size-list"; 23 22 import { todayAwareFormat } from "../utils/TimeUtils.js"; 24 - import { RestType } from "ts-json-schema-generator"; 25 - import { parseArtistCredits, parseCredits } from "../utils/StringUtils.js"; 23 + import { parseArrayFromMaybeString, parseArtistCredits, parseCredits } from "../utils/StringUtils.js"; 26 24 27 25 export interface HistoryIngressResult { 28 26 plays: PlayObject[], ··· 72 70 return items; 73 71 } 74 72 75 - const GOOGLE_OAUTH_OPTS: GenerateAuthUrlOpts = { 76 - access_type: 'offline', 77 - scope: [ 78 - "http://gdata.youtube.com", 79 - "https://www.googleapis.com/auth/youtube", 80 - "https://www.googleapis.com/auth/youtube.force-ssl", 81 - "https://www.googleapis.com/auth/youtube-paid-content", 82 - "https://www.googleapis.com/auth/accounts.reauth", 83 - ], 84 - include_granted_scopes: true, 85 - prompt: 'consent', 86 - }; 73 + const DEFAULT_SCOPES = [ 74 + "http://gdata.youtube.com", 75 + "https://www.googleapis.com/auth/youtube", 76 + "https://www.googleapis.com/auth/youtube.force-ssl", 77 + "https://www.googleapis.com/auth/youtube-paid-content", 78 + "https://www.googleapis.com/auth/accounts.reauth", 79 + ]; 80 + 81 + const VALID_SCOPES = [ 82 + "https://www.googleapis.com/auth/youtube", 83 + "https://www.googleapis.com/auth/youtube.force-ssl", 84 + "https://www.googleapis.com/auth/youtube-paid-content", 85 + ] 86 + 87 + const getGoogleOauthOpts = (): GenerateAuthUrlOpts => { 88 + let scopes: string[]; 89 + const userInput = parseArrayFromMaybeString(process.env.YTM_SCOPES); 90 + if (userInput.length > 0) { 91 + scopes = userInput.map(x => { 92 + if (x.toLocaleLowerCase() === 'default') { 93 + return DEFAULT_SCOPES; 94 + } else if (x.toLocaleLowerCase() === 'valid') { 95 + return VALID_SCOPES; 96 + } 97 + return x; 98 + }).flat(1); 99 + } else { 100 + scopes = VALID_SCOPES; 101 + } 102 + 103 + return { 104 + access_type: 'offline', 105 + scope: scopes, 106 + include_granted_scopes: true, 107 + prompt: 'consent', 108 + }; 109 + } 87 110 88 111 export default class YTMusicSource extends AbstractSource { 89 112 ··· 206 229 redirectUri: this.redirectUri, 207 230 }); 208 231 209 - const authorizationUrl = this.oauthClient.generateAuthUrl(GOOGLE_OAUTH_OPTS); 232 + const scopeOpts = getGoogleOauthOpts(); 233 + this.logger.debug(`Using scopes:\n${(scopeOpts.scope as string[]).join('\n')}`) 234 + const authorizationUrl = this.oauthClient.generateAuthUrl(getGoogleOauthOpts()); 210 235 this.verificationUrl = authorizationUrl; 211 236 } 212 237