diff --git a/README.md b/README.md index f4cc63aa7a5718f9aac057ed476e1e5d22103825..d8b7158867447a9cb3066bf45609dab18b04aa1d 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,13 @@ 1. `npm run npm-dev` or `bun run bun-dev` 2. load `BiliBiliConfig.json` into Grayjay +## TO-DO +- [ ] check that share urls/uris work and share into the spotify app + ## Grayjay Bugs - [ ] none + + + + + diff --git a/build/SpotifyConfig.json b/build/SpotifyConfig.json index 68f3bc94e63761ebd8e521d4e30ec19075c7dd40..10df0b703793d9c2a3ef99efc3136e7e97487fd1 100644 --- a/build/SpotifyConfig.json +++ b/build/SpotifyConfig.json @@ -21,11 +21,12 @@ "open.spotify.com", "spclient.wg.spotify.com", "gue1-spclient.spotify.com", - "seektables.scdn.co" + "seektables.scdn.co", + "api-partner.spotify.com" ], "authentication": { "loginUrl": "https://accounts.spotify.com/en/login", - "cookiesToFind": ["SESSDATA"], + "cookiesToFind": ["sp_dc"], "userAgent": "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36" } } diff --git a/build/SpotifyScript.js b/build/SpotifyScript.js index 5cc2ec1b898c0ea1982b6ba6ad3e0b099eab1109..34b9ecccd63ecfd7c77fa50aa93c6907cc5e95ff 100644 --- a/build/SpotifyScript.js +++ b/build/SpotifyScript.js @@ -1,10 +1,11 @@ -const SONG_REGEX = /^https:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]*)($|\/)/; +const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/; const SONG_URL_PREFIX = "https://open.spotify.com/track/"; const IMAGE_URL_PREFIX = "https://i.scdn.co/image/"; const PLATFORM = "Spotify"; // const USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0" as const const HARDCODED_ZERO = 0; const HARDCODED_EMPTY_STRING = ""; +const EMPTY_AUTHOR = new PlatformAuthorLink(new PlatformID(PLATFORM, "", plugin.config.id), "", ""); const local_http = http; // const local_utility = utility // set missing constants @@ -13,6 +14,7 @@ Type.Order.Views = "Most played"; Type.Order.Favorites = "Most favorited"; /** State */ let local_state; +//#endregion //#region source methods source.enable = enable; source.disable = disable; @@ -95,6 +97,8 @@ if (IS_TESTING) { } } */ +//#endregion +//#region enable function enable(conf, settings, savedState) { if (IS_TESTING) { log("IS_TESTING true"); @@ -110,10 +114,12 @@ function enable(conf, settings, savedState) { local_state = state; } else { - const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW"; - const song_url = `${SONG_URL_PREFIX}${song_uri_id}`; - const song_html_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/; - const match_result = local_http.GET(song_url, {}, false).body.match(song_html_regex); + // download bearer token + const homepage_url = "https://open.spotify.com"; + const bearer_token_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/; + // use the authenticated client to get a logged in bearer token + const homepage_response = local_http.GET(homepage_url, {}, true); + const match_result = homepage_response.body.match(bearer_token_regex); if (match_result === null) { throw new ScriptException("regex error"); } @@ -122,19 +128,31 @@ function enable(conf, settings, savedState) { throw new ScriptException("regex error"); } const token_response = JSON.parse(maybe_json); - local_state = { bearer_token: token_response.accessToken }; + const bearer_token = token_response.accessToken; + // download license uri + const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0"; + const get_license_url_response = local_http.GET(get_license_url_url, { Authorization: `Bearer ${bearer_token}` }, false); + const get_license_response = JSON.parse(get_license_url_response.body); + const license_uri = `https://gue1-spclient.spotify.com/${get_license_response.uri}`; + local_state = { + bearer_token, + license_uri: license_uri + }; } } +//#endregion function disable() { - log("BiliBili log: disabling"); + log("Spotify log: disabling"); } function saveState() { return JSON.stringify(local_state); } +//#region home function getHome() { const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW"; const song_url = `${SONG_URL_PREFIX}${song_uri_id}`; - const song_metadata_response = get_song_metadata(song_uri_id); + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(song_uri_id); + const song_metadata_response = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body); const first_artist = song_metadata_response.artist[0]; if (first_artist === undefined) { throw new ScriptException("missing artist"); @@ -156,91 +174,246 @@ function getHome() { })]; return new VideoPager(songs, false); } +//#endregion +//#region content // https://open.spotify.com/track/6XXxKsu3RJeN3ZvbMYrgQW +// https://open.spotify.com/episode/3Z88ZE0i3L7AIrymrBwtqg function isContentDetailsUrl(url) { - return SONG_REGEX.test(url); + return CONTENT_REGEX.test(url); } function getContentDetails(url) { - const match_result = url.match(SONG_REGEX); + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to listen to songs"); + } + const match_result = url.match(CONTENT_REGEX); if (match_result === null) { throw new ScriptException("regex error"); } - const maybe_song_uri_id = match_result[1]; - if (maybe_song_uri_id === undefined) { + const maybe_content_type = match_result[1]; + if (maybe_content_type === undefined) { throw new ScriptException("regex error"); } - const song_url = `${SONG_URL_PREFIX}${maybe_song_uri_id}`; - const song_metadata_response = get_song_metadata(maybe_song_uri_id); - const first_artist = song_metadata_response.artist[0]; - if (first_artist === undefined) { - throw new ScriptException("missing artist"); + const content_type = maybe_content_type; + const content_uri_id = match_result[2]; + if (content_uri_id === undefined) { + throw new ScriptException("regex error"); } - const format = "MP4_128"; - const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format; })?.file_id; - if (maybe_file_id === undefined) { - throw new ScriptException("missing expected format"); + switch (content_type) { + case "track": { + const song_url = `${SONG_URL_PREFIX}${content_uri_id}`; + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(content_uri_id); + const song_metadata_response = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body); + const first_artist = song_metadata_response.artist[0]; + if (first_artist === undefined) { + throw new ScriptException("missing artist"); + } + const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"; + const format = is_premium() ? "MP4_256" : "MP4_128"; + const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format; })?.file_id; + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format"); + } + const { url, headers } = file_manifest_args(maybe_file_id); + const file_manifest = JSON.parse(local_http.GET(url, headers, false).body); + const duration = song_metadata_response.duration / 1000; + const file_url = file_manifest.cdnurl[0]; + if (file_url === undefined) { + throw new ScriptException("unreachable"); + } + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: format, + bitrate: HARDCODED_ZERO, + container: "audio/mp4", + codecs: "mp4a.40.2", + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })]; + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: song_metadata_response.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, artist_url), + url: song_url, + thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { + return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height); + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: song_metadata_response.canonical_uri, + // readonly uploadDate?: number + description: HARDCODED_EMPTY_STRING, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO) + // readonly subtitles?: ISubtitleSource[] + }); + } + case "episode": { + const episode_url = `https://open.spotify.com/episode/${content_uri_id}`; + const { url: transcript_url, headers: transcript_headers } = transcript_args(content_uri_id); + const { url, headers } = episode_metadata_args(content_uri_id); + const responses = local_http.batch() + .GET(transcript_url, transcript_headers, false) + .GET(url, headers, false) + .execute(); + if (responses[0] === undefined || responses[1] === undefined) { + throw new ScriptException("unreachable"); + } + const transcript_response = JSON.parse(responses[0].body); + const episode_metadata_response = JSON.parse(responses[1].body); + const format = "MP4_128"; + const maybe_file_id = episode_metadata_response.data.episodeUnionV2.audio.items.find(function (file) { return file.format === format; })?.fileId; + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format"); + } + const { url: manifest_url, headers: manifest_headers } = file_manifest_args(maybe_file_id); + const file_manifest = JSON.parse(local_http.GET(manifest_url, manifest_headers, false).body); + const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000; + const file_url = file_manifest.cdnurl[0]; + if (file_url === undefined) { + throw new ScriptException("unreachable"); + } + const codecs = "mp4a.40.2"; + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: codecs, + bitrate: 128000, + container: "audio/mp4", + codecs, + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })]; + const subtitle_name = function () { + switch (transcript_response.language) { + case "en": + return "English"; + default: + throw assert_no_fall_through(transcript_response.language, "unreachable"); + } + }(); + let vtt_text = `WEBVTT ${subtitle_name}\n`; + vtt_text += "\n"; + transcript_response.section.forEach(function (section, index) { + if ("title" in section) { + return; + } + const next = transcript_response.section[index + 1]; + let end = next?.startMs; + if (end === undefined) { + end = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds; + } + vtt_text += `${milliseconds_to_WebVTT_timestamp(section.startMs)} --> ${milliseconds_to_WebVTT_timestamp(end)}\n`; + vtt_text += `${section.text.sentence.text}\n`; + vtt_text += "\n"; + }); + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: episode_metadata_response.data.episodeUnionV2.name, + author: EMPTY_AUTHOR, + url: episode_url, + thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { + return new Thumbnail(image.url, image.height); + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: episode_metadata_response.data.episodeUnionV2.uri, + uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + description: episode_metadata_response.data.episodeUnionV2.htmlDescription, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO), + subtitles: [{ + url: episode_url, + name: subtitle_name, + getSubtitles() { + return vtt_text; + }, + format: "text/vtt", + }] + }); + } + default: + throw assert_no_fall_through(content_type, "unreachable"); } +} +function transcript_args(episode_uri_id) { + const transcript_url_prefix = "https://spclient.wg.spotify.com/transcript-read-along/v2/episode/"; + const url = new URL(`${transcript_url_prefix}${episode_uri_id}`); + url.searchParams.set("format", "json"); + return { + url: url.toString(), + headers: { Authorization: `Bearer ${local_state.bearer_token}` } + }; +} +function file_manifest_args(file_id) { const file_manifest_url_prefix = "https://gue1-spclient.spotify.com/storage-resolve/v2/files/audio/interactive/10/"; const file_manifest_params = "?product=9&alt=json"; - const file_manifest = JSON.parse(local_http.GET(`${file_manifest_url_prefix}${maybe_file_id}${file_manifest_params}`, { Authorization: `Bearer ${local_state.bearer_token}` }, false).body); - log(file_manifest); - const duration = song_metadata_response.duration / 1000; - const audio_sources = file_manifest.cdnurl.map(function (url) { - return new AudioUrlSource({ - //audio/mp4; codecs="mp4a.40.2 - name: format, - bitrate: HARDCODED_ZERO, - container: "audio/mp4", - codecs: "mp4a.40.2", - duration, - url, - language: Language.UNKNOWN, - }); + return { + url: `${file_manifest_url_prefix}${file_id}${file_manifest_params}`, + headers: { Authorization: `Bearer ${local_state.bearer_token}` } + }; +} +function episode_metadata_args(episode_uri_id) { + const episode_metadata_url_prefix = "https://api-partner.spotify.com/pathfinder/v1/query"; + const variables = JSON.stringify({ + uri: `spotify:episode:${episode_uri_id}` }); - //https://seektables.scdn.co/seektable/4c652e57fd36f84d77af2b9d1d1332327a8fd774.json - const seektable_url_prefix = "https://seektables.scdn.co/seektable/"; - const seektable_response = JSON.parse(local_http.GET(`${seektable_url_prefix}${maybe_file_id}.json`, {}, false).body); - log(seektable_response); - const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0"; - const get_license_response = JSON.parse(local_http.GET(get_license_url_url, { Authorization: `Bearer ${local_state.bearer_token}` }, false).body); - log(get_license_response); - const license_url = `https://gue1-spclient.spotify.com/${get_license_response.uri}`; - log(license_url); - return new PlatformVideoDetails({ - id: new PlatformID(PLATFORM, maybe_song_uri_id, plugin.config.id), - name: song_metadata_response.name, - author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), - url: song_url, - thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { - return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height); - })), - duration, - viewCount: HARDCODED_ZERO, - isLive: false, - shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number - description: HARDCODED_EMPTY_STRING, - video: new UnMuxVideoSourceDescriptor([], audio_sources), - // readonly live?: IVideoSource - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" + } }); + const url = new URL(episode_metadata_url_prefix); + url.searchParams.set("operationName", "getEpisodeOrChapter"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; } -function get_song_metadata(song_uri_id) { +function song_metadata_args(song_uri_id) { const song_metadata_url = "https://spclient.wg.spotify.com/metadata/4/track/"; - const song_metadata_response = JSON.parse(local_http.GET(`${song_metadata_url}${get_gid(song_uri_id)}`, { - Authorization: `Bearer ${local_state.bearer_token}`, - Accept: "application/json" - }, false).body); - return song_metadata_response; + return { + url: `${song_metadata_url}${get_gid(song_uri_id)}`, + headers: { + Authorization: `Bearer ${local_state.bearer_token}`, + Accept: "application/json" + } + }; +} +//#endregion +//#region utilities +/** + * Converts seconds to the timestamp format used in WebVTT + * @param seconds + * @returns + */ +function milliseconds_to_WebVTT_timestamp(milliseconds) { + return new Date(milliseconds).toISOString().substring(11, 23); +} +function assert_never(value) { + log(value); +} +function log_passthrough(value) { + log(value); + return value; +} +function assert_no_fall_through(value, exception_message) { + log(["Spotify log:", value]); + if (exception_message !== undefined) { + return new ScriptException(exception_message); + } + return; +} +//#endregion +function is_premium() { + return false; } -// function assert_never(value: never) { -// log(value) -// } -// function log_passthrough<T>(value: T): T { -// log(value) -// return value -// } // https://open.spotifycdn.com/cdn/build/web-player/vendor~web-player.391a2438.js const Z = "0123456789abcdef"; const Q = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -305,5 +478,7 @@ function get_gid(song_uri_id) { c ? null : ee[s >>> 24] + ee[s >>> 16 & 255] + ee[s >>> 8 & 255] + ee[255 & s] + ee[a >>> 24] + ee[a >>> 16 & 255] + ee[a >>> 8 & 255] + ee[255 & a] + ee[r >>> 24] + ee[r >>> 16 & 255] + ee[r >>> 8 & 255] + ee[255 & r] + ee[o >>> 24] + ee[o >>> 16 & 255] + ee[o >>> 8 & 255] + ee[255 & o]; }(song_uri_id) : song_uri_id; } -// export { get_gid }; +// export statements are removed during build step +// used for unit testing in SpotifyScript.test.ts +// export { get_gid, assert_never, log_passthrough }; //# sourceMappingURL=http://localhost:8080/SpotifyScript.js.map \ No newline at end of file diff --git a/build/SpotifyScript.js.map b/build/SpotifyScript.js.map index 6f8f817d6fdf3679bf335e0444c394bff435cc91..33885938a918941a11d10f677c0afc6fd0c64c5e 100644 --- a/build/SpotifyScript.js.map +++ b/build/SpotifyScript.js.map @@ -1 +1 @@ -{"version":3,"file":"SpotifyScript.js","sourceRoot":"http://localhost:8080/","sources":["SpotifyScript.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"SpotifyScript.js","sourceRoot":"http://localhost:8080/","sources":["SpotifyScript.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/build/SpotifyScript.ts b/build/SpotifyScript.ts index f7be22792c3f03bc2e55fdde8caa5d3ac504a8e9..318168054e1285ee43bed60b5d9be7fe683a786f 100644 --- a/build/SpotifyScript.ts +++ b/build/SpotifyScript.ts @@ -1,14 +1,17 @@ +//#region constants import { - FileManifestResponse, - GetLicenseResponse, - SeektableResponse, - Settings, - SongMetadataResponse, - // SpotifySource, - State + type ContentType, + type EpisodeMetadataResponse, + type FileManifestResponse, + type GetLicenseResponse, + type Settings, + type SongMetadataResponse, + // type SpotifySource, + type State, + type TranscriptResponse, } from "./types.js" -const SONG_REGEX = /^https:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]*)($|\/)/ +const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/ const SONG_URL_PREFIX = "https://open.spotify.com/track/" as const const IMAGE_URL_PREFIX = "https://i.scdn.co/image/" as const @@ -17,6 +20,7 @@ const PLATFORM = "Spotify" as const const HARDCODED_ZERO = 0 as const const HARDCODED_EMPTY_STRING = "" as const +const EMPTY_AUTHOR = new PlatformAuthorLink(new PlatformID(PLATFORM, "", plugin.config.id), "", "") const local_http = http // const local_utility = utility @@ -28,6 +32,7 @@ Type.Order.Favorites = "Most favorited" /** State */ let local_state: State +//#endregion //#region source methods source.enable = enable @@ -119,7 +124,9 @@ if (IS_TESTING) { } } */ +//#endregion +//#region enable function enable(conf: SourceConfig, settings: Settings, savedState: string | null) { if (IS_TESTING) { log("IS_TESTING true") @@ -134,10 +141,14 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul const state: State = JSON.parse(savedState) local_state = state } else { - const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" - const song_url = `${SONG_URL_PREFIX}${song_uri_id}` - const song_html_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/ - const match_result = local_http.GET(song_url, {}, false).body.match(song_html_regex) + // download bearer token + const homepage_url = "https://open.spotify.com" + const bearer_token_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/ + + // use the authenticated client to get a logged in bearer token + const homepage_response = local_http.GET(homepage_url, {}, true) + + const match_result = homepage_response.body.match(bearer_token_regex) if (match_result === null) { throw new ScriptException("regex error") } @@ -146,23 +157,45 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul throw new ScriptException("regex error") } const token_response: { accessToken: string } = JSON.parse(maybe_json) - local_state = { bearer_token: token_response.accessToken } + const bearer_token = token_response.accessToken + + + // download license uri + const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0" + const get_license_url_response = local_http.GET( + get_license_url_url, + { Authorization: `Bearer ${bearer_token}` }, + false + ) + const get_license_response: GetLicenseResponse = JSON.parse( + get_license_url_response.body + ) + const license_uri = `https://gue1-spclient.spotify.com/${get_license_response.uri}` + + + local_state = { + bearer_token, + license_uri: license_uri + } } } +//#endregion function disable() { - log("BiliBili log: disabling") + log("Spotify log: disabling") } function saveState() { return JSON.stringify(local_state) } +//#region home function getHome() { const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" const song_url = `${SONG_URL_PREFIX}${song_uri_id}` - const song_metadata_response: SongMetadataResponse = get_song_metadata(song_uri_id) + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(song_uri_id) + const song_metadata_response: SongMetadataResponse = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body) const first_artist = song_metadata_response.artist[0] if (first_artist === undefined) { throw new ScriptException("missing artist") @@ -184,136 +217,282 @@ function getHome() { })] return new VideoPager(songs, false) } +//#endregion +//#region content // https://open.spotify.com/track/6XXxKsu3RJeN3ZvbMYrgQW +// https://open.spotify.com/episode/3Z88ZE0i3L7AIrymrBwtqg function isContentDetailsUrl(url: string) { - return SONG_REGEX.test(url) + return CONTENT_REGEX.test(url) } function getContentDetails(url: string) { - const match_result = url.match(SONG_REGEX) + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to listen to songs") + } + const match_result = url.match(CONTENT_REGEX) if (match_result === null) { throw new ScriptException("regex error") } - const maybe_song_uri_id = match_result[1] - if (maybe_song_uri_id === undefined) { + const maybe_content_type = match_result[1] + if (maybe_content_type === undefined) { throw new ScriptException("regex error") } - const song_url = `${SONG_URL_PREFIX}${maybe_song_uri_id}` - - const song_metadata_response: SongMetadataResponse = get_song_metadata(maybe_song_uri_id) - const first_artist = song_metadata_response.artist[0] - if (first_artist === undefined) { - throw new ScriptException("missing artist") + const content_type: ContentType = maybe_content_type as ContentType + const content_uri_id = match_result[2] + if (content_uri_id === undefined) { + throw new ScriptException("regex error") } + switch (content_type) { + case "track": { + const song_url = `${SONG_URL_PREFIX}${content_uri_id}` + + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(content_uri_id) + const song_metadata_response: SongMetadataResponse = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body) + const first_artist = song_metadata_response.artist[0] + if (first_artist === undefined) { + throw new ScriptException("missing artist") + } + const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m" + + const format = is_premium() ? "MP4_256" : "MP4_128" + + const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format })?.file_id + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format") + } + + const { url, headers } = file_manifest_args(maybe_file_id) + const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const duration = song_metadata_response.duration / 1000 + + const file_url = file_manifest.cdnurl[0] + if (file_url === undefined) { + throw new ScriptException("unreachable") + } + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: format, + bitrate: HARDCODED_ZERO, + container: "audio/mp4", + codecs: "mp4a.40.2", + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })] + + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: song_metadata_response.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, artist_url), + url: song_url, + thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { + return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height) + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: song_metadata_response.canonical_uri, + // readonly uploadDate?: number + description: HARDCODED_EMPTY_STRING, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO) + // readonly subtitles?: ISubtitleSource[] + }) + } + + case "episode": { + const episode_url = `https://open.spotify.com/episode/${content_uri_id}` + + const { url: transcript_url, headers: transcript_headers } = transcript_args(content_uri_id) + const { url, headers } = episode_metadata_args(content_uri_id) + const responses = local_http.batch() + .GET(transcript_url, transcript_headers, false) + .GET(url, headers, false) + .execute() + if (responses[0] === undefined || responses[1] === undefined) { + throw new ScriptException("unreachable") + } + const transcript_response: TranscriptResponse = JSON.parse(responses[0].body) + const episode_metadata_response: EpisodeMetadataResponse = JSON.parse(responses[1].body) + + const format = "MP4_128" + const maybe_file_id = episode_metadata_response.data.episodeUnionV2.audio.items.find(function (file) { return file.format === format })?.fileId + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format") + } + + const { url: manifest_url, headers: manifest_headers } = file_manifest_args(maybe_file_id) + const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(manifest_url, manifest_headers, false).body) + + const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000 + + const file_url = file_manifest.cdnurl[0] + if (file_url === undefined) { + throw new ScriptException("unreachable") + } + const codecs = "mp4a.40.2" + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: codecs, + bitrate: 128000, + container: "audio/mp4", + codecs, + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })] + + const subtitle_name = function () { + switch (transcript_response.language) { + case "en": + return "English" + default: + throw assert_no_fall_through(transcript_response.language, "unreachable") + } + }() + + let vtt_text = `WEBVTT ${subtitle_name}\n` + vtt_text += "\n" + transcript_response.section.forEach(function (section, index) { + if ("title" in section) { + return + } + const next = transcript_response.section[index + 1] + let end = next?.startMs + if (end === undefined) { + end = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds + } + vtt_text += `${milliseconds_to_WebVTT_timestamp(section.startMs)} --> ${milliseconds_to_WebVTT_timestamp(end)}\n` + vtt_text += `${section.text.sentence.text}\n` + vtt_text += "\n" + }) + + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: episode_metadata_response.data.episodeUnionV2.name, + author: EMPTY_AUTHOR, + url: episode_url, + thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { + return new Thumbnail(image.url, image.height) + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: episode_metadata_response.data.episodeUnionV2.uri, + uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + description: episode_metadata_response.data.episodeUnionV2.htmlDescription, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO), + subtitles: [{ + url: episode_url, + name: subtitle_name, + getSubtitles() { + return vtt_text + }, + format: "text/vtt", + }] + }) + } - const format = "MP4_128" + default: + throw assert_no_fall_through(content_type, "unreachable") + } +} - const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format })?.file_id - if (maybe_file_id === undefined) { - throw new ScriptException("missing expected format") +function transcript_args(episode_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const transcript_url_prefix = "https://spclient.wg.spotify.com/transcript-read-along/v2/episode/" + const url = new URL(`${transcript_url_prefix}${episode_uri_id}`) + url.searchParams.set("format", "json") + return { + url: url.toString(), + headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +function file_manifest_args(file_id: string): { readonly url: string, readonly headers: { Authorization: string } } { const file_manifest_url_prefix = "https://gue1-spclient.spotify.com/storage-resolve/v2/files/audio/interactive/10/" const file_manifest_params = "?product=9&alt=json" - const file_manifest: FileManifestResponse = JSON.parse( - local_http.GET( - `${file_manifest_url_prefix}${maybe_file_id}${file_manifest_params}`, - { Authorization: `Bearer ${local_state.bearer_token}` }, - false - ).body - ) - - log(file_manifest) - - const duration = song_metadata_response.duration / 1000 - - const audio_sources = file_manifest.cdnurl.map(function (url) { - return new AudioUrlSource({ - //audio/mp4; codecs="mp4a.40.2 - name: format, - bitrate: HARDCODED_ZERO, - container: "audio/mp4", - codecs: "mp4a.40.2", - duration, - url, - language: Language.UNKNOWN, - }) - }) - - //https://seektables.scdn.co/seektable/4c652e57fd36f84d77af2b9d1d1332327a8fd774.json - const seektable_url_prefix = "https://seektables.scdn.co/seektable/" - - const seektable_response: SeektableResponse = JSON.parse( - local_http.GET( - `${seektable_url_prefix}${maybe_file_id}.json`, - {}, - false - ).body - ) - - log(seektable_response) - - const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0" - - const get_license_response: GetLicenseResponse = JSON.parse( - local_http.GET( - get_license_url_url, - { Authorization: `Bearer ${local_state.bearer_token}` }, - false - ).body - ) - - log(get_license_response) - - const license_url = `https://gue1-spclient.spotify.com/${get_license_response.uri}` - - log(license_url) + return { + url: `${file_manifest_url_prefix}${file_id}${file_manifest_params}`, + headers: { Authorization: `Bearer ${local_state.bearer_token}` } + } +} - return new PlatformVideoDetails({ - id: new PlatformID(PLATFORM, maybe_song_uri_id, plugin.config.id), - name: song_metadata_response.name, - author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), - url: song_url, - thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { - return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height) - })), - duration, - viewCount: HARDCODED_ZERO, - isLive: false, - shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number - description: HARDCODED_EMPTY_STRING, - video: new UnMuxVideoSourceDescriptor([], audio_sources), - // readonly live?: IVideoSource - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] +function episode_metadata_args(episode_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const episode_metadata_url_prefix = "https://api-partner.spotify.com/pathfinder/v1/query" + const variables = JSON.stringify({ + uri: `spotify:episode:${episode_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" + } }) + const url = new URL(episode_metadata_url_prefix) + url.searchParams.set("operationName", "getEpisodeOrChapter") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } } -function get_song_metadata(song_uri_id: string): SongMetadataResponse { +function song_metadata_args(song_uri_id: string): { + readonly url: string, + readonly headers: { + Authorization: string, + Accept: "application/json" + } +} { const song_metadata_url = "https://spclient.wg.spotify.com/metadata/4/track/" - const song_metadata_response: SongMetadataResponse = JSON.parse( - local_http.GET( - `${song_metadata_url}${get_gid(song_uri_id)}`, - { - Authorization: `Bearer ${local_state.bearer_token}`, - Accept: "application/json" - }, - false - ).body - ) - return song_metadata_response + return { + url: `${song_metadata_url}${get_gid(song_uri_id)}`, + headers: { + Authorization: `Bearer ${local_state.bearer_token}`, + Accept: "application/json" + } + } +} +//#endregion + +//#region utilities +/** + * Converts seconds to the timestamp format used in WebVTT + * @param seconds + * @returns + */ +function milliseconds_to_WebVTT_timestamp(milliseconds: number) { + return new Date(milliseconds).toISOString().substring(11, 23) } -// function assert_never(value: never) { -// log(value) -// } +function assert_never(value: never) { + log(value) +} + +function log_passthrough<T>(value: T): T { + log(value) + return value +} +function assert_no_fall_through(value: never): void +function assert_no_fall_through(value: never, exception_message: string): ScriptException +function assert_no_fall_through(value: never, exception_message?: string): ScriptException | undefined { + log(["Spotify log:", value]) + if (exception_message !== undefined) { + return new ScriptException(exception_message) + } + return +} +//#endregion + +function is_premium(): boolean { + return false +} -// function log_passthrough<T>(value: T): T { -// log(value) -// return value -// } // https://open.spotifycdn.com/cdn/build/web-player/vendor~web-player.391a2438.js const Z = "0123456789abcdef" @@ -383,6 +562,10 @@ function get_gid(song_uri_id: string) { }(song_uri_id) : song_uri_id } +// export statements are removed during build step +// used for unit testing in SpotifyScript.test.ts // export { - get_gid + get_gid, + assert_never, + log_passthrough } diff --git a/package-lock.json b/package-lock.json index 65e22bb2bc489dc8a485c3657bf1adfbddddfc5c..84fd98a119a942d5f0cea36d5f0e498487146c82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "1.0.0", "license": "MPL-2.0", "devDependencies": { - "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#d898f236620e6b5b4d54053c1b15be24bd68a8c1", + "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", "@types/node": "^20.12.7", "http-server": "^14.1.1", - "npm-check-updates": "^16.14.18" + "npm-check-updates": "^16.14.20" }, "engines": { "node": ">=20.0.0" @@ -39,8 +39,8 @@ }, "node_modules/@grayjay/plugin": { "version": "1.0.0", - "resolved": "git+ssh://git@gitlab.com/kaidelorenzo/grayjay-plugin-types.git#d898f236620e6b5b4d54053c1b15be24bd68a8c1", - "integrity": "sha512-c7GSfB625zIWnxEjdP7PQr8Jalp9hla5h46I6cx9ircACAlWVM7MdtFcuJS+6/SIPvlDyLQz/3k512bqMYPDtg==", + "resolved": "git+ssh://git@gitlab.com/kaidelorenzo/grayjay-plugin-types.git#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", + "integrity": "sha512-wvYWXupDyr4kKlTCJbuAuEvi48gRx/RpX6j31m4KeRukCBdV/QuHeS3L+GRVRXIU/ltBraEiNLo2CiMIxY2qiw==", "dev": true, "dependencies": { "@types/sync-fetch": "^0.4.3", @@ -3328,9 +3328,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", - "integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { "node": "14 || >=16.14" diff --git a/package.json b/package.json index d41d34c81290e3436a7d590622ae7d2cd58ad2f2..f1e2f1aebba9f24386a20317dab677c496b3971e 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ }, "type": "module", "devDependencies": { - "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#d898f236620e6b5b4d54053c1b15be24bd68a8c1", + "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", "@types/node": "^20.12.7", "http-server": "^14.1.1", - "npm-check-updates": "^16.14.18" + "npm-check-updates": "^16.14.20" } } diff --git a/src/SpotifyScript.ts b/src/SpotifyScript.ts index b19a7c895380b3581889f96e834fb491df98ab8a..6fb3db001b3dbd6625e39d4ba79a13f2e7b68517 100644 --- a/src/SpotifyScript.ts +++ b/src/SpotifyScript.ts @@ -1,14 +1,17 @@ +//#region constants import { - FileManifestResponse, - GetLicenseResponse, - SeektableResponse, - Settings, - SongMetadataResponse, - // SpotifySource, - State + type ContentType, + type EpisodeMetadataResponse, + type FileManifestResponse, + type GetLicenseResponse, + type Settings, + type SongMetadataResponse, + // type SpotifySource, + type State, + type TranscriptResponse, } from "./types.js" -const SONG_REGEX = /^https:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]*)($|\/)/ +const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/ const SONG_URL_PREFIX = "https://open.spotify.com/track/" as const const IMAGE_URL_PREFIX = "https://i.scdn.co/image/" as const @@ -17,6 +20,7 @@ const PLATFORM = "Spotify" as const const HARDCODED_ZERO = 0 as const const HARDCODED_EMPTY_STRING = "" as const +const EMPTY_AUTHOR = new PlatformAuthorLink(new PlatformID(PLATFORM, "", plugin.config.id), "", "") const local_http = http // const local_utility = utility @@ -28,6 +32,7 @@ Type.Order.Favorites = "Most favorited" /** State */ let local_state: State +//#endregion //#region source methods source.enable = enable @@ -119,7 +124,9 @@ if (IS_TESTING) { } } */ +//#endregion +//#region enable function enable(conf: SourceConfig, settings: Settings, savedState: string | null) { if (IS_TESTING) { log("IS_TESTING true") @@ -134,10 +141,14 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul const state: State = JSON.parse(savedState) local_state = state } else { - const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" - const song_url = `${SONG_URL_PREFIX}${song_uri_id}` - const song_html_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/ - const match_result = local_http.GET(song_url, {}, false).body.match(song_html_regex) + // download bearer token + const homepage_url = "https://open.spotify.com" + const bearer_token_regex = /<script id="session" data-testid="session" type="application\/json">({.*?})<\/script><script id="features" type="application\/json">/ + + // use the authenticated client to get a logged in bearer token + const homepage_response = local_http.GET(homepage_url, {}, true) + + const match_result = homepage_response.body.match(bearer_token_regex) if (match_result === null) { throw new ScriptException("regex error") } @@ -146,23 +157,45 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul throw new ScriptException("regex error") } const token_response: { accessToken: string } = JSON.parse(maybe_json) - local_state = { bearer_token: token_response.accessToken } + const bearer_token = token_response.accessToken + + + // download license uri + const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0" + const get_license_url_response = local_http.GET( + get_license_url_url, + { Authorization: `Bearer ${bearer_token}` }, + false + ) + const get_license_response: GetLicenseResponse = JSON.parse( + get_license_url_response.body + ) + const license_uri = `https://gue1-spclient.spotify.com/${get_license_response.uri}` + + + local_state = { + bearer_token, + license_uri: license_uri + } } } +//#endregion function disable() { - log("BiliBili log: disabling") + log("Spotify log: disabling") } function saveState() { return JSON.stringify(local_state) } +//#region home function getHome() { const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" const song_url = `${SONG_URL_PREFIX}${song_uri_id}` - const song_metadata_response: SongMetadataResponse = get_song_metadata(song_uri_id) + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(song_uri_id) + const song_metadata_response: SongMetadataResponse = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body) const first_artist = song_metadata_response.artist[0] if (first_artist === undefined) { throw new ScriptException("missing artist") @@ -184,136 +217,282 @@ function getHome() { })] return new VideoPager(songs, false) } +//#endregion +//#region content // https://open.spotify.com/track/6XXxKsu3RJeN3ZvbMYrgQW +// https://open.spotify.com/episode/3Z88ZE0i3L7AIrymrBwtqg function isContentDetailsUrl(url: string) { - return SONG_REGEX.test(url) + return CONTENT_REGEX.test(url) } function getContentDetails(url: string) { - const match_result = url.match(SONG_REGEX) + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to listen to songs") + } + const match_result = url.match(CONTENT_REGEX) if (match_result === null) { throw new ScriptException("regex error") } - const maybe_song_uri_id = match_result[1] - if (maybe_song_uri_id === undefined) { + const maybe_content_type = match_result[1] + if (maybe_content_type === undefined) { throw new ScriptException("regex error") } - const song_url = `${SONG_URL_PREFIX}${maybe_song_uri_id}` - - const song_metadata_response: SongMetadataResponse = get_song_metadata(maybe_song_uri_id) - const first_artist = song_metadata_response.artist[0] - if (first_artist === undefined) { - throw new ScriptException("missing artist") + const content_type: ContentType = maybe_content_type as ContentType + const content_uri_id = match_result[2] + if (content_uri_id === undefined) { + throw new ScriptException("regex error") } + switch (content_type) { + case "track": { + const song_url = `${SONG_URL_PREFIX}${content_uri_id}` + + const { url: metadata_url, headers: metadata_headers } = song_metadata_args(content_uri_id) + const song_metadata_response: SongMetadataResponse = JSON.parse(local_http.GET(metadata_url, metadata_headers, false).body) + const first_artist = song_metadata_response.artist[0] + if (first_artist === undefined) { + throw new ScriptException("missing artist") + } + const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m" + + const format = is_premium() ? "MP4_256" : "MP4_128" + + const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format })?.file_id + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format") + } + + const { url, headers } = file_manifest_args(maybe_file_id) + const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const duration = song_metadata_response.duration / 1000 + + const file_url = file_manifest.cdnurl[0] + if (file_url === undefined) { + throw new ScriptException("unreachable") + } + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: format, + bitrate: HARDCODED_ZERO, + container: "audio/mp4", + codecs: "mp4a.40.2", + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })] + + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: song_metadata_response.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, artist_url), + url: song_url, + thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { + return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height) + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: song_metadata_response.canonical_uri, + // readonly uploadDate?: number + description: HARDCODED_EMPTY_STRING, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO) + // readonly subtitles?: ISubtitleSource[] + }) + } + + case "episode": { + const episode_url = `https://open.spotify.com/episode/${content_uri_id}` + + const { url: transcript_url, headers: transcript_headers } = transcript_args(content_uri_id) + const { url, headers } = episode_metadata_args(content_uri_id) + const responses = local_http.batch() + .GET(transcript_url, transcript_headers, false) + .GET(url, headers, false) + .execute() + if (responses[0] === undefined || responses[1] === undefined) { + throw new ScriptException("unreachable") + } + const transcript_response: TranscriptResponse = JSON.parse(responses[0].body) + const episode_metadata_response: EpisodeMetadataResponse = JSON.parse(responses[1].body) + + const format = "MP4_128" + const maybe_file_id = episode_metadata_response.data.episodeUnionV2.audio.items.find(function (file) { return file.format === format })?.fileId + if (maybe_file_id === undefined) { + throw new ScriptException("missing expected format") + } + + const { url: manifest_url, headers: manifest_headers } = file_manifest_args(maybe_file_id) + const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(manifest_url, manifest_headers, false).body) + + const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000 + + const file_url = file_manifest.cdnurl[0] + if (file_url === undefined) { + throw new ScriptException("unreachable") + } + const codecs = "mp4a.40.2" + const audio_sources = [new AudioUrlWidevineSource({ + //audio/mp4; codecs="mp4a.40.2 + name: codecs, + bitrate: 128000, + container: "audio/mp4", + codecs, + duration, + url: file_url, + language: Language.UNKNOWN, + bearerToken: local_state.bearer_token, + licenseUri: local_state.license_uri + })] + + const subtitle_name = function () { + switch (transcript_response.language) { + case "en": + return "English" + default: + throw assert_no_fall_through(transcript_response.language, "unreachable") + } + }() + + let vtt_text = `WEBVTT ${subtitle_name}\n` + vtt_text += "\n" + transcript_response.section.forEach(function (section, index) { + if ("title" in section) { + return + } + const next = transcript_response.section[index + 1] + let end = next?.startMs + if (end === undefined) { + end = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds + } + vtt_text += `${milliseconds_to_WebVTT_timestamp(section.startMs)} --> ${milliseconds_to_WebVTT_timestamp(end)}\n` + vtt_text += `${section.text.sentence.text}\n` + vtt_text += "\n" + }) + + return new PlatformVideoDetails({ + id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), + name: episode_metadata_response.data.episodeUnionV2.name, + author: EMPTY_AUTHOR, + url: episode_url, + thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { + return new Thumbnail(image.url, image.height) + })), + duration, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: episode_metadata_response.data.episodeUnionV2.uri, + uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + description: episode_metadata_response.data.episodeUnionV2.htmlDescription, + video: new UnMuxVideoSourceDescriptor([], audio_sources), + rating: new RatingLikes(HARDCODED_ZERO), + subtitles: [{ + url: episode_url, + name: subtitle_name, + getSubtitles() { + return vtt_text + }, + format: "text/vtt", + }] + }) + } - const format = "MP4_128" + default: + throw assert_no_fall_through(content_type, "unreachable") + } +} - const maybe_file_id = song_metadata_response.file.find(function (file) { return file.format === format })?.file_id - if (maybe_file_id === undefined) { - throw new ScriptException("missing expected format") +function transcript_args(episode_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const transcript_url_prefix = "https://spclient.wg.spotify.com/transcript-read-along/v2/episode/" + const url = new URL(`${transcript_url_prefix}${episode_uri_id}`) + url.searchParams.set("format", "json") + return { + url: url.toString(), + headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +function file_manifest_args(file_id: string): { readonly url: string, readonly headers: { Authorization: string } } { const file_manifest_url_prefix = "https://gue1-spclient.spotify.com/storage-resolve/v2/files/audio/interactive/10/" const file_manifest_params = "?product=9&alt=json" - const file_manifest: FileManifestResponse = JSON.parse( - local_http.GET( - `${file_manifest_url_prefix}${maybe_file_id}${file_manifest_params}`, - { Authorization: `Bearer ${local_state.bearer_token}` }, - false - ).body - ) - - log(file_manifest) - - const duration = song_metadata_response.duration / 1000 - - const audio_sources = file_manifest.cdnurl.map(function (url) { - return new AudioUrlSource({ - //audio/mp4; codecs="mp4a.40.2 - name: format, - bitrate: HARDCODED_ZERO, - container: "audio/mp4", - codecs: "mp4a.40.2", - duration, - url, - language: Language.UNKNOWN, - }) - }) - - //https://seektables.scdn.co/seektable/4c652e57fd36f84d77af2b9d1d1332327a8fd774.json - const seektable_url_prefix = "https://seektables.scdn.co/seektable/" - - const seektable_response: SeektableResponse = JSON.parse( - local_http.GET( - `${seektable_url_prefix}${maybe_file_id}.json`, - {}, - false - ).body - ) - - log(seektable_response) - - const get_license_url_url = "https://gue1-spclient.spotify.com/melody/v1/license_url?keysystem=com.widevine.alpha&sdk_name=harmony&sdk_version=4.41.0" - - const get_license_response: GetLicenseResponse = JSON.parse( - local_http.GET( - get_license_url_url, - { Authorization: `Bearer ${local_state.bearer_token}` }, - false - ).body - ) - - log(get_license_response) - - const license_url = `https://gue1-spclient.spotify.com/${get_license_response.uri}` - - log(license_url) + return { + url: `${file_manifest_url_prefix}${file_id}${file_manifest_params}`, + headers: { Authorization: `Bearer ${local_state.bearer_token}` } + } +} - return new PlatformVideoDetails({ - id: new PlatformID(PLATFORM, maybe_song_uri_id, plugin.config.id), - name: song_metadata_response.name, - author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.gid, plugin.config.id), first_artist.name, "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), - url: song_url, - thumbnails: new Thumbnails(song_metadata_response.album.cover_group.image.map(function (image) { - return new Thumbnail(`${IMAGE_URL_PREFIX}${image.file_id}`, image.height) - })), - duration, - viewCount: HARDCODED_ZERO, - isLive: false, - shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number - description: HARDCODED_EMPTY_STRING, - video: new UnMuxVideoSourceDescriptor([], audio_sources), - // readonly live?: IVideoSource - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] +function episode_metadata_args(episode_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const episode_metadata_url_prefix = "https://api-partner.spotify.com/pathfinder/v1/query" + const variables = JSON.stringify({ + uri: `spotify:episode:${episode_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" + } }) + const url = new URL(episode_metadata_url_prefix) + url.searchParams.set("operationName", "getEpisodeOrChapter") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } } -function get_song_metadata(song_uri_id: string): SongMetadataResponse { +function song_metadata_args(song_uri_id: string): { + readonly url: string, + readonly headers: { + Authorization: string, + Accept: "application/json" + } +} { const song_metadata_url = "https://spclient.wg.spotify.com/metadata/4/track/" - const song_metadata_response: SongMetadataResponse = JSON.parse( - local_http.GET( - `${song_metadata_url}${get_gid(song_uri_id)}`, - { - Authorization: `Bearer ${local_state.bearer_token}`, - Accept: "application/json" - }, - false - ).body - ) - return song_metadata_response + return { + url: `${song_metadata_url}${get_gid(song_uri_id)}`, + headers: { + Authorization: `Bearer ${local_state.bearer_token}`, + Accept: "application/json" + } + } +} +//#endregion + +//#region utilities +/** + * Converts seconds to the timestamp format used in WebVTT + * @param seconds + * @returns + */ +function milliseconds_to_WebVTT_timestamp(milliseconds: number) { + return new Date(milliseconds).toISOString().substring(11, 23) } -// function assert_never(value: never) { -// log(value) -// } +function assert_never(value: never) { + log(value) +} + +function log_passthrough<T>(value: T): T { + log(value) + return value +} +function assert_no_fall_through(value: never): void +function assert_no_fall_through(value: never, exception_message: string): ScriptException +function assert_no_fall_through(value: never, exception_message?: string): ScriptException | undefined { + log(["Spotify log:", value]) + if (exception_message !== undefined) { + return new ScriptException(exception_message) + } + return +} +//#endregion + +function is_premium(): boolean { + return false +} -// function log_passthrough<T>(value: T): T { -// log(value) -// return value -// } // https://open.spotifycdn.com/cdn/build/web-player/vendor~web-player.391a2438.js const Z = "0123456789abcdef" @@ -383,6 +562,10 @@ function get_gid(song_uri_id: string) { }(song_uri_id) : song_uri_id } +// export statements are removed during build step +// used for unit testing in SpotifyScript.test.ts export { - get_gid + get_gid, + assert_never, + log_passthrough } diff --git a/src/types.ts b/src/types.ts index aa051b39b72bc351a85bfa5043d2537993d4e831..4213704d8d707629dbe3f8c057acc6166ca2b7be 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +//#region custom types export type Settings = unknown export type SpotifySource = Required<Source< @@ -10,6 +11,55 @@ export type SpotifySource = Required<Source< export type State = { readonly bearer_token: string + readonly license_uri: string +} +//#endregion + +//#region JSON types +export type ContentType = "track" | "episode" + +export type TranscriptResponse = { + readonly section: ({ + readonly startMs: number + readonly text: { + readonly sentence: { + readonly text: string + } + } + } | { + readonly startMs: number + readonly title: unknown + })[] + readonly language: "en" +} + +export type EpisodeMetadataResponse = { + readonly data: { + readonly episodeUnionV2: { + readonly name: string + readonly duration: { + readonly totalMilliseconds: number + } + readonly coverArt: { + readonly sources: { + readonly url: string + readonly height: number + }[] + } + readonly releaseDate: { + readonly isoString: string + } + readonly uri: string + readonly audio: { + readonly items: { + readonly fileId: string + // only MP4_128 and MP4_256 are available on the web and therefore what we support + readonly format: "MP4_128" | "AAC_24" + }[] + } + readonly htmlDescription: string + } + } } export type SongMetadataResponse = { @@ -36,7 +86,8 @@ export type SongMetadataResponse = { readonly canonical_uri: string readonly file: { readonly file_id: string - readonly format: "MP4_128" | "AAC_24" + // only MP4_128 and MP4_256 are available on the web and therefore what we support + readonly format: "MP4_128" | "AAC_24" | "MP4_256" | "MP4_256_DUAL" | "OGG_VORBIS_320" }[] } @@ -61,3 +112,4 @@ export type GetLicenseResponse = { readonly expires: number readonly uri: string } +//#endregion