From b696a5b787818bba5bd983dbaab76af40ef35e26 Mon Sep 17 00:00:00 2001 From: Kai DeLorenzo <git@seaoflaurels.com> Date: Wed, 1 May 2024 17:47:49 -0500 Subject: [PATCH] added initial playlist support --- README.md | 14 +- build/SpotifyConfig.json | 3 +- build/SpotifyScript.js | 603 +++++++++++++++++++++++++++++++--- build/SpotifyScript.js.map | 2 +- build/SpotifyScript.ts | 639 +++++++++++++++++++++++++++++++++++-- package-lock.json | 12 +- package.json | 2 +- src/SpotifyScript.ts | 639 +++++++++++++++++++++++++++++++++++-- src/types.ts | 217 ++++++++++++- 9 files changed, 2019 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index d8b7158..ee31f0f 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,13 @@ ## TO-DO - [ ] check that share urls/uris work and share into the spotify app +- [ ] an entire podcast has a rating. maybe give the episode the podcast rating use this to load it https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryShowMetadataV2&variables=%7B%22uri%22%3A%22spotify%3Ashow%3A5VzFvh1JlEhBMS6ZHZ8CNO%22%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%225fb034a236a3e8301e9eca0e23def3341ed66c891ea2d4fea374c091dc4b4a6a%22%7D%7D +- [ ] there is data about the number of people following a podcast somewhere that shows up in the mobile app +- [ ] test the logged out version of the plugin ## Grayjay Bugs -- [ ] none - - - - - +- [ ] RatingScaler doesn't work +- [ ] websockets are hard to use +- [ ] datetime doesn't display for playlists +- [ ] there is no way to get to the creator of a playlist +- [ ] the pager that goes in the contents property of a playlist doesn't ever call the nextPage method \ No newline at end of file diff --git a/build/SpotifyConfig.json b/build/SpotifyConfig.json index 10df0b7..6951090 100644 --- a/build/SpotifyConfig.json +++ b/build/SpotifyConfig.json @@ -22,7 +22,8 @@ "spclient.wg.spotify.com", "gue1-spclient.spotify.com", "seektables.scdn.co", - "api-partner.spotify.com" + "api-partner.spotify.com", + "apresolve.spotify.com" ], "authentication": { "loginUrl": "https://accounts.spotify.com/en/login", diff --git a/build/SpotifyScript.js b/build/SpotifyScript.js index 34b9ecc..53fa2cc 100644 --- a/build/SpotifyScript.js +++ b/build/SpotifyScript.js @@ -1,11 +1,15 @@ const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/; +const PLAYLIST_REGEX = /^https:\/\/open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]*)($|\/)/; const SONG_URL_PREFIX = "https://open.spotify.com/track/"; +const PODCAST_URL_PREFIX = "https://open.spotify.com/show/"; +const ARTIST_URL_PREFIX = "https://open.spotify.com/artist/"; +const ALBUM_URL_PREFIX = "https://open.spotify.com/album/"; +const QUERY_URL = "https://api-partner.spotify.com/pathfinder/v1/query"; 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 @@ -32,9 +36,9 @@ source.getHome = getHome; // source.searchChannelContents = searchChannelContents source.isContentDetailsUrl = isContentDetailsUrl; source.getContentDetails = getContentDetails; -// source.isPlaylistUrl = isPlaylistUrl +source.isPlaylistUrl = isPlaylistUrl; // source.searchPlaylists = searchPlaylists -// source.getPlaylist = getPlaylist +source.getPlaylist = getPlaylist; // source.getComments = getComments // source.getSubComments = getSubComments // source.getLiveChatWindow = getLiveChatWindow @@ -139,6 +143,12 @@ function enable(conf, settings, savedState) { license_uri: license_uri }; } + if (is_premium()) { + local_state = { + bearer_token: "BQB5uzdWBsXahudafNcc3RR7kExwq4vpsbkSCOuGkn06aYQ8it6x-M5PmaVi2gapw5NgXMO4tlDSenQcCqv2dQg94a_4fsi11yX5qkAeqW0f_bRNHZ3cg1QlJgX8kKnOmEs5I8jmhY2pR0k8ParLvLZt7tVQYVceei3NJM4w4oKr6thqYyCST-3BHJximVhvT5_cmMrFac5VBWkgioQPxNUSO1U6ICi0hN2W5WMYg8KjdrjCPKfFiYTE3Z9myO0fGI13o1uWzNRrXHc075HuOYvvv_5UbobXPyPVbSEfLqGuaPstmN8Ubj7XV6FXYPnvNSNnJlLx1GwLx2EoyA", + license_uri: local_state.license_uri + }; + } } //#endregion function disable() { @@ -149,30 +159,72 @@ function saveState() { } //#region home function getHome() { - const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW"; - const song_url = `${SONG_URL_PREFIX}${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]; + const playlists = [ + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "the coolest album of all time", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "dayly mix mix 1111", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "tines for two", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/4ClcTeCoE9aPMhy0CLoD9P", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }) + ]; + return new ContentPager(playlists, false); + /* + const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" + const song_url = `${SONG_URL_PREFIX}${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"); + throw new ScriptException("missing artist") } //https://spclient.wg.spotify.com/metadata/4/track/e4eac7232f3d48fb965b5a03c49eb93a const songs = [new PlatformVideo({ - id: new PlatformID(PLATFORM, 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: song_metadata_response.duration / 1000, - viewCount: HARDCODED_ZERO, - isLive: false, - shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number - })]; - return new VideoPager(songs, false); + id: new PlatformID(PLATFORM, 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: song_metadata_response.duration / 1000, + viewCount: HARDCODED_ZERO, + isLive: false, + shareUrl: song_metadata_response.canonical_uri, + // readonly uploadDate?: number + })] + return new VideoPager(songs, false) + */ } //#endregion //#region content @@ -202,30 +254,101 @@ function getContentDetails(url) { 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]; + const { url: track_metadata_url, headers: _track_metadata_headers } = track_metadata_args(content_uri_id); + const batch = local_http + .batch() + .GET(metadata_url, metadata_headers, false) + .GET(track_metadata_url, _track_metadata_headers, false); + if (is_premium()) { + const { url, headers } = lyrics_args(content_uri_id); + batch.GET(url, headers, false); + } + const results = batch + .execute(); + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable"); + } + const song_metadata_response = JSON.parse(results[0].body); + const track_metadata_response = JSON.parse(results[1].body); + const first_artist = track_metadata_response.data.trackUnion.firstArtist.items[0]; if (first_artist === undefined) { throw new ScriptException("missing artist"); } - const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"; + const artist_url = `https://open.spotify.com/artist/${first_artist.id}`; + const highest_quality_artist_cover_art = first_artist.visuals.avatarImage.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current; + }); + let subtitles = []; + if (results[2] !== undefined) { + const lyrics_response = JSON.parse(results[2].body); + const subtitle_name = function () { + switch (lyrics_response.lyrics.language) { + case "en": + return "English"; + default: + throw assert_no_fall_through(lyrics_response.lyrics.language, "unreachable"); + } + }(); + const convert = milliseconds_to_WebVTT_timestamp; + let vtt_text = `WEBVTT ${subtitle_name}\n`; + vtt_text += "\n"; + lyrics_response.lyrics.lines.forEach(function (line, index) { + const next = lyrics_response.lyrics.lines[index + 1]; + let end = next?.startTimeMs; + if (end === undefined) { + end = track_metadata_response.data.trackUnion.duration.totalMilliseconds.toString(); + } + vtt_text += `${convert(parseInt(line.startTimeMs))} --> ${convert(parseInt(end))}\n`; + vtt_text += `${line.words}\n`; + vtt_text += "\n"; + }); + subtitles = [{ + url: song_url, + name: subtitle_name, + getSubtitles() { + return vtt_text; + }, + format: "text/vtt", + }]; + } 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 { url: artist_metadata_url, headers: artist_metadata_headers } = artist_metadata_args(first_artist.id); + const second_results = local_http + .batch() + .GET(url, headers, false) + .GET(artist_metadata_url, artist_metadata_headers, false) + .execute(); + if (second_results[0] === undefined || second_results[1] === undefined) { + throw new ScriptException("unreachable"); + } + const file_manifest = JSON.parse(second_results[0].body); + const artist_metadata_response = JSON.parse(second_results[1].body); + const duration = track_metadata_response.data.trackUnion.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: format, - bitrate: HARDCODED_ZERO, + name: codecs, + bitrate: function (format) { + switch (format) { + case "MP4_128": + return 128000; + case "MP4_256": + return 256000; + default: + throw assert_no_fall_through(format, "unreachable"); + } + }(format), container: "audio/mp4", - codecs: "mp4a.40.2", + codecs, duration, url: file_url, language: Language.UNKNOWN, @@ -235,20 +358,20 @@ function getContentDetails(url) { 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), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, first_artist.id, plugin.config.id), first_artist.profile.name, artist_url, highest_quality_artist_cover_art.url, artist_metadata_response.data.artistUnion.stats.monthlyListeners), 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, + viewCount: parseInt(track_metadata_response.data.trackUnion.playcount), isLive: false, shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number + datetime: new Date(track_metadata_response.data.trackUnion.albumOfTrack.date.isoString).getTime() / 1000, description: HARDCODED_EMPTY_STRING, video: new UnMuxVideoSourceDescriptor([], audio_sources), - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] + rating: new RatingLikes(HARDCODED_ZERO), + subtitles }); } case "episode": { @@ -264,13 +387,56 @@ function getContentDetails(url) { } const transcript_response = JSON.parse(responses[0].body); const episode_metadata_response = JSON.parse(responses[1].body); + if (episode_metadata_response.data.episodeUnionV2.mediaTypes.length === 2) { + function assert_video(_mediaTypes) { } + assert_video(episode_metadata_response.data.episodeUnionV2.mediaTypes); + //TODO since we don't use the transcript we should only load it when audio only podcasts are played + // TODO handle video podcasts. Grayjay doesn't currently support the websocket functionality necessary + // the basic process to get the video play info is + // connect to the websocket wss://gue1-dealer.spotify.com/?access_token=<bearer-token> + // register the device https://gue1-spclient.spotify.com/track-playback/v1/devices + // generate the device id using code found in the min js like this + /* + web player js + const t = Math.ceil(e / 2); + return function(e) { + let t = ""; + for (let n = 0; n < e.length; n++) { + const i = e[n]; + i < 16 && (t += "0"), + t += i.toString(16) + } + return t + }(Oe(t)) + */ + // load devices info https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_aced97d86694f14d304dd4e6f1f7f8c3bff + // transfer to our device https://gue1-spclient.spotify.com/connect-state/v1/connect/transfer/from/9a7079bd5b5605839c1d9080d0f4368bfcd6d2eb/to/aced97d86694f14d304dd4e6f1f7f8c3bff + // signal the play of the given podcast (not quite sure how this works :/) + // recieve the video play info via the websocket connection + // + } 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 limited_podcast_metadata = episode_metadata_response.data.episodeUnionV2.podcastV2.data; + const podcast_uri_id = id_from_uri(limited_podcast_metadata.uri); + const highest_quality_cover_art = limited_podcast_metadata.coverArt.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current; + }); 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 { url: podcast_metadata_url, headers: podcast_metadata_headers } = podcast_metadata_args(podcast_uri_id); + const results = local_http + .batch() + .GET(podcast_metadata_url, podcast_metadata_headers, false) + .GET(manifest_url, manifest_headers, false) + .execute(); + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable"); + } + const full_podcast_metadata = JSON.parse(results[0].body); + const file_manifest = JSON.parse(results[1].body); const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000; const file_url = file_manifest.cdnurl[0]; if (file_url === undefined) { @@ -315,7 +481,7 @@ function getContentDetails(url) { return new PlatformVideoDetails({ id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), name: episode_metadata_response.data.episodeUnionV2.name, - author: EMPTY_AUTHOR, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, podcast_uri_id, plugin.config.id), limited_podcast_metadata.name, `${PODCAST_URL_PREFIX}${podcast_uri_id}`, highest_quality_cover_art.url), url: episode_url, thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { return new Thumbnail(image.url, image.height); @@ -324,10 +490,10 @@ function getContentDetails(url) { viewCount: HARDCODED_ZERO, isLive: false, shareUrl: episode_metadata_response.data.episodeUnionV2.uri, - uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + datetime: 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), + rating: new RatingScaler(full_podcast_metadata.data.podcastUnionV2.rating.averageRating.average), subtitles: [{ url: episode_url, name: subtitle_name, @@ -342,6 +508,22 @@ function getContentDetails(url) { throw assert_no_fall_through(content_type, "unreachable"); } } +function podcast_metadata_args(podcast_uri_id) { + const variables = JSON.stringify({ + uri: `spotify:show:${podcast_uri_id}` + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "5fb034a236a3e8301e9eca0e23def3341ed66c891ea2d4fea374c091dc4b4a6a" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "queryShowMetadataV2"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} 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}`); @@ -351,6 +533,17 @@ function transcript_args(episode_uri_id) { headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; } +function lyrics_args(song_uri_id) { + const url = new URL(`https://spclient.wg.spotify.com/color-lyrics/v2/track/${song_uri_id}`); + return { + url: url.toString(), + headers: { + Accept: "application/json", + "app-platform": "WebPlayer", + 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"; @@ -360,7 +553,6 @@ function file_manifest_args(file_id) { }; } 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}` }); @@ -370,12 +562,28 @@ function episode_metadata_args(episode_uri_id) { sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" } }); - const url = new URL(episode_metadata_url_prefix); + const url = new URL(QUERY_URL); 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 track_metadata_args(song_uri_id) { + const variables = JSON.stringify({ + uri: `spotify:track:${song_uri_id}` + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "ae85b52abb74d20a4c331d4143d4772c95f34757bfa8c625474b912b9055b5c0" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "getTrack"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} function song_metadata_args(song_uri_id) { const song_metadata_url = "https://spclient.wg.spotify.com/metadata/4/track/"; return { @@ -386,8 +594,317 @@ function song_metadata_args(song_uri_id) { } }; } +function artist_metadata_args(artist_uri_id) { + const variables = JSON.stringify({ + uri: `spotify:artist:${artist_uri_id}`, + locale: "", + includePrerelease: true + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "da986392124383827dc03cbb3d66c1de81225244b6e20f8d78f9f802cc43df6e" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "queryArtistOverview"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} +//#endregion +//#region playlists +// https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ +// https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT +function isPlaylistUrl(url) { + return PLAYLIST_REGEX.test(url); +} +function getPlaylist(url) { + const match_result = url.match(PLAYLIST_REGEX); + if (match_result === null) { + throw new ScriptException("regex error"); + } + const maybe_playlist_type = match_result[1]; + if (maybe_playlist_type === undefined) { + throw new ScriptException("regex error"); + } + const playlist_type = maybe_playlist_type; + const playlist_uri_id = match_result[2]; + if (playlist_uri_id === undefined) { + throw new ScriptException("regex error"); + } + switch (playlist_type) { + case "album": { + // if the author is the same as the album then include the artist pick otherwise nothing + // TODO we could load in extra info for all the other artists but it might be hard to do that in a request efficient way + const pagination_limit = 50; + const offset = 0; + const { url, headers } = album_metadata_args(playlist_uri_id, offset, pagination_limit); + const album_metadata_response = JSON.parse(local_http.GET(url, headers, false).body); + const album_artist = album_metadata_response.data.albumUnion.artists.items[0]; + if (album_artist === undefined) { + throw new ScriptException("missing album artist"); + } + const unix_time = new Date(album_metadata_response.data.albumUnion.date.isoString).getTime() / 1000; + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: album_metadata_response.data.albumUnion.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, album_artist.id, plugin.config.id), album_artist.profile.name, `${ARTIST_URL_PREFIX}${album_artist.id}`, album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url), + datetime: unix_time, + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: album_metadata_response.data.albumUnion.tracks.totalCount, + contents: new AlbumPager(playlist_uri_id, offset, pagination_limit, album_metadata_response, album_artist, unix_time) + }); + } + case "playlist": { + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to open playlists"); + } + const pagination_limit = 25; + const offset = 0; + const { url, headers } = fetch_playlist_args(playlist_uri_id, offset, pagination_limit); + const playlist_response = JSON.parse(local_http.GET(url, headers, false).body); + const owner = playlist_response.data.playlistV2.ownerV2.data; + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: playlist_response.data.playlistV2.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, owner.username, plugin.config.id), owner.name, `${ARTIST_URL_PREFIX}${owner.username}`, owner.avatar?.sources[owner.avatar.sources.length - 1]?.url), + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: playlist_response.data.playlistV2.content.totalCount, + contents: new SpotifyPlaylistPager(playlist_uri_id, offset, pagination_limit, playlist_response) + }); + } + default: { + throw assert_no_fall_through(playlist_type, "unreachable"); + } + } +} +class SpotifyPlaylistPager extends VideoPager { + playlist_uri_id; + pagination_limit; + offset; + total_tracks; + constructor(playlist_uri_id, offset, pagination_limit, playlist_response) { + const total_tracks = playlist_response.data.playlistV2.content.totalCount; + const songs = format_playlist_tracks(playlist_response.data.playlistV2.content); + super(songs, total_tracks > offset + pagination_limit); + this.playlist_uri_id = playlist_uri_id; + this.pagination_limit = pagination_limit; + this.offset = offset + pagination_limit; + this.total_tracks = total_tracks; + } + nextPage() { + const { url, headers } = fetch_playlist_contents_args(this.playlist_uri_id, this.offset, this.pagination_limit); + const playlist_content_response = JSON.parse(local_http.GET(url, headers, false).body); + const songs = format_playlist_tracks(playlist_content_response.data.playlistV2.content); + this.results = songs; + this.hasMore = this.total_tracks > this.offset + this.pagination_limit; + this.offset += this.pagination_limit; + return this; + } + hasMorePagers() { + return this.hasMore; + } +} +function format_playlist_tracks(content) { + return content.items.map(function (playlist_track_metadata) { + const song = playlist_track_metadata.itemV2.data; + const track_uri_id = id_from_uri(song.uri); + const artist = song.artists.items[0]; + if (artist === undefined) { + throw new ScriptException("missing artist"); + } + const url = `${SONG_URL_PREFIX}${track_uri_id}`; + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: song.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), artist.profile.name, `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}` + // TODO figure out a way to get the artist thumbnail + ), + url, + thumbnails: new Thumbnails(song.albumOfTrack.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height); + })), + duration: song.trackDuration.totalMilliseconds / 1000, + viewCount: parseInt(song.playcount), + isLive: false, + shareUrl: url, + datetime: new Date(playlist_track_metadata.addedAt.isoString).getTime() / 1000 + }); + }); +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_contents_args(playlist_uri_id, offset, limit) { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "fetchPlaylistContents"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_args(playlist_uri_id, offset, limit) { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "fetchPlaylist"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} +class AlbumPager extends VideoPager { + album_uri_id; + pagination_limit; + offset; + thumbnails; + album_artist; + unix_time; + total_tracks; + constructor(album_uri_id, offset, pagination_limit, album_metadata_response, album_artist, unix_time) { + const total_tracks = album_metadata_response.data.albumUnion.tracks.totalCount; + const thumbnails = new Thumbnails(album_metadata_response.data.albumUnion.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height); + })); + const songs = format_album_tracks(album_metadata_response.data.albumUnion.tracks, thumbnails, album_artist, unix_time); + super(songs, total_tracks > offset + pagination_limit); + this.album_uri_id = album_uri_id; + this.pagination_limit = pagination_limit; + this.offset = offset + pagination_limit; + this.thumbnails = thumbnails; + this.album_artist = album_artist; + this.unix_time = unix_time; + this.total_tracks = total_tracks; + } + nextPage() { + const { url, headers } = album_tracks_args(this.album_uri_id, this.offset, this.pagination_limit); + const album_tracks_response = JSON.parse(local_http.GET(url, headers, false).body); + const songs = format_album_tracks(album_tracks_response.data.albumUnion.tracks, this.thumbnails, this.album_artist, this.unix_time); + this.results = songs; + this.hasMore = this.total_tracks > this.offset + this.pagination_limit; + this.offset += this.pagination_limit; + return this; + } + hasMorePagers() { + return this.hasMore; + } +} +function format_album_tracks(tracks, thumbnails, album_artist, unix_time) { + return tracks.items.map(function (track) { + const track_uri_id = id_from_uri(track.track.uri); + const artist = track.track.artists.items[0]; + if (artist === undefined) { + throw new ScriptException("missing artist"); + } + const url = `${SONG_URL_PREFIX}${track_uri_id}`; + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: track.track.name, + author: new PlatformAuthorLink(new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), artist.profile.name, `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}`, id_from_uri(artist.uri) === album_artist.id ? album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url : undefined), + url, + thumbnails, + duration: track.track.duration.totalMilliseconds / 1000, + viewCount: parseInt(track.track.playcount), + isLive: false, + shareUrl: url, + datetime: unix_time + }); + }); +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_tracks_args(album_uri_id, offset, limit) { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + offset: offset, + limit: limit + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "queryAlbumTracks"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_metadata_args(album_uri_id, offset, limit) { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + locale: "", + offset: offset, + limit: limit + }); + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }); + const url = new URL(QUERY_URL); + url.searchParams.set("operationName", "getAlbum"); + url.searchParams.set("variables", variables); + url.searchParams.set("extensions", extensions); + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } }; +} //#endregion //#region utilities +function id_from_uri(uri) { + const match_result = uri.match(/^spotify:(show|album|track|artist):([0-9a-zA-Z]*)$/); + if (match_result === null) { + throw new ScriptException("regex error"); + } + const uri_id = match_result[2]; + if (uri_id === undefined) { + throw new ScriptException("regex error"); + } + return uri_id; +} /** * Converts seconds to the timestamp format used in WebVTT * @param seconds @@ -411,6 +928,7 @@ function assert_no_fall_through(value, exception_message) { return; } //#endregion +//#region bad function is_premium() { return false; } @@ -478,6 +996,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; } +//#endregion // export statements are removed during build step // used for unit testing in SpotifyScript.test.ts // export { get_gid, assert_never, log_passthrough }; diff --git a/build/SpotifyScript.js.map b/build/SpotifyScript.js.map index 3388593..c6ad143 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 3181680..cc3c00b 100644 --- a/build/SpotifyScript.ts +++ b/build/SpotifyScript.ts @@ -1,18 +1,34 @@ //#region constants import { + type AlbumResponse, + type AlbumTracksResponse, + type ArtistDetails, + type ArtistMetadataResponse, type ContentType, type EpisodeMetadataResponse, type FileManifestResponse, type GetLicenseResponse, + type LyricsResponse, + type PlaylistContent, + type PlaylistContentResponse, + type PlaylistResponse, + type PlaylistType, + type PodcastMetadataResponse, type Settings, type SongMetadataResponse, - // type SpotifySource, type State, + type TrackMetadataResponse, + type Tracks, type TranscriptResponse, } from "./types.js" const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/ +const PLAYLIST_REGEX = /^https:\/\/open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]*)($|\/)/ const SONG_URL_PREFIX = "https://open.spotify.com/track/" as const +const PODCAST_URL_PREFIX = "https://open.spotify.com/show/" as const +const ARTIST_URL_PREFIX = "https://open.spotify.com/artist/" as const +const ALBUM_URL_PREFIX = "https://open.spotify.com/album/" as const +const QUERY_URL = "https://api-partner.spotify.com/pathfinder/v1/query" as const const IMAGE_URL_PREFIX = "https://i.scdn.co/image/" as const const PLATFORM = "Spotify" as const @@ -20,7 +36,6 @@ 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 @@ -56,9 +71,9 @@ source.getHome = getHome source.isContentDetailsUrl = isContentDetailsUrl source.getContentDetails = getContentDetails -// source.isPlaylistUrl = isPlaylistUrl +source.isPlaylistUrl = isPlaylistUrl // source.searchPlaylists = searchPlaylists -// source.getPlaylist = getPlaylist +source.getPlaylist = getPlaylist // source.getComments = getComments // source.getSubComments = getSubComments @@ -178,6 +193,15 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul license_uri: license_uri } } + if (is_premium()) { + local_state = { + bearer_token: "BQB5uzdWBsXahudafNcc3RR7kExwq4vpsbkSCOuGkn06aYQ8it6x-M5PmaVi2gapw5NgXMO4tlDSenQcCqv2dQg94a_4fsi11yX5qkAeqW0f_bRNHZ3cg1QlJgX8kKnOmEs5I8jmhY2pR0k8ParLvLZt7tVQYVceei3NJM4w4oKr6thqYyCST-3BHJximVhvT5_cmMrFac5VBWkgioQPxNUSO1U6ICi0hN2W5WMYg8KjdrjCPKfFiYTE3Z9myO0fGI13o1uWzNRrXHc075HuOYvvv_5UbobXPyPVbSEfLqGuaPstmN8Ubj7XV6FXYPnvNSNnJlLx1GwLx2EoyA", + license_uri: local_state.license_uri + } + } + + + } //#endregion @@ -191,6 +215,46 @@ function saveState() { //#region home function getHome() { + const playlists = [ + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "the coolest album of all time", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "dayly mix mix 1111", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "tines for two", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/4ClcTeCoE9aPMhy0CLoD9P", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }) + ] + return new ContentPager(playlists, false) + /* const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" const song_url = `${SONG_URL_PREFIX}${song_uri_id}` @@ -216,6 +280,7 @@ function getHome() { // readonly uploadDate?: number })] return new VideoPager(songs, false) + */ } //#endregion @@ -248,12 +313,65 @@ function getContentDetails(url: string) { 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] + const { url: track_metadata_url, headers: _track_metadata_headers } = track_metadata_args(content_uri_id) + const batch = local_http + .batch() + .GET(metadata_url, metadata_headers, false) + .GET(track_metadata_url, _track_metadata_headers, false) + if (is_premium()) { + const { url, headers } = lyrics_args(content_uri_id) + batch.GET(url, headers, false) + } + const results = batch + .execute() + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable") + } + const song_metadata_response: SongMetadataResponse = JSON.parse(results[0].body) + const track_metadata_response: TrackMetadataResponse = JSON.parse(results[1].body) + const first_artist = track_metadata_response.data.trackUnion.firstArtist.items[0] if (first_artist === undefined) { throw new ScriptException("missing artist") } - const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m" + const artist_url = `https://open.spotify.com/artist/${first_artist.id}` + const highest_quality_artist_cover_art = first_artist.visuals.avatarImage.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current + }) + + let subtitles: ISubtitleSource[] = [] + + if (results[2] !== undefined) { + const lyrics_response: LyricsResponse = JSON.parse(results[2].body) + const subtitle_name = function () { + switch (lyrics_response.lyrics.language) { + case "en": + return "English" + default: + throw assert_no_fall_through(lyrics_response.lyrics.language, "unreachable") + } + }() + const convert = milliseconds_to_WebVTT_timestamp + let vtt_text = `WEBVTT ${subtitle_name}\n` + vtt_text += "\n" + lyrics_response.lyrics.lines.forEach(function (line, index) { + const next = lyrics_response.lyrics.lines[index + 1] + let end = next?.startTimeMs + if (end === undefined) { + end = track_metadata_response.data.trackUnion.duration.totalMilliseconds.toString() + } + vtt_text += `${convert(parseInt(line.startTimeMs))} --> ${convert(parseInt(end))}\n` + vtt_text += `${line.words}\n` + vtt_text += "\n" + }) + subtitles = [{ + url: song_url, + name: subtitle_name, + getSubtitles() { + return vtt_text + }, + format: "text/vtt", + }] + } const format = is_premium() ? "MP4_256" : "MP4_128" @@ -263,20 +381,40 @@ function getContentDetails(url: string) { } const { url, headers } = file_manifest_args(maybe_file_id) - const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(url, headers, false).body) + const { url: artist_metadata_url, headers: artist_metadata_headers } = artist_metadata_args(first_artist.id) + const second_results = local_http + .batch() + .GET(url, headers, false) + .GET(artist_metadata_url, artist_metadata_headers, false) + .execute() + if (second_results[0] === undefined || second_results[1] === undefined) { + throw new ScriptException("unreachable") + } + const file_manifest: FileManifestResponse = JSON.parse(second_results[0].body) + const artist_metadata_response: ArtistMetadataResponse = JSON.parse(second_results[1].body) - const duration = song_metadata_response.duration / 1000 + const duration = track_metadata_response.data.trackUnion.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: format, - bitrate: HARDCODED_ZERO, + name: codecs, + bitrate: function (format: "MP4_128" | "MP4_256") { + switch (format) { + case "MP4_128": + return 128000 + case "MP4_256": + return 256000 + default: + throw assert_no_fall_through(format, "unreachable") + } + }(format), container: "audio/mp4", - codecs: "mp4a.40.2", + codecs, duration, url: file_url, language: Language.UNKNOWN, @@ -287,20 +425,26 @@ function getContentDetails(url: string) { 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), + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, first_artist.id, plugin.config.id), + first_artist.profile.name, + artist_url, + highest_quality_artist_cover_art.url, + artist_metadata_response.data.artistUnion.stats.monthlyListeners + ), 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, + viewCount: parseInt(track_metadata_response.data.trackUnion.playcount), isLive: false, shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number + datetime: new Date(track_metadata_response.data.trackUnion.albumOfTrack.date.isoString).getTime() / 1000, description: HARDCODED_EMPTY_STRING, video: new UnMuxVideoSourceDescriptor([], audio_sources), - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] + rating: new RatingLikes(HARDCODED_ZERO), + subtitles }) } @@ -319,14 +463,60 @@ function getContentDetails(url: string) { const transcript_response: TranscriptResponse = JSON.parse(responses[0].body) const episode_metadata_response: EpisodeMetadataResponse = JSON.parse(responses[1].body) + if (episode_metadata_response.data.episodeUnionV2.mediaTypes.length === 2) { + function assert_video(_mediaTypes: ["AUDIO", "VIDEO"]) { } + assert_video(episode_metadata_response.data.episodeUnionV2.mediaTypes) + //TODO since we don't use the transcript we should only load it when audio only podcasts are played + + // TODO handle video podcasts. Grayjay doesn't currently support the websocket functionality necessary + // the basic process to get the video play info is + // connect to the websocket wss://gue1-dealer.spotify.com/?access_token=<bearer-token> + // register the device https://gue1-spclient.spotify.com/track-playback/v1/devices + // generate the device id using code found in the min js like this + /* + web player js + const t = Math.ceil(e / 2); + return function(e) { + let t = ""; + for (let n = 0; n < e.length; n++) { + const i = e[n]; + i < 16 && (t += "0"), + t += i.toString(16) + } + return t + }(Oe(t)) + */ + // load devices info https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_aced97d86694f14d304dd4e6f1f7f8c3bff + // transfer to our device https://gue1-spclient.spotify.com/connect-state/v1/connect/transfer/from/9a7079bd5b5605839c1d9080d0f4368bfcd6d2eb/to/aced97d86694f14d304dd4e6f1f7f8c3bff + // signal the play of the given podcast (not quite sure how this works :/) + // recieve the video play info via the websocket connection + // + } + 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 limited_podcast_metadata = episode_metadata_response.data.episodeUnionV2.podcastV2.data + const podcast_uri_id = id_from_uri(limited_podcast_metadata.uri) + const highest_quality_cover_art = limited_podcast_metadata.coverArt.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current + }) + 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 { url: podcast_metadata_url, headers: podcast_metadata_headers } = podcast_metadata_args(podcast_uri_id) + const results = local_http + .batch() + .GET(podcast_metadata_url, podcast_metadata_headers, false) + .GET(manifest_url, manifest_headers, false) + .execute() + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable") + } + const full_podcast_metadata: PodcastMetadataResponse = JSON.parse(results[0].body) + const file_manifest: FileManifestResponse = JSON.parse(results[1].body) const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000 @@ -376,7 +566,12 @@ function getContentDetails(url: string) { return new PlatformVideoDetails({ id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), name: episode_metadata_response.data.episodeUnionV2.name, - author: EMPTY_AUTHOR, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, podcast_uri_id, plugin.config.id), + limited_podcast_metadata.name, + `${PODCAST_URL_PREFIX}${podcast_uri_id}`, + highest_quality_cover_art.url, + ), url: episode_url, thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { return new Thumbnail(image.url, image.height) @@ -385,10 +580,10 @@ function getContentDetails(url: string) { viewCount: HARDCODED_ZERO, isLive: false, shareUrl: episode_metadata_response.data.episodeUnionV2.uri, - uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + datetime: 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), + rating: new RatingScaler(full_podcast_metadata.data.podcastUnionV2.rating.averageRating.average), subtitles: [{ url: episode_url, name: subtitle_name, @@ -405,6 +600,23 @@ function getContentDetails(url: string) { } } +function podcast_metadata_args(podcast_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:show:${podcast_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "5fb034a236a3e8301e9eca0e23def3341ed66c891ea2d4fea374c091dc4b4a6a" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryShowMetadataV2") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} + 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}`) @@ -415,6 +627,24 @@ function transcript_args(episode_uri_id: string): { readonly url: string, readon } } +function lyrics_args(song_uri_id: string): { + readonly url: string, readonly headers: { + Authorization: string, + Accept: string, + "app-platform": "WebPlayer" + } +} { + const url = new URL(`https://spclient.wg.spotify.com/color-lyrics/v2/track/${song_uri_id}`) + return { + url: url.toString(), + headers: { + Accept: "application/json", + "app-platform": "WebPlayer", + 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" @@ -425,7 +655,6 @@ function file_manifest_args(file_id: string): { readonly url: string, readonly h } 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}` }) @@ -435,13 +664,30 @@ function episode_metadata_args(episode_uri_id: string): { readonly url: string, sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" } }) - const url = new URL(episode_metadata_url_prefix) + const url = new URL(QUERY_URL) 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 track_metadata_args(song_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:track:${song_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "ae85b52abb74d20a4c331d4143d4772c95f34757bfa8c625474b912b9055b5c0" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "getTrack") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} + function song_metadata_args(song_uri_id: string): { readonly url: string, readonly headers: { @@ -458,9 +704,354 @@ function song_metadata_args(song_uri_id: string): { } } } + +function artist_metadata_args(artist_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:artist:${artist_uri_id}`, + locale: "", + includePrerelease: true + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "da986392124383827dc03cbb3d66c1de81225244b6e20f8d78f9f802cc43df6e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryArtistOverview") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +//#endregion + +//#region playlists +// https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ +// https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT +function isPlaylistUrl(url: string): boolean { + return PLAYLIST_REGEX.test(url) +} +function getPlaylist(url: string): PlatformPlaylistDetails { + const match_result = url.match(PLAYLIST_REGEX) + if (match_result === null) { + throw new ScriptException("regex error") + } + const maybe_playlist_type = match_result[1] + if (maybe_playlist_type === undefined) { + throw new ScriptException("regex error") + } + const playlist_type: PlaylistType = maybe_playlist_type as PlaylistType + const playlist_uri_id = match_result[2] + if (playlist_uri_id === undefined) { + throw new ScriptException("regex error") + } + switch (playlist_type) { + case "album": { + // if the author is the same as the album then include the artist pick otherwise nothing + // TODO we could load in extra info for all the other artists but it might be hard to do that in a request efficient way + + const pagination_limit = 50 as const + const offset = 0 + + const { url, headers } = album_metadata_args(playlist_uri_id, offset, pagination_limit) + const album_metadata_response: AlbumResponse = JSON.parse(local_http.GET(url, headers, false).body) + const album_artist = album_metadata_response.data.albumUnion.artists.items[0] + if (album_artist === undefined) { + throw new ScriptException("missing album artist") + } + const unix_time = new Date(album_metadata_response.data.albumUnion.date.isoString).getTime() / 1000 + + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: album_metadata_response.data.albumUnion.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, album_artist.id, plugin.config.id), + album_artist.profile.name, + `${ARTIST_URL_PREFIX}${album_artist.id}`, + album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url + ), + datetime: unix_time, + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: album_metadata_response.data.albumUnion.tracks.totalCount, + contents: new AlbumPager(playlist_uri_id, offset, pagination_limit, album_metadata_response, album_artist, unix_time) + }) + } + case "playlist": { + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to open playlists") + } + const pagination_limit = 25 as const + const offset = 0 + + const { url, headers } = fetch_playlist_args(playlist_uri_id, offset, pagination_limit) + const playlist_response: PlaylistResponse = JSON.parse(local_http.GET(url, headers, false).body) + const owner = playlist_response.data.playlistV2.ownerV2.data + + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: playlist_response.data.playlistV2.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, owner.username, plugin.config.id), + owner.name, + `${ARTIST_URL_PREFIX}${owner.username}`, + owner.avatar?.sources[owner.avatar.sources.length - 1]?.url + ), + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: playlist_response.data.playlistV2.content.totalCount, + contents: new SpotifyPlaylistPager(playlist_uri_id, offset, pagination_limit, playlist_response) + }) + } + default: { + throw assert_no_fall_through(playlist_type, "unreachable") + } + } +} +class SpotifyPlaylistPager extends VideoPager { + private offset: number + private readonly total_tracks: number + constructor( + private readonly playlist_uri_id: string, + offset: number, + private readonly pagination_limit: number, + playlist_response: PlaylistResponse + ) { + const total_tracks = playlist_response.data.playlistV2.content.totalCount + + const songs = format_playlist_tracks(playlist_response.data.playlistV2.content) + + super(songs, total_tracks > offset + pagination_limit) + this.offset = offset + pagination_limit + this.total_tracks = total_tracks + } + override nextPage(this: SpotifyPlaylistPager): SpotifyPlaylistPager { + const { url, headers } = fetch_playlist_contents_args(this.playlist_uri_id, this.offset, this.pagination_limit) + const playlist_content_response: PlaylistContentResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const songs = format_playlist_tracks(playlist_content_response.data.playlistV2.content) + this.results = songs + this.hasMore = this.total_tracks > this.offset + this.pagination_limit + this.offset += this.pagination_limit + return this + } + override hasMorePagers(this: SpotifyPlaylistPager): boolean { + return this.hasMore + } +} +function format_playlist_tracks(content: PlaylistContent) { + return content.items.map(function (playlist_track_metadata) { + const song = playlist_track_metadata.itemV2.data + const track_uri_id = id_from_uri(song.uri) + const artist = song.artists.items[0] + if (artist === undefined) { + throw new ScriptException("missing artist") + } + const url = `${SONG_URL_PREFIX}${track_uri_id}` + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: song.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), + artist.profile.name, + `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}` + // TODO figure out a way to get the artist thumbnail + ), + url, + thumbnails: new Thumbnails(song.albumOfTrack.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height) + })), + duration: song.trackDuration.totalMilliseconds / 1000, + viewCount: parseInt(song.playcount), + isLive: false, + shareUrl: url, + datetime: new Date(playlist_track_metadata.addedAt.isoString).getTime() / 1000 + }) + }) +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_contents_args(playlist_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "fetchPlaylistContents") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_args(playlist_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "fetchPlaylist") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +class AlbumPager extends VideoPager { + private offset: number + private readonly thumbnails: Thumbnails + private readonly album_artist: ArtistDetails + private readonly unix_time: number + private readonly total_tracks: number + constructor( + private readonly album_uri_id: string, + offset: number, + private readonly pagination_limit: number, + album_metadata_response: AlbumResponse, + album_artist: ArtistDetails, + unix_time: number, + ) { + const total_tracks = album_metadata_response.data.albumUnion.tracks.totalCount + const thumbnails = new Thumbnails(album_metadata_response.data.albumUnion.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height) + })) + + const songs = format_album_tracks(album_metadata_response.data.albumUnion.tracks, thumbnails, album_artist, unix_time) + + super(songs, total_tracks > offset + pagination_limit) + this.offset = offset + pagination_limit + this.thumbnails = thumbnails + this.album_artist = album_artist + this.unix_time = unix_time + this.total_tracks = total_tracks + } + override nextPage(this: AlbumPager): AlbumPager { + const { url, headers } = album_tracks_args(this.album_uri_id, this.offset, this.pagination_limit) + const album_tracks_response: AlbumTracksResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const songs = format_album_tracks(album_tracks_response.data.albumUnion.tracks, this.thumbnails, this.album_artist, this.unix_time) + this.results = songs + this.hasMore = this.total_tracks > this.offset + this.pagination_limit + this.offset += this.pagination_limit + return this + } + override hasMorePagers(this: AlbumPager): boolean { + return this.hasMore + } +} +function format_album_tracks(tracks: Tracks, thumbnails: Thumbnails, album_artist: ArtistDetails, unix_time: number) { + return tracks.items.map(function (track) { + const track_uri_id = id_from_uri(track.track.uri) + const artist = track.track.artists.items[0] + if (artist === undefined) { + throw new ScriptException("missing artist") + } + const url = `${SONG_URL_PREFIX}${track_uri_id}` + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: track.track.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), + artist.profile.name, + `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}`, + id_from_uri(artist.uri) === album_artist.id ? album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url : undefined + ), + url, + thumbnails, + duration: track.track.duration.totalMilliseconds / 1000, + viewCount: parseInt(track.track.playcount), + isLive: false, + shareUrl: url, + datetime: unix_time + }) + }) +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_tracks_args(album_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryAlbumTracks") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_metadata_args(album_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + locale: "", + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "getAlbum") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} //#endregion //#region utilities +function id_from_uri(uri: string): string { + const match_result = uri.match(/^spotify:(show|album|track|artist):([0-9a-zA-Z]*)$/) + if (match_result === null) { + throw new ScriptException("regex error") + } + const uri_id = match_result[2] + if (uri_id === undefined) { + throw new ScriptException("regex error") + } + return uri_id +} + /** * Converts seconds to the timestamp format used in WebVTT * @param seconds @@ -489,6 +1080,7 @@ function assert_no_fall_through(value: never, exception_message?: string): Scrip } //#endregion +//#region bad function is_premium(): boolean { return false } @@ -561,6 +1153,7 @@ function get_gid(song_uri_id: string) { 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 } +//#endregion // export statements are removed during build step // used for unit testing in SpotifyScript.test.ts diff --git a/package-lock.json b/package-lock.json index 84fd98a..7971c7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "devDependencies": { - "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", + "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#bb6dbb7f72e6e992180f63ad6442a61c9fee2cd9", "@types/node": "^20.12.7", "http-server": "^14.1.1", "npm-check-updates": "^16.14.20" @@ -39,8 +39,8 @@ }, "node_modules/@grayjay/plugin": { "version": "1.0.0", - "resolved": "git+ssh://git@gitlab.com/kaidelorenzo/grayjay-plugin-types.git#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", - "integrity": "sha512-wvYWXupDyr4kKlTCJbuAuEvi48gRx/RpX6j31m4KeRukCBdV/QuHeS3L+GRVRXIU/ltBraEiNLo2CiMIxY2qiw==", + "resolved": "git+ssh://git@gitlab.com/kaidelorenzo/grayjay-plugin-types.git#bb6dbb7f72e6e992180f63ad6442a61c9fee2cd9", + "integrity": "sha512-mEzQEm6QhmNSv4Pr0FRl9m2kTygr9MrTNjGYFJyeSlCN3HFbv2+9MSXwrtFhHAaIbxphbOs6pJtIA2VIFg3lJg==", "dev": true, "dependencies": { "@types/sync-fetch": "^0.4.3", @@ -499,9 +499,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.12.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", + "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/package.json b/package.json index f1e2f1a..7003e96 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "type": "module", "devDependencies": { - "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#c9fcd5e27dd6f04b19e5ac0e03ee46a71f699dfc", + "@grayjay/plugin": "gitlab:kaidelorenzo/grayjay-plugin-types#bb6dbb7f72e6e992180f63ad6442a61c9fee2cd9", "@types/node": "^20.12.7", "http-server": "^14.1.1", "npm-check-updates": "^16.14.20" diff --git a/src/SpotifyScript.ts b/src/SpotifyScript.ts index 6fb3db0..c70798e 100644 --- a/src/SpotifyScript.ts +++ b/src/SpotifyScript.ts @@ -1,18 +1,34 @@ //#region constants import { + type AlbumResponse, + type AlbumTracksResponse, + type ArtistDetails, + type ArtistMetadataResponse, type ContentType, type EpisodeMetadataResponse, type FileManifestResponse, type GetLicenseResponse, + type LyricsResponse, + type PlaylistContent, + type PlaylistContentResponse, + type PlaylistResponse, + type PlaylistType, + type PodcastMetadataResponse, type Settings, type SongMetadataResponse, - // type SpotifySource, type State, + type TrackMetadataResponse, + type Tracks, type TranscriptResponse, } from "./types.js" const CONTENT_REGEX = /^https:\/\/open\.spotify\.com\/(track|episode)\/([a-zA-Z0-9]*)($|\/)/ +const PLAYLIST_REGEX = /^https:\/\/open\.spotify\.com\/(album|playlist)\/([a-zA-Z0-9]*)($|\/)/ const SONG_URL_PREFIX = "https://open.spotify.com/track/" as const +const PODCAST_URL_PREFIX = "https://open.spotify.com/show/" as const +const ARTIST_URL_PREFIX = "https://open.spotify.com/artist/" as const +const ALBUM_URL_PREFIX = "https://open.spotify.com/album/" as const +const QUERY_URL = "https://api-partner.spotify.com/pathfinder/v1/query" as const const IMAGE_URL_PREFIX = "https://i.scdn.co/image/" as const const PLATFORM = "Spotify" as const @@ -20,7 +36,6 @@ 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 @@ -56,9 +71,9 @@ source.getHome = getHome source.isContentDetailsUrl = isContentDetailsUrl source.getContentDetails = getContentDetails -// source.isPlaylistUrl = isPlaylistUrl +source.isPlaylistUrl = isPlaylistUrl // source.searchPlaylists = searchPlaylists -// source.getPlaylist = getPlaylist +source.getPlaylist = getPlaylist // source.getComments = getComments // source.getSubComments = getSubComments @@ -178,6 +193,15 @@ function enable(conf: SourceConfig, settings: Settings, savedState: string | nul license_uri: license_uri } } + if (is_premium()) { + local_state = { + bearer_token: "BQB5uzdWBsXahudafNcc3RR7kExwq4vpsbkSCOuGkn06aYQ8it6x-M5PmaVi2gapw5NgXMO4tlDSenQcCqv2dQg94a_4fsi11yX5qkAeqW0f_bRNHZ3cg1QlJgX8kKnOmEs5I8jmhY2pR0k8ParLvLZt7tVQYVceei3NJM4w4oKr6thqYyCST-3BHJximVhvT5_cmMrFac5VBWkgioQPxNUSO1U6ICi0hN2W5WMYg8KjdrjCPKfFiYTE3Z9myO0fGI13o1uWzNRrXHc075HuOYvvv_5UbobXPyPVbSEfLqGuaPstmN8Ubj7XV6FXYPnvNSNnJlLx1GwLx2EoyA", + license_uri: local_state.license_uri + } + } + + + } //#endregion @@ -191,6 +215,46 @@ function saveState() { //#region home function getHome() { + const playlists = [ + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "the coolest album of all time", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "dayly mix mix 1111", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }), + new PlatformPlaylist({ + id: new PlatformID(PLATFORM, "an album id", plugin.config.id), + name: "tines for two", + // thumbnails: new Thumbnails([new Thumbnail("https://i.scdn.co/image/ab6765630000ba8a95af9fcb5c610d710793568a", 11)]), + author: new PlatformAuthorLink(new PlatformID(PLATFORM, "an artist id", plugin.config.id), "beyonce", "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m"), + // datetime: 1714580179, + datetime: 1714580179, + url: "https://open.spotify.com/playlist/4ClcTeCoE9aPMhy0CLoD9P", + videoCount: 11, + /** Only usable for IPlatformPlaylistDef not IPlatformPlaylistDetailsDef */ + thumbnail: "https://i.scdn.co/image/ab6765630000ba8ab3d3d2577970462809eb1145" + }) + ] + return new ContentPager(playlists, false) + /* const song_uri_id = "6XXxKsu3RJeN3ZvbMYrgQW" const song_url = `${SONG_URL_PREFIX}${song_uri_id}` @@ -216,6 +280,7 @@ function getHome() { // readonly uploadDate?: number })] return new VideoPager(songs, false) + */ } //#endregion @@ -248,12 +313,65 @@ function getContentDetails(url: string) { 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] + const { url: track_metadata_url, headers: _track_metadata_headers } = track_metadata_args(content_uri_id) + const batch = local_http + .batch() + .GET(metadata_url, metadata_headers, false) + .GET(track_metadata_url, _track_metadata_headers, false) + if (is_premium()) { + const { url, headers } = lyrics_args(content_uri_id) + batch.GET(url, headers, false) + } + const results = batch + .execute() + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable") + } + const song_metadata_response: SongMetadataResponse = JSON.parse(results[0].body) + const track_metadata_response: TrackMetadataResponse = JSON.parse(results[1].body) + const first_artist = track_metadata_response.data.trackUnion.firstArtist.items[0] if (first_artist === undefined) { throw new ScriptException("missing artist") } - const artist_url = "https://open.spotify.com/artist/6vWDO969PvNqNYHIOW5v0m" + const artist_url = `https://open.spotify.com/artist/${first_artist.id}` + const highest_quality_artist_cover_art = first_artist.visuals.avatarImage.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current + }) + + let subtitles: ISubtitleSource[] = [] + + if (results[2] !== undefined) { + const lyrics_response: LyricsResponse = JSON.parse(results[2].body) + const subtitle_name = function () { + switch (lyrics_response.lyrics.language) { + case "en": + return "English" + default: + throw assert_no_fall_through(lyrics_response.lyrics.language, "unreachable") + } + }() + const convert = milliseconds_to_WebVTT_timestamp + let vtt_text = `WEBVTT ${subtitle_name}\n` + vtt_text += "\n" + lyrics_response.lyrics.lines.forEach(function (line, index) { + const next = lyrics_response.lyrics.lines[index + 1] + let end = next?.startTimeMs + if (end === undefined) { + end = track_metadata_response.data.trackUnion.duration.totalMilliseconds.toString() + } + vtt_text += `${convert(parseInt(line.startTimeMs))} --> ${convert(parseInt(end))}\n` + vtt_text += `${line.words}\n` + vtt_text += "\n" + }) + subtitles = [{ + url: song_url, + name: subtitle_name, + getSubtitles() { + return vtt_text + }, + format: "text/vtt", + }] + } const format = is_premium() ? "MP4_256" : "MP4_128" @@ -263,20 +381,40 @@ function getContentDetails(url: string) { } const { url, headers } = file_manifest_args(maybe_file_id) - const file_manifest: FileManifestResponse = JSON.parse(local_http.GET(url, headers, false).body) + const { url: artist_metadata_url, headers: artist_metadata_headers } = artist_metadata_args(first_artist.id) + const second_results = local_http + .batch() + .GET(url, headers, false) + .GET(artist_metadata_url, artist_metadata_headers, false) + .execute() + if (second_results[0] === undefined || second_results[1] === undefined) { + throw new ScriptException("unreachable") + } + const file_manifest: FileManifestResponse = JSON.parse(second_results[0].body) + const artist_metadata_response: ArtistMetadataResponse = JSON.parse(second_results[1].body) - const duration = song_metadata_response.duration / 1000 + const duration = track_metadata_response.data.trackUnion.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: format, - bitrate: HARDCODED_ZERO, + name: codecs, + bitrate: function (format: "MP4_128" | "MP4_256") { + switch (format) { + case "MP4_128": + return 128000 + case "MP4_256": + return 256000 + default: + throw assert_no_fall_through(format, "unreachable") + } + }(format), container: "audio/mp4", - codecs: "mp4a.40.2", + codecs, duration, url: file_url, language: Language.UNKNOWN, @@ -287,20 +425,26 @@ function getContentDetails(url: string) { 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), + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, first_artist.id, plugin.config.id), + first_artist.profile.name, + artist_url, + highest_quality_artist_cover_art.url, + artist_metadata_response.data.artistUnion.stats.monthlyListeners + ), 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, + viewCount: parseInt(track_metadata_response.data.trackUnion.playcount), isLive: false, shareUrl: song_metadata_response.canonical_uri, - // readonly uploadDate?: number + datetime: new Date(track_metadata_response.data.trackUnion.albumOfTrack.date.isoString).getTime() / 1000, description: HARDCODED_EMPTY_STRING, video: new UnMuxVideoSourceDescriptor([], audio_sources), - rating: new RatingLikes(HARDCODED_ZERO) - // readonly subtitles?: ISubtitleSource[] + rating: new RatingLikes(HARDCODED_ZERO), + subtitles }) } @@ -319,14 +463,60 @@ function getContentDetails(url: string) { const transcript_response: TranscriptResponse = JSON.parse(responses[0].body) const episode_metadata_response: EpisodeMetadataResponse = JSON.parse(responses[1].body) + if (episode_metadata_response.data.episodeUnionV2.mediaTypes.length === 2) { + function assert_video(_mediaTypes: ["AUDIO", "VIDEO"]) { } + assert_video(episode_metadata_response.data.episodeUnionV2.mediaTypes) + //TODO since we don't use the transcript we should only load it when audio only podcasts are played + + // TODO handle video podcasts. Grayjay doesn't currently support the websocket functionality necessary + // the basic process to get the video play info is + // connect to the websocket wss://gue1-dealer.spotify.com/?access_token=<bearer-token> + // register the device https://gue1-spclient.spotify.com/track-playback/v1/devices + // generate the device id using code found in the min js like this + /* + web player js + const t = Math.ceil(e / 2); + return function(e) { + let t = ""; + for (let n = 0; n < e.length; n++) { + const i = e[n]; + i < 16 && (t += "0"), + t += i.toString(16) + } + return t + }(Oe(t)) + */ + // load devices info https://gue1-spclient.spotify.com/connect-state/v1/devices/hobs_aced97d86694f14d304dd4e6f1f7f8c3bff + // transfer to our device https://gue1-spclient.spotify.com/connect-state/v1/connect/transfer/from/9a7079bd5b5605839c1d9080d0f4368bfcd6d2eb/to/aced97d86694f14d304dd4e6f1f7f8c3bff + // signal the play of the given podcast (not quite sure how this works :/) + // recieve the video play info via the websocket connection + // + } + 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 limited_podcast_metadata = episode_metadata_response.data.episodeUnionV2.podcastV2.data + const podcast_uri_id = id_from_uri(limited_podcast_metadata.uri) + const highest_quality_cover_art = limited_podcast_metadata.coverArt.sources.reduce(function (accumulator, current) { + return accumulator.height > current.height ? accumulator : current + }) + 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 { url: podcast_metadata_url, headers: podcast_metadata_headers } = podcast_metadata_args(podcast_uri_id) + const results = local_http + .batch() + .GET(podcast_metadata_url, podcast_metadata_headers, false) + .GET(manifest_url, manifest_headers, false) + .execute() + if (results[0] === undefined || results[1] === undefined) { + throw new ScriptException("unreachable") + } + const full_podcast_metadata: PodcastMetadataResponse = JSON.parse(results[0].body) + const file_manifest: FileManifestResponse = JSON.parse(results[1].body) const duration = episode_metadata_response.data.episodeUnionV2.duration.totalMilliseconds / 1000 @@ -376,7 +566,12 @@ function getContentDetails(url: string) { return new PlatformVideoDetails({ id: new PlatformID(PLATFORM, content_uri_id, plugin.config.id), name: episode_metadata_response.data.episodeUnionV2.name, - author: EMPTY_AUTHOR, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, podcast_uri_id, plugin.config.id), + limited_podcast_metadata.name, + `${PODCAST_URL_PREFIX}${podcast_uri_id}`, + highest_quality_cover_art.url, + ), url: episode_url, thumbnails: new Thumbnails(episode_metadata_response.data.episodeUnionV2.coverArt.sources.map(function (image) { return new Thumbnail(image.url, image.height) @@ -385,10 +580,10 @@ function getContentDetails(url: string) { viewCount: HARDCODED_ZERO, isLive: false, shareUrl: episode_metadata_response.data.episodeUnionV2.uri, - uploadDate: new Date(episode_metadata_response.data.episodeUnionV2.releaseDate.isoString).getTime() / 1000, + datetime: 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), + rating: new RatingScaler(full_podcast_metadata.data.podcastUnionV2.rating.averageRating.average), subtitles: [{ url: episode_url, name: subtitle_name, @@ -405,6 +600,23 @@ function getContentDetails(url: string) { } } +function podcast_metadata_args(podcast_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:show:${podcast_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "5fb034a236a3e8301e9eca0e23def3341ed66c891ea2d4fea374c091dc4b4a6a" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryShowMetadataV2") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} + 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}`) @@ -415,6 +627,24 @@ function transcript_args(episode_uri_id: string): { readonly url: string, readon } } +function lyrics_args(song_uri_id: string): { + readonly url: string, readonly headers: { + Authorization: string, + Accept: string, + "app-platform": "WebPlayer" + } +} { + const url = new URL(`https://spclient.wg.spotify.com/color-lyrics/v2/track/${song_uri_id}`) + return { + url: url.toString(), + headers: { + Accept: "application/json", + "app-platform": "WebPlayer", + 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" @@ -425,7 +655,6 @@ function file_manifest_args(file_id: string): { readonly url: string, readonly h } 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}` }) @@ -435,13 +664,30 @@ function episode_metadata_args(episode_uri_id: string): { readonly url: string, sha256Hash: "9697538fe993af785c10725a40bb9265a20b998ccd2383bd6f586e01303824e9" } }) - const url = new URL(episode_metadata_url_prefix) + const url = new URL(QUERY_URL) 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 track_metadata_args(song_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:track:${song_uri_id}` + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "ae85b52abb74d20a4c331d4143d4772c95f34757bfa8c625474b912b9055b5c0" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "getTrack") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} + function song_metadata_args(song_uri_id: string): { readonly url: string, readonly headers: { @@ -458,9 +704,354 @@ function song_metadata_args(song_uri_id: string): { } } } + +function artist_metadata_args(artist_uri_id: string): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:artist:${artist_uri_id}`, + locale: "", + includePrerelease: true + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "da986392124383827dc03cbb3d66c1de81225244b6e20f8d78f9f802cc43df6e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryArtistOverview") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +//#endregion + +//#region playlists +// https://open.spotify.com/album/6BzxX6zkDsYKFJ04ziU5xQ +// https://open.spotify.com/playlist/37i9dQZF1E38112qhvV3BT +function isPlaylistUrl(url: string): boolean { + return PLAYLIST_REGEX.test(url) +} +function getPlaylist(url: string): PlatformPlaylistDetails { + const match_result = url.match(PLAYLIST_REGEX) + if (match_result === null) { + throw new ScriptException("regex error") + } + const maybe_playlist_type = match_result[1] + if (maybe_playlist_type === undefined) { + throw new ScriptException("regex error") + } + const playlist_type: PlaylistType = maybe_playlist_type as PlaylistType + const playlist_uri_id = match_result[2] + if (playlist_uri_id === undefined) { + throw new ScriptException("regex error") + } + switch (playlist_type) { + case "album": { + // if the author is the same as the album then include the artist pick otherwise nothing + // TODO we could load in extra info for all the other artists but it might be hard to do that in a request efficient way + + const pagination_limit = 50 as const + const offset = 0 + + const { url, headers } = album_metadata_args(playlist_uri_id, offset, pagination_limit) + const album_metadata_response: AlbumResponse = JSON.parse(local_http.GET(url, headers, false).body) + const album_artist = album_metadata_response.data.albumUnion.artists.items[0] + if (album_artist === undefined) { + throw new ScriptException("missing album artist") + } + const unix_time = new Date(album_metadata_response.data.albumUnion.date.isoString).getTime() / 1000 + + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: album_metadata_response.data.albumUnion.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, album_artist.id, plugin.config.id), + album_artist.profile.name, + `${ARTIST_URL_PREFIX}${album_artist.id}`, + album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url + ), + datetime: unix_time, + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: album_metadata_response.data.albumUnion.tracks.totalCount, + contents: new AlbumPager(playlist_uri_id, offset, pagination_limit, album_metadata_response, album_artist, unix_time) + }) + } + case "playlist": { + if (!bridge.isLoggedIn()) { + throw new LoginRequiredException("login to open playlists") + } + const pagination_limit = 25 as const + const offset = 0 + + const { url, headers } = fetch_playlist_args(playlist_uri_id, offset, pagination_limit) + const playlist_response: PlaylistResponse = JSON.parse(local_http.GET(url, headers, false).body) + const owner = playlist_response.data.playlistV2.ownerV2.data + + return new PlatformPlaylistDetails({ + id: new PlatformID(PLATFORM, playlist_uri_id, plugin.config.id), + name: playlist_response.data.playlistV2.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, owner.username, plugin.config.id), + owner.name, + `${ARTIST_URL_PREFIX}${owner.username}`, + owner.avatar?.sources[owner.avatar.sources.length - 1]?.url + ), + url: `${ALBUM_URL_PREFIX}${playlist_uri_id}`, + videoCount: playlist_response.data.playlistV2.content.totalCount, + contents: new SpotifyPlaylistPager(playlist_uri_id, offset, pagination_limit, playlist_response) + }) + } + default: { + throw assert_no_fall_through(playlist_type, "unreachable") + } + } +} +class SpotifyPlaylistPager extends VideoPager { + private offset: number + private readonly total_tracks: number + constructor( + private readonly playlist_uri_id: string, + offset: number, + private readonly pagination_limit: number, + playlist_response: PlaylistResponse + ) { + const total_tracks = playlist_response.data.playlistV2.content.totalCount + + const songs = format_playlist_tracks(playlist_response.data.playlistV2.content) + + super(songs, total_tracks > offset + pagination_limit) + this.offset = offset + pagination_limit + this.total_tracks = total_tracks + } + override nextPage(this: SpotifyPlaylistPager): SpotifyPlaylistPager { + const { url, headers } = fetch_playlist_contents_args(this.playlist_uri_id, this.offset, this.pagination_limit) + const playlist_content_response: PlaylistContentResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const songs = format_playlist_tracks(playlist_content_response.data.playlistV2.content) + this.results = songs + this.hasMore = this.total_tracks > this.offset + this.pagination_limit + this.offset += this.pagination_limit + return this + } + override hasMorePagers(this: SpotifyPlaylistPager): boolean { + return this.hasMore + } +} +function format_playlist_tracks(content: PlaylistContent) { + return content.items.map(function (playlist_track_metadata) { + const song = playlist_track_metadata.itemV2.data + const track_uri_id = id_from_uri(song.uri) + const artist = song.artists.items[0] + if (artist === undefined) { + throw new ScriptException("missing artist") + } + const url = `${SONG_URL_PREFIX}${track_uri_id}` + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: song.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), + artist.profile.name, + `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}` + // TODO figure out a way to get the artist thumbnail + ), + url, + thumbnails: new Thumbnails(song.albumOfTrack.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height) + })), + duration: song.trackDuration.totalMilliseconds / 1000, + viewCount: parseInt(song.playcount), + isLive: false, + shareUrl: url, + datetime: new Date(playlist_track_metadata.addedAt.isoString).getTime() / 1000 + }) + }) +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_contents_args(playlist_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "fetchPlaylistContents") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +/** + * + * @param playlist_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function fetch_playlist_args(playlist_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:playlist:${playlist_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "91d4c2bc3e0cd1bc672281c4f1f59f43ff55ba726ca04a45810d99bd091f3f0e" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "fetchPlaylist") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +class AlbumPager extends VideoPager { + private offset: number + private readonly thumbnails: Thumbnails + private readonly album_artist: ArtistDetails + private readonly unix_time: number + private readonly total_tracks: number + constructor( + private readonly album_uri_id: string, + offset: number, + private readonly pagination_limit: number, + album_metadata_response: AlbumResponse, + album_artist: ArtistDetails, + unix_time: number, + ) { + const total_tracks = album_metadata_response.data.albumUnion.tracks.totalCount + const thumbnails = new Thumbnails(album_metadata_response.data.albumUnion.coverArt.sources.map(function (source) { + return new Thumbnail(source.url, source.height) + })) + + const songs = format_album_tracks(album_metadata_response.data.albumUnion.tracks, thumbnails, album_artist, unix_time) + + super(songs, total_tracks > offset + pagination_limit) + this.offset = offset + pagination_limit + this.thumbnails = thumbnails + this.album_artist = album_artist + this.unix_time = unix_time + this.total_tracks = total_tracks + } + override nextPage(this: AlbumPager): AlbumPager { + const { url, headers } = album_tracks_args(this.album_uri_id, this.offset, this.pagination_limit) + const album_tracks_response: AlbumTracksResponse = JSON.parse(local_http.GET(url, headers, false).body) + + const songs = format_album_tracks(album_tracks_response.data.albumUnion.tracks, this.thumbnails, this.album_artist, this.unix_time) + this.results = songs + this.hasMore = this.total_tracks > this.offset + this.pagination_limit + this.offset += this.pagination_limit + return this + } + override hasMorePagers(this: AlbumPager): boolean { + return this.hasMore + } +} +function format_album_tracks(tracks: Tracks, thumbnails: Thumbnails, album_artist: ArtistDetails, unix_time: number) { + return tracks.items.map(function (track) { + const track_uri_id = id_from_uri(track.track.uri) + const artist = track.track.artists.items[0] + if (artist === undefined) { + throw new ScriptException("missing artist") + } + const url = `${SONG_URL_PREFIX}${track_uri_id}` + return new PlatformVideo({ + id: new PlatformID(PLATFORM, track_uri_id, plugin.config.id), + name: track.track.name, + author: new PlatformAuthorLink( + new PlatformID(PLATFORM, id_from_uri(artist.uri), plugin.config.id), + artist.profile.name, + `${ARTIST_URL_PREFIX}${id_from_uri(artist.uri)}`, + id_from_uri(artist.uri) === album_artist.id ? album_artist.visuals.avatarImage.sources[album_artist.visuals.avatarImage.sources.length - 1]?.url : undefined + ), + url, + thumbnails, + duration: track.track.duration.totalMilliseconds / 1000, + viewCount: parseInt(track.track.playcount), + isLive: false, + shareUrl: url, + datetime: unix_time + }) + }) +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_tracks_args(album_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "queryAlbumTracks") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} +/** + * + * @param album_uri_id + * @param offset the track to start loading from in the album (0 is the first track) + * @param limit the maximum number of tracks to load information about + * @returns + */ +function album_metadata_args(album_uri_id: string, offset: number, limit: number): { readonly url: string, readonly headers: { Authorization: string } } { + const variables = JSON.stringify({ + uri: `spotify:album:${album_uri_id}`, + locale: "", + offset: offset, + limit: limit + }) + const extensions = JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: "469874edcad37b7a379d4f22f0083a49ea3d6ae097916120d9bbe3e36ca79e9d" + } + }) + const url = new URL(QUERY_URL) + url.searchParams.set("operationName", "getAlbum") + url.searchParams.set("variables", variables) + url.searchParams.set("extensions", extensions) + return { url: url.toString(), headers: { Authorization: `Bearer ${local_state.bearer_token}` } } +} //#endregion //#region utilities +function id_from_uri(uri: string): string { + const match_result = uri.match(/^spotify:(show|album|track|artist):([0-9a-zA-Z]*)$/) + if (match_result === null) { + throw new ScriptException("regex error") + } + const uri_id = match_result[2] + if (uri_id === undefined) { + throw new ScriptException("regex error") + } + return uri_id +} + /** * Converts seconds to the timestamp format used in WebVTT * @param seconds @@ -489,6 +1080,7 @@ function assert_no_fall_through(value: never, exception_message?: string): Scrip } //#endregion +//#region bad function is_premium(): boolean { return false } @@ -561,6 +1153,7 @@ function get_gid(song_uri_id: string) { 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 } +//#endregion // export statements are removed during build step // used for unit testing in SpotifyScript.test.ts diff --git a/src/types.ts b/src/types.ts index 4213704..8c18b32 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,7 @@ export type State = { //#region JSON types export type ContentType = "track" | "episode" +export type PlaylistType = "album" | "playlist" export type TranscriptResponse = { readonly section: ({ @@ -41,10 +42,7 @@ export type EpisodeMetadataResponse = { readonly totalMilliseconds: number } readonly coverArt: { - readonly sources: { - readonly url: string - readonly height: number - }[] + readonly sources: ImageSources } readonly releaseDate: { readonly isoString: string @@ -58,10 +56,70 @@ export type EpisodeMetadataResponse = { }[] } readonly htmlDescription: string + readonly podcastV2: { + readonly data: { + readonly name: string + /** in this format "spotify:show:5VzFvh1JlEhBMS6ZHZ8CNO" */ + readonly uri: string + readonly coverArt: { + readonly sources: ImageSources + } + } + } + readonly mediaTypes: ["AUDIO"] | ["AUDIO", "VIDEO"] + } + } +} + +export type ArtistMetadataResponse = { + readonly data: { + readonly artistUnion: { + readonly stats: { + readonly followers: number + readonly monthlyListeners: number + readonly worldRank: number + } } } } +export type TrackMetadataResponse = { + readonly data: { + readonly trackUnion: { + readonly playcount: string + readonly firstArtist: { + readonly items: { + readonly id: string + readonly profile: { readonly name: string } + readonly visuals: { + readonly avatarImage: { + readonly sources: ImageSources + } + } + }[] + } + readonly albumOfTrack: { + readonly date: { + readonly isoString: string + } + } + readonly duration: { + readonly totalMilliseconds: number + } + } + } +} + +export type LyricsResponse = { + readonly lyrics: { + readonly language: "en" + readonly lines: { + readonly startTimeMs: string + readonly words: string + }[] + } +} + export type SongMetadataResponse = { readonly name: string /** in milliseconds */ @@ -73,16 +131,16 @@ export type SongMetadataResponse = { readonly height: number }[] } + readonly date: { + readonly day: number + readonly month: number + readonly year: number + } } readonly artist: { gid: string name: string }[] - readonly date: { - readonly day: number - readonly month: number - readonly year: number - } readonly canonical_uri: string readonly file: { readonly file_id: string @@ -91,6 +149,147 @@ export type SongMetadataResponse = { }[] } +export type PodcastMetadataResponse = { + readonly data: { + readonly podcastUnionV2: { + readonly rating: { + readonly averageRating: { + readonly average: number + readonly totalRatings: number + } + } + } + } +} + +export type ArtistDetails = { + readonly id: string + readonly profile: { readonly name: string } + readonly visuals: { + readonly avatarImage: { + readonly sources: ImageSources + } + } +} + +export type AlbumTracksResponse = { + readonly data: { + readonly albumUnion: { + readonly tracks: Tracks + } + } +} + +export type Tracks = { + readonly items: { + readonly track: { + readonly playcount: string + readonly name: string + readonly duration: { readonly totalMilliseconds: number } + readonly artists: { + readonly items: { + readonly profile: { readonly name: string } + readonly uri: string + }[] + } + readonly uri: string + } + }[] + readonly totalCount: number +} + +export type AlbumResponse = { + readonly data: { + readonly albumUnion: { + readonly name: string + readonly tracks: Tracks + readonly artists: { + readonly items: ArtistDetails[] + } + readonly coverArt: { + readonly sources: ImageSources + } + readonly date: { + readonly isoString: string + } + + } + } +} + +export type PlaylistContentResponse = { + readonly data: { + readonly playlistV2: { + readonly content: PlaylistContent + } + } +} + +type Owner = { + readonly data: { + readonly name: string + readonly username: string + readonly avatar: null | { + readonly sources: ImageSources + } + + } +} + +export type PlaylistResponse = { + readonly data: { + readonly playlistV2: { + readonly name: string + readonly ownerV2: Owner + readonly images: { + readonly items: { + readonly sources: { + readonly url: string + readonly height: number | null + }[] + }[] + } + readonly content: PlaylistContent + } + } + +} + +type ImageSources = { + readonly height: number + readonly url: string +}[] + +export type PlaylistContent = { + readonly totalCount: number + readonly items: { + readonly addedAt: { + readonly isoString: string + } + readonly itemV2: { + readonly data: { + readonly playcount: string + readonly trackDuration: { + readonly totalMilliseconds: number + } + readonly name: string + readonly uri: string + readonly albumOfTrack: { + readonly coverArt: { + readonly sources: ImageSources + } + } + readonly artists: { + readonly items: { + readonly uri: string + readonly profile: { readonly name: string } + }[] + } + } + } + }[] +} + export type FileManifestResponse = { readonly cdnurl: string[] } -- GitLab