diff --git a/.gitignore b/.gitignore index beb432f6..b724b2a3 100644 --- a/.gitignore +++ b/.gitignore @@ -118,6 +118,7 @@ dist .pnp.* config/*.json +config/yti-* *.txt .idea/ diff --git a/config/ytmusic.json.example b/config/ytmusic.json.example index 758fb51a..ffb48e99 100644 --- a/config/ytmusic.json.example +++ b/config/ytmusic.json.example @@ -4,8 +4,6 @@ "enable": true, "clients": [], "data": { - "cookie": "VISITOR_INFO1_LIVE=jMDXz2_L8rY; __Secure-3PAPISID=3AxsXpSXGqOInSDn1jEKn; DEVICE_INFO=ChxOekU0TmTBpjek5EWZ0G; YSC=7gZdl3Zdl3; SID=TwhNsaZRXYTAtXxzGyu6rZdpg2HvGROeW8J4Ym_FhkhoZMUYEQ.; __Secure-1PSID=TwhNOsaZRXYTyRBe4rxAtXRIKsIEtk_Qot2VLBNfHQrQ.; __Secure-3PSID=ZRXYTAtXRIKsIEtk_Qot2yRBerZdpg2HvvZRXYTAtXRIKsIEtk_Qot2yRBerkuZICFQ.; HSID=A1UMmELW79; SSID=AKhomOs; APISID=IlHHmuzkPdQzZZDhHn3; SAPISID=3AxsXpy0u75Qb/n1jEKn; __Secure-1PAPISID=3AxsXpQb/AkSDn1jEKn; LOGIN_INFO=AFmP6vFpyVCZZAIgDwbkhWMBBhluaIWAPP:QUQ314UW5NWMjNmd2ZUJnYnJsakdIMjZoaE5zVVMjNmd2ZZUiHRlb3ZlV3ZIcUVyRVIMjNmdjNmd2ZZUivYlNqX2ZNZUiHdUNFNFdaYmJIW1NkJRX3hqdlU2YnFESkFuSS1uTldnZVRmLXNjWFc5OUJuR3dTd3JsZGZYa2EtZFQ2a0k2Ry1KQQ==; PREF=volume=26; SIDCC=AFvI_94PxXwls-ndqpGfPgFX3FWj80y_94PxXwls-ndqfSh15sP; __Secure-1PSIDCC=AFvIBnUbRr96I96UCIp2U4T8HRVk2B0HfKzhzxwsiP; __Secure-3PSIDCC=AFvIB3bINuUN0ETDR9gO91wpwWIVmpGki3BxT3bINuUN0ETDR9gO91wCH", - "authUser": 0 } } ] diff --git a/docsite/docs/configuration/configuration.mdx b/docsite/docs/configuration/configuration.mdx index 9f075944..dd684dac 100644 --- a/docsite/docs/configuration/configuration.mdx +++ b/docsite/docs/configuration/configuration.mdx @@ -721,28 +721,30 @@ After starting multi-scrobbler with credentials in-place open the dashboard (`ht ### [Youtube Music](https://music.youtube.com) +
+ + Migrating from YT Music cookie-based Source + + In multi-scrobbler **below v0.9.0** YT Music credentials were extracted from browser cookies. Due to authentication inconsistency and YT service changes this was approach was dropped in favor of [oauth authentication which is more stable.](https://ytjs.dev/guide/authentication.html#youtube-tv-oauth2) + + Your existing credentials cannot be migrated. However, the oauth approach is quite easy. Continue following the directions below to setup new authentication for your YT Music Source. + +
+ :::note -* YT Music authentication is "browser based" which means your credentials may expire after a (long?) period of time OR if you log out of https://music.youtube.com. In the event this happens just repeat the steps below to get new credentials. [See the FAQ](../FAQ.md#youtube-music-fails-after-some-time) for a more detailed explanation. * Communication to YT Music is **unofficial** and not supported or endorsed by Google. This means that **this integration may stop working at any time** if Google decides to change how YT Music works in the browser. * Due to this scrobble history from YTM is often inconsistent and can cause missed scrobbles. [See the FAQ](../FAQ.md#youtube-music-misses-scrobbles) for a more detailed explanation. ::: -Credentials for YT Music are obtained from a browser request to https://music.youtube.com **once you are logged in.** [Specific requirements are here and summarized below:](https://github.com/nickp10/youtube-music-ts-api/blob/master/DOCUMENTATION.md#authenticate) - -* Open a new tab -* Open the developer tools (Ctrl-Shift-I) and select the “Network” tab -* Go to https://music.youtube.com and ensure you are logged in - -Then... +To authenticate simply start multi-scrobbler with an empty YT Music configuration. An authentication URL/code will be logged in additon to being available from the dashboard. -1. Find and select an authenticated POST request. The simplest way is to filter by /browse using the search bar of the developer tools. If you don’t see the request, try scrolling down a bit or clicking on the library button in the top bar. -2. **Make sure **Headers** pane is selected and open -3. In the **Request Headers** section find and copy the **entire value** found after `Cookie:` and use this as the `cookie` value in your multi-scrobbler config -4. If present, in the **Request Headers** section find and copy the number found in `X-google-AuthUser` and use this as the value for `authUser` in your multi-scrobbler config +``` +[2024-10-09 15:24:17.358 -0400] INFO : [App] [Sources] [Ytmusic - MyYTM] ERROR: Sign in with the code 'CLV-KFA-BVKY' using the authentication link on the dashboard or https://www.google.com/device +``` -![Google Headers](google-header.jpg) +Visit the authentication URL and enter the code that was provided (also available on the dashboard). After completing the setup flow MS will log `Auth success` and the YT Music dashboard card will display as **Idle** after refreshing. Click the **Start** link to begin monitoring. #### Configuration diff --git a/docsite/docs/configuration/google-header.jpg b/docsite/docs/configuration/google-header.jpg deleted file mode 100644 index 4ca5f28b..00000000 Binary files a/docsite/docs/configuration/google-header.jpg and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 27d588bd..689b54c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,7 +81,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.0", - "youtube-music-ts-api": "^1.7.0" + "youtubei.js": "^10.5.0" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", @@ -543,6 +543,11 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.0.tgz", + "integrity": "sha512-+imAQkHf7U/Rwvu0wk1XWgsP3WnpCWmK7B48f0XqSNzgk64+grljTKC7pnO/xBiEMUziF7vKRfbBnOQhg126qQ==" + }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", @@ -1132,6 +1137,14 @@ "npm": ">=9.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", @@ -3814,14 +3827,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -4152,14 +4157,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -6611,6 +6608,17 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jintr": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.1.1.tgz", + "integrity": "sha512-89cwX4ouogeDGOBsEVsVYsnWWvWjchmwXBB4kiBhmjOKw19FiOKhNhMhpxhTlK2ctl7DS+d/ethfmuBpzoNNgA==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "dependencies": { + "acorn": "^8.8.0" + } + }, "node_modules/jiti": { "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", @@ -9577,18 +9585,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, - "node_modules/sha1": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sha1/-/sha1-1.1.1.tgz", - "integrity": "sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==", - "dependencies": { - "charenc": ">= 0.0.1", - "crypt": ">= 0.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10693,6 +10689,17 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -11709,12 +11716,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/youtube-music-ts-api": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/youtube-music-ts-api/-/youtube-music-ts-api-1.7.0.tgz", - "integrity": "sha512-IaH14moHLbXTs3e8cM/Zpld+Jf4pQeYslCGmm6karfQSJ5OQ6h1dZ01qblpzKgqZrcbRF3tg7ASAYNDpB0RaFw==", + "node_modules/youtubei.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.5.0.tgz", + "integrity": "sha512-iyA+VF28c15tCCKH9ExM2RKC3zYiHzA/eixGlJ3vERANkuI+xYKzAZ4vtOhmyqwrAddu88R/DkzEsmpph5NWjg==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], "dependencies": { - "sha1": "^1.1.1" + "@bufbuild/protobuf": "^2.0.0", + "jintr": "^2.1.1", + "tslib": "^2.5.0", + "undici": "^5.19.1" } }, "node_modules/zod": { diff --git a/package.json b/package.json index 6a75f74c..7e98aa3c 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "vite-express": "^0.16.0", "vlc-client": "^1.1.1", "xml2js": "0.6.0", - "youtube-music-ts-api": "^1.7.0" + "youtubei.js": "^10.5.0" }, "devDependencies": { "@dbus-types/notifications": "^0.0.5", diff --git a/patches/youtube-music-ts-api+1.7.0.patch b/patches/youtube-music-ts-api+1.7.0.patch deleted file mode 100644 index 68aa5b10..00000000 --- a/patches/youtube-music-ts-api+1.7.0.patch +++ /dev/null @@ -1,68 +0,0 @@ -diff --git a/node_modules/youtube-music-ts-api/build/exports.d.ts b/node_modules/youtube-music-ts-api/build/exports.d.ts -index e256777..c151c4b 100644 ---- a/node_modules/youtube-music-ts-api/build/exports.d.ts -+++ b/node_modules/youtube-music-ts-api/build/exports.d.ts -@@ -7,7 +7,7 @@ declare module 'youtube-music-ts-api' { - } - - declare module 'youtube-music-ts-api/service/youtube-music' { -- import { IYouTubeMusic, IYouTubeMusicAuthenticated, IYouTubeMusicGuest } from "youtube-music-ts-api/interfaces-primary"; -+ import { IYouTubeMusic, IYouTubeMusicAuthenticated, IYouTubeMusicGuest, OnAuthChange } from "youtube-music-ts-api/interfaces-primary"; - /** - * Defines the main YouTube Music API object. Using this object, you can either choose to make calls as a guest or an - * authenticated user. Not all APIs are available as a guest, so it is preferred to authenticate the user if possible. -@@ -19,9 +19,10 @@ declare module 'youtube-music-ts-api/service/youtube-music' { - * @param cookiesStr The cookie string of a valid logged in user. To obtain this cookie value, log into https://music.youtube.com as a user - * and use your browser's developer tools to obtain the "cookie" value sent as a request header. Extra values in the cookie will be ignored. - * @param authUser X-Goog-AuthUser header value -+ * @param onAuthChange A callback called when cookies/authuser are updated based on YTM response - * @returns A promise that will yield authenticated access to the YouTube Music API. - */ -- authenticate(cookiesStr: string, authUser?: number): Promise; -+ authenticate(cookiesStr: string, authUser?: number, onAuthChange?: OnAuthChange): Promise; - /** - * Provides guest access to the YouTube Music API. Only non-restrictive APIs (such as public playlists) are available to guests. - * -@@ -33,6 +34,10 @@ declare module 'youtube-music-ts-api/service/youtube-music' { - - declare module 'youtube-music-ts-api/interfaces-primary' { - import { IAlbumDetail, IAlbumSummary, IArtistSummary, IPlaylistDetail, IPlaylistSummary, ITrackDetail } from "youtube-music-ts-api/interfaces-supplementary"; -+ export type OnAuthChange = (cookieStr: string, authUser: number, changed: Map) => void; - /** - * Defines the main YouTube Music API object. Using this object, you can either choose to make calls as a guest or an - * authenticated user. Not all APIs are available as a guest, so it is preferred to authenticate the user if possible. -@@ -44,9 +49,10 @@ declare module 'youtube-music-ts-api/interfaces-primary' { - * @param cookiesStr The cookie string of a valid logged in user. To obtain this cookie value, log into https://music.youtube.com as a user - * and use your browser's developer tools to obtain the "cookie" value sent as a request header. Extra values in the cookie will be ignored. - * @param authUser X-Goog-AuthUser header value -+ * @param onAuthChange A callback called when cookies/authuser are updated based on YTM response - * @returns A promise that will yield authenticated access to the YouTube Music API. - */ -- authenticate(cookiesStr: string, authUser: number): Promise; -+ authenticate(cookiesStr: string, authUser: number, onAuthChange?: OnAuthChange): Promise; - /** - * Provides guest access to the YouTube Music API. Only non-restrictive APIs (such as public playlists) are available to guests. - * -@@ -112,7 +118,7 @@ declare module 'youtube-music-ts-api/interfaces-primary' { - * - * @returns A promise that will yield a playlist with detailed information on a recently played tracks. - */ -- getLibraryHistory(): Promise; -+ getLibraryHistory(asPlaylists?: boolean): Promise; - /** - * Moves the specified track within the playlist. - * -diff --git a/node_modules/youtube-music-ts-api/build/exports.js b/node_modules/youtube-music-ts-api/build/exports.js -index ae406e1..a9d5481 100644 ---- a/node_modules/youtube-music-ts-api/build/exports.js -+++ b/node_modules/youtube-music-ts-api/build/exports.js -@@ -1,2 +1,3 @@ - #! /usr/bin/env node --!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("https"),require("sha1")):"function"==typeof define&&define.amd?define(["https","sha1"],t):"object"==typeof exports?exports["youtube-music-ts-api"]=t(require("https"),require("sha1")):e["youtube-music-ts-api"]=t(e.https,e.sha1)}(global,((e,t)=>(()=>{"use strict";var s={523:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.default={context:{capabilities:{},client:{clientName:"WEB_REMIX",clientVersion:"0.1",experimentIds:[],experimentsToken:"",gl:"DE",hl:"en",locationInfo:{locationPermissionAuthorizationStatus:"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED"},musicAppInfo:{musicActivityMasterSwitch:"MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",musicLocationMasterSwitch:"MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE",pwaInstallabilityStatus:"PWA_INSTALLABILITY_STATUS_UNKNOWN"},utcOffsetMinutes:60},request:{internalExperimentFlags:[{key:"force_music_enable_outertube_tastebuilder_browse",value:"true"},{key:"force_music_enable_outertube_playlist_detail_browse",value:"true"},{key:"force_music_enable_outertube_search_suggestions",value:"true"}],sessionIndex:{}},user:{enableSafetyMode:!1}}}},479:function(e,t,s){var r=this&&this.__createBinding||(Object.create?function(e,t,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(t,s);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,r,i)}:function(e,t,s,r){void 0===r&&(r=s),e[r]=t[s]}),i=this&&this.__exportStar||function(e,t){for(var s in e)"default"===s||Object.prototype.hasOwnProperty.call(t,s)||r(t,e,s)},n=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=s(116);Object.defineProperty(t,"default",{enumerable:!0,get:function(){return n(a).default}}),i(s(800),t),i(s(189),t)},800:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0})},189:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0})},175:function(e,t,s){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=r(s(841)),n=r(s(421));class a extends i.default{constructor(){super(),this.trackParser=new n.default}parseAlbumsSummaryResponse(e){const t=[],s=this.traverse(e,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents","*","itemSectionRenderer","contents","0","gridRenderer","items");if(Array.isArray(s))for(let e=0;e{Object.defineProperty(t,"__esModule",{value:!0}),t.default=class{traverse(e,...t){if(e){if(Array.isArray(t)&&t.length>0){if(Array.isArray(e)&&"*"===t[0]){for(let s=0;s1){t=parseInt(n[1]);break}}return{id:this.traverse(e,"musicTwoRowItemRenderer","title","runs","0","navigationEndpoint","browseEndpoint","browseId"),name:this.traverse(e,"musicTwoRowItemRenderer","title","runs","0","text"),thumbnails:this.traverse(e,"musicTwoRowItemRenderer","thumbnailRenderer","musicThumbnailRenderer","thumbnail","thumbnails"),count:t}}parsePlaylistDetailResponse(e){const t=this.traverse(e,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents","0","musicPlaylistShelfRenderer");if(t)return this.parsePlaylistDetail(e,t)}parsePlaylistDetailContinuation(e,t){const s=this.traverse(t,"continuationContents","musicPlaylistShelfContinuation","contents"),r=this.trackParser.parseTrackDetails(s);Array.isArray(e.tracks)&&e.tracks.push.apply(e.tracks,r),e.continuationToken=this.traverse(t,"continuationContents","musicPlaylistShelfContinuation","continuations","0","nextContinuationData","continuation")}parsePlaylistDetail(e,t){const s=void 0===this.traverse(e,"header","musicEditablePlaylistDetailHeaderRenderer"),r=s?e:this.traverse(e,"header","musicEditablePlaylistDetailHeaderRenderer"),i=s?"PUBLIC":this.traverse(r,"editHeader","musicPlaylistEditHeaderRenderer","privacy"),n=this.traverse(r,"header","musicDetailHeaderRenderer");let a=0;const o=this.traverse(n,"secondSubtitle","runs","0","text");if(o){const e=o.split(" ");e&&e.length>0&&(a=parseInt(e[0]))}const u=this.traverse(t,"contents");return{id:this.traverse(t,"playlistId"),name:this.traverse(n,"title","runs","0","text"),description:this.traverse(n,"description","runs","0","text"),privacy:i,count:a,tracks:this.trackParser.parseTrackDetails(u),continuationToken:this.traverse(t,"continuations","0","nextContinuationData","continuation")}}mergeValidPlaylistTracks(...e){const t=[];for(const s of e)for(const e of s.tracks)this.trackParser.isTrackDataMissing(e)||t.find((t=>t.id===e.id))||t.push(e);return t}}t.default=a},421:function(e,t,s){var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const i=r(s(841));class n extends i.default{parseTrackDetails(e){const t=[];if(Array.isArray(e))for(const s of e){const e=this.traverse(s,"musicResponsiveListItemRenderer");if(e){const s=this.parseTrackDetail(e);s&&s.id&&t.push(s)}}return t}parseTrackDetail(e){const t=[],s=[],r=this.traverse(e,"flexColumns","1","musicResponsiveListItemFlexColumnRenderer","text","runs");if(Array.isArray(r))for(const e of r){const r=this.traverse(e,"navigationEndpoint","browseEndpoint","browseId"),i=this.traverse(e,"text");i&&(r&&s.push({id:r,name:i}),t.push({id:r,name:i}))}let i;const n=this.traverse(e,"flexColumns","2","musicResponsiveListItemFlexColumnRenderer","text","runs","0");n&&(i={id:this.traverse(n,"navigationEndpoint","browseEndpoint","browseId"),name:this.traverse(n,"text")});const a=this.traverse(e,"fixedColumns","0","musicResponsiveListItemFixedColumnRenderer","text","simpleText")||this.traverse(e,"fixedColumns","0","musicResponsiveListItemFixedColumnRenderer","text","runs","0","text"),o=this.traverse(e,"menu","menuRenderer","topLevelButtons");let u="INDIFFERENT";if(Array.isArray(o))for(const e of o)e.likeButtonRenderer&&(u=e.likeButtonRenderer.likeStatus);return{id:this.traverse(e,"overlay","musicItemThumbnailOverlayRenderer","content","musicPlayButtonRenderer","playNavigationEndpoint","watchEndpoint","videoId"),alternateId:this.traverse(e,"playlistItemData","playlistSetVideoId"),title:this.traverse(e,"flexColumns","0","musicResponsiveListItemFlexColumnRenderer","text","runs","0","text"),artists:s.length>0?s:t,album:i,thumbnails:this.traverse(e,"thumbnail","musicThumbnailRenderer","thumbnail","thumbnails"),likeStatus:u,duration:a}}isTrackDataMissing(e){return"Song is private"==e.title}parseTracksDetailResponse(e){const t=this.traverse(e,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents","*","itemSectionRenderer","contents","0","musicShelfRenderer"),s=this.traverse(t,"contents");return{continuationToken:this.traverse(t,"continuations","0","nextContinuationData","continuation"),tracks:this.parseTrackDetails(s)}}parseTracksDetailContinuation(e,t){const s=this.traverse(t,"continuationContents","musicShelfContinuation","contents"),r=this.parseTrackDetails(s);Array.isArray(e.tracks)&&e.tracks.push.apply(e.tracks,r),e.continuationToken=this.traverse(t,"continuationContents","musicShelfContinuation","continuations","0","nextContinuationData","continuation")}parseAlbumTrackDetails(e,t,s){const r=[];for(const i of e)r.push({id:this.traverse(i,"videoId"),title:this.traverse(i,"title"),artists:t,album:s,durationMillis:parseInt(this.traverse(i,"lengthMs")),trackNumber:parseInt(this.traverse(i,"albumTrackIndex"))});return r}}t.default=n},653:function(e,t,s){var r=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(i,n){function a(e){try{u(r.next(e))}catch(e){n(e)}}function o(e){try{u(r.throw(e))}catch(e){n(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(a,o)}u((r=r.apply(e,t||[])).next())}))},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=i(s(26)),a=i(s(418));class o extends a.default{constructor(e,t){super(),this.cookies=e,this.authUser=t}generateHeaders(){return Object.assign(Object.assign({},super.generateHeaders()),{Authorization:this.generateAuthorization(),Cookie:this.generateCookie(),"X-Goog-AuthUser":this.authUser})}generateAuthorization(){let e=(new Date).getTime();const t=`${e} ${this.cookies.get("__Secure-3PAPISID")} ${this.origin}`;return`SAPISIDHASH ${e}_${(0,n.default)(t)}`}generateCookie(){let e="";return this.cookies.forEach((function(t,s){e&&(e+=";"),e+=s+"="+t})),e}getLibraryAlbums(){return r(this,void 0,void 0,(function*(){const e=yield this.sendRequest("browse",{browseId:"FEmusic_liked_albums"});return this.albumParser.parseAlbumsSummaryResponse(e)}))}getLibraryArtists(){return r(this,void 0,void 0,(function*(){const e=yield this.sendRequest("browse",{browseId:"FEmusic_library_corpus_track_artists"});return this.artistParser.parseArtistsSummaryResponse(e)}))}getLibraryPlaylists(){return r(this,void 0,void 0,(function*(){const e=yield this.sendRequest("browse",{browseId:"FEmusic_liked_playlists"});return this.playlistParser.parsePlaylistsSummaryResponse(e)}))}getLibraryTracks(){return r(this,void 0,void 0,(function*(){return this.getLibraryTracksInternal()}))}getLibraryTracksInternal(){return r(this,void 0,void 0,(function*(){const e={browseId:"FEmusic_liked_videos"},t=yield this.sendRequest("browse",e),s=this.trackParser.parseTracksDetailResponse(t);for(;s.continuationToken;){const t=yield this.sendRequest("browse",e,`ctoken=${s.continuationToken}&continuation=${s.continuationToken}`);this.trackParser.parseTracksDetailContinuation(s,t)}return s.tracks}))}getLibraryHistory(){return r(this,void 0,void 0,(function*(){const e=yield this.sendRequest("browse",{browseId:"FEmusic_history"}),t=this.playlistParser.traverse(e,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents"),s=[];for(const e of t)void 0!==e.musicShelfRenderer&&void 0!==e.musicShelfRenderer.contents&&e.musicShelfRenderer.contents.length>0&&s.push(e.musicShelfRenderer.contents);const r=s.flat(1);return{id:"FEmusic_history",name:"History",description:"Recently played music in reverse chronological order",privacy:"PRIVATE",count:r.length,tracks:this.trackParser.parseTrackDetails(r)}}))}createPlaylist(e,t,s,i){return r(this,void 0,void 0,(function*(){const r=yield this.sendRequest("playlist/create",{title:e,description:t,privacyStatus:s||"PRIVATE",sourcePlaylistId:this.playlistIdTrim(i)});if(r&&r.playlistId)return{id:r.playlistId,name:e,count:0}}))}deletePlaylist(e){return r(this,void 0,void 0,(function*(){return"STATUS_SUCCEEDED"===(yield this.sendRequest("playlist/delete",{playlistId:this.playlistIdTrim(e)})).status}))}addTracksToPlaylist(e,...t){return r(this,void 0,void 0,(function*(){return"STATUS_SUCCEEDED"===(yield this.sendRequest("browse/edit_playlist",{playlistId:this.playlistIdTrim(e),actions:t.map((e=>({action:"ACTION_ADD_VIDEO",addedVideoId:e.id})))})).status}))}removeTracksFromPlaylist(e,...t){return r(this,void 0,void 0,(function*(){const s=[];for(const e of t){if(!e.id)throw new Error("Track ID is missing. Ensure you have both the ID and the Alternate ID.");if(!e.alternateId)throw new Error("Track Alternate ID is missing. Ensure you have both the ID and the Alternate ID.");s.push({action:"ACTION_REMOVE_VIDEO",removedVideoId:e.id,setVideoId:e.alternateId})}return"STATUS_SUCCEEDED"===(yield this.sendRequest("browse/edit_playlist",{playlistId:this.playlistIdTrim(e),actions:s})).status}))}moveTrackWithinPlaylist(e,t,s){return r(this,void 0,void 0,(function*(){const r=[];if(!t||!t.alternateId)throw new Error("The track being moved is missing. Ensure you have specified a track to move.");return r.push({action:"ACTION_MOVE_VIDEO_BEFORE",movedSetVideoIdSuccessor:s?s.alternateId:void 0,setVideoId:t.alternateId}),"STATUS_SUCCEEDED"===(yield this.sendRequest("browse/edit_playlist",{playlistId:this.playlistIdTrim(e),actions:r})).status}))}rateTrack(e,t){return r(this,void 0,void 0,(function*(){let s;return s="LIKE"===t?"like/like":"DISLIKE"===t?"like/dislike":"like/removelike","STATUS_SUCCEEDED"===(yield this.sendRequest(s,{target:{videoId:e}})).status}))}}t.default=o},303:function(e,t,s){var r=this&&this.__createBinding||(Object.create?function(e,t,s,r){void 0===r&&(r=s);var i=Object.getOwnPropertyDescriptor(t,s);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,r,i)}:function(e,t,s,r){void 0===r&&(r=s),e[r]=t[s]}),i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),n=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var s in e)"default"!==s&&Object.prototype.hasOwnProperty.call(e,s)&&r(t,e,s);return i(t,e),t},a=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(i,n){function a(e){try{u(r.next(e))}catch(e){n(e)}}function o(e){try{u(r.throw(e))}catch(e){n(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(a,o)}u((r=r.apply(e,t||[])).next())}))},o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const u=n(s(799)),l=o(s(175)),c=o(s(304)),d=o(s(660)),h=o(s(421)),p=o(s(523));t.default=class{constructor(){this.hostname="music.youtube.com",this.basePath="/youtubei/v1/",this.queryString="?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30",this.origin="https://music.youtube.com",this.albumParser=new l.default,this.artistParser=new c.default,this.playlistParser=new d.default,this.trackParser=new h.default}generateHeaders(){return{"X-Origin":this.origin}}sendRequest(e,t,s){return a(this,void 0,void 0,(function*(){let r;t&&(t=Object.assign(Object.assign({},p.default),t),r=JSON.stringify(t));const i=s?`${this.queryString}&${s}`:this.queryString,n=yield this.sendHttpsRequest({hostname:this.hostname,path:`${this.basePath}${e}${i}`,method:"POST",headers:this.generateHeaders()},r);if(200===n.statusCode&&n.body){const e=JSON.parse(n.body);if(e)return e}throw new Error(`Could not send the specified request to ${e}. Status code: ${n.statusCode}`)}))}sendHttpsRequest(e,t){return a(this,void 0,void 0,(function*(){return new Promise(((s,r)=>{const i=e.headers||{};e.headers=i,e.timeout=6e4,t&&(i["Content-Type"]="application/json",i["Content-Length"]=t.length);const n=u.request(e,(e=>{let t="";e.on("data",(e=>{t+=e})),e.on("end",(()=>{e.body=t,s(e)}))})).on("error",(e=>{r(e)}));t&&n.write(t),n.end()}))}))}playlistIdTrim(e){return e&&e.toUpperCase().startsWith("VL")?e.substring(2):e}playlistIdPad(e){return e&&!e.toUpperCase().startsWith("VL")?"VL"+e:e}}},418:function(e,t,s){var r=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(i,n){function a(e){try{u(r.next(e))}catch(e){n(e)}}function o(e){try{u(r.throw(e))}catch(e){n(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(a,o)}u((r=r.apply(e,t||[])).next())}))},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=i(s(303));class a extends n.default{constructor(){super()}getAlbum(e){return r(this,void 0,void 0,(function*(){const t={browseId:e,browseEndpointContextSupportedConfigs:{browseEndpointContextMusicConfig:{pageType:"MUSIC_PAGE_TYPE_ALBUM"}}},s=yield this.sendRequest("browse",t);return this.albumParser.parseAlbumDetailResponse(s)}))}getPlaylist(e,t=0){return r(this,void 0,void 0,(function*(){const s=yield this.getPlaylistInternal(e);for(;t>0;){const r=s.tracks.length!==s.count,i=!!s.tracks.find((e=>this.trackParser.isTrackDataMissing(e)));if(!r&&!i)break;{const r=yield this.getPlaylistInternal(e),i=this.playlistParser.mergeValidPlaylistTracks(s,r);s.tracks=i,t--}}return s}))}getPlaylistInternal(e){return r(this,void 0,void 0,(function*(){const t={browseId:this.playlistIdPad(e),browseEndpointContextSupportedConfigs:{browseEndpointContextMusicConfig:{pageType:"MUSIC_PAGE_TYPE_PLAYLIST"}}},s=yield this.sendRequest("browse",t),r=this.playlistParser.parsePlaylistDetailResponse(s);for(;r.continuationToken;){const e=yield this.sendRequest("browse",t,`ctoken=${r.continuationToken}&continuation=${r.continuationToken}`);this.playlistParser.parsePlaylistDetailContinuation(r,e)}return r}))}}t.default=a},116:function(e,t,s){var r=this&&this.__awaiter||function(e,t,s,r){return new(s||(s=Promise))((function(i,n){function a(e){try{u(r.next(e))}catch(e){n(e)}}function o(e){try{u(r.throw(e))}catch(e){n(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(a,o)}u((r=r.apply(e,t||[])).next())}))},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});const n=i(s(653)),a=i(s(418));t.default=class{authenticate(e,t=0){return r(this,void 0,void 0,(function*(){if(!e)throw new Error("The specific cookie string is missing");const s=e.split(";");if(!s||0===s.length)throw new Error("An invalid cookie string was specified");const r=new Map;for(const e of s){const t=e.split("=");t&&2===t.length&&r.set(t[0].trim(),t[1].trim())}return new n.default(r,t)}))}guest(){return r(this,void 0,void 0,(function*(){return new a.default}))}}},799:t=>{t.exports=e},26:e=>{e.exports=t}},r={};return function e(t){var i=r[t];if(void 0!==i)return i.exports;var n=r[t]={exports:{}};return s[t].call(n.exports,n,n.exports,e),n.exports}(479)})())); -\ No newline at end of file -+/*! For license information please see exports.js.LICENSE.txt */ -+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("https"),require("sha1")):"function"==typeof define&&define.amd?define(["https","sha1"],e):"object"==typeof exports?exports["youtube-music-ts-api"]=e(require("https"),require("sha1")):t["youtube-music-ts-api"]=e(t.https,t.sha1)}(global,((t,e)=>(()=>{"use strict";var r={611:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.default={context:{capabilities:{},client:{clientName:"WEB_REMIX",clientVersion:"0.1",experimentIds:[],experimentsToken:"",gl:"DE",hl:"en",locationInfo:{locationPermissionAuthorizationStatus:"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED"},musicAppInfo:{musicActivityMasterSwitch:"MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE",musicLocationMasterSwitch:"MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE",pwaInstallabilityStatus:"PWA_INSTALLABILITY_STATUS_UNKNOWN"},utcOffsetMinutes:60},request:{internalExperimentFlags:[{key:"force_music_enable_outertube_tastebuilder_browse",value:"true"},{key:"force_music_enable_outertube_playlist_detail_browse",value:"true"},{key:"force_music_enable_outertube_search_suggestions",value:"true"}],sessionIndex:{}},user:{enableSafetyMode:!1}}}},77:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0})},98:(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0})},710:function(t,e,r){function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function o(t,e){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!r){if(Array.isArray(t)||(r=function(t,e){if(t){if("string"==typeof t)return i(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?i(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){r&&(t=r);var n=0,o=function(){};return{s:o,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,s=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return u=t.done,t},e:function(t){s=!0,a=t},f:function(){try{u||null==r.return||r.return()}finally{if(s)throw a}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r{function r(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r1&&void 0!==arguments[1]?arguments[1]:0;if(!t)throw new Error("The specific cookie string is missing");var n=t.split(";");if(!n||0===n.length)throw new Error("An invalid cookie string was specified");var o,i=new Map,a=function(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=function(t,e){if(t){if("string"==typeof t)return r(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var o=0,i=function(){};return{s:i,n:function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,s=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return u=t.done,t},e:function(t){s=!0,a=t},f:function(){try{u||null==n.return||n.return()}finally{if(s)throw a}}}}(n);try{for(a.s();!(o=a.n()).done;){var u=o.value.split("=");u&&2===u.length&&i.set(u[0].trim(),u[1].trim())}}catch(t){a.e(t)}finally{a.f()}return[i,e]},e.buildCookies=function(t){var e="";return t.forEach((function(t,r){e&&(e+=";"),e+=r+"="+t})),e}},445:(t,e)=>{function r(t){return r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r(t)}function n(t){return function(t){if(Array.isArray(t))return o(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return o(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?o(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r1?e-1:0),o=1;o0){if(Array.isArray(t)&&"*"===r[0]){for(var i=0;i=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,s=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return u=t.done,t},e:function(t){s=!0,a=t},f:function(){try{u||null==r.return||r.return()}finally{if(s)throw a}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r1){e=parseInt(a[1]);break}}return{id:this.traverse(t,"musicTwoRowItemRenderer","title","runs","0","navigationEndpoint","browseEndpoint","browseId"),name:this.traverse(t,"musicTwoRowItemRenderer","title","runs","0","text"),thumbnails:this.traverse(t,"musicTwoRowItemRenderer","thumbnailRenderer","musicThumbnailRenderer","thumbnail","thumbnails"),count:e}}},{key:"parsePlaylistDetailResponse",value:function(t){var e=this.traverse(t,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents","0","musicPlaylistShelfRenderer");if(e)return this.parsePlaylistDetail(t,e)}},{key:"parsePlaylistDetailContinuation",value:function(t,e){var r=this.traverse(e,"continuationContents","musicPlaylistShelfContinuation","contents"),n=this.trackParser.parseTrackDetails(r);Array.isArray(t.tracks)&&t.tracks.push.apply(t.tracks,n),t.continuationToken=this.traverse(e,"continuationContents","musicPlaylistShelfContinuation","continuations","0","nextContinuationData","continuation")}},{key:"parsePlaylistDetail",value:function(t,e){var r=void 0===this.traverse(t,"header","musicEditablePlaylistDetailHeaderRenderer"),n=r?t:this.traverse(t,"header","musicEditablePlaylistDetailHeaderRenderer"),o=r?"PUBLIC":this.traverse(n,"editHeader","musicPlaylistEditHeaderRenderer","privacy"),i=this.traverse(n,"header","musicDetailHeaderRenderer"),a=0,u=this.traverse(i,"secondSubtitle","runs","0","text");if(u){var s=u.split(" ");s&&s.length>0&&(a=parseInt(s[0]))}var c=this.traverse(e,"contents");return{id:this.traverse(e,"playlistId"),name:this.traverse(i,"title","runs","0","text"),description:this.traverse(i,"description","runs","0","text"),privacy:o,count:a,tracks:this.trackParser.parseTrackDetails(c),continuationToken:this.traverse(e,"continuations","0","nextContinuationData","continuation")}}},{key:"mergeValidPlaylistTracks",value:function(){for(var t=this,e=[],r=arguments.length,n=new Array(r),i=0;i=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,s=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return u=t.done,t},e:function(t){s=!0,a=t},f:function(){try{u||null==r.return||r.return()}finally{if(s)throw a}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r0?r:e,album:l,thumbnails:this.traverse(t,"thumbnail","musicThumbnailRenderer","thumbnail","thumbnails"),likeStatus:y,duration:h}}},{key:"isTrackDataMissing",value:function(t){return"Song is private"==t.title}},{key:"parseTracksDetailResponse",value:function(t){var e=this.traverse(t,"contents","singleColumnBrowseResultsRenderer","tabs","0","tabRenderer","content","sectionListRenderer","contents","*","itemSectionRenderer","contents","0","musicShelfRenderer"),r=this.traverse(e,"contents");return{continuationToken:this.traverse(e,"continuations","0","nextContinuationData","continuation"),tracks:this.parseTrackDetails(r)}}},{key:"parseTracksDetailContinuation",value:function(t,e){var r=this.traverse(e,"continuationContents","musicShelfContinuation","contents"),n=this.parseTrackDetails(r);Array.isArray(t.tracks)&&t.tracks.push.apply(t.tracks,n),t.continuationToken=this.traverse(e,"continuationContents","musicShelfContinuation","continuations","0","nextContinuationData","continuation")}},{key:"parseAlbumTrackDetails",value:function(t,e,r){var n,i=[],a=o(t);try{for(a.s();!(n=a.n()).done;){var u=n.value;i.push({id:this.traverse(u,"videoId"),title:this.traverse(u,"title"),artists:e,album:r,durationMillis:parseInt(this.traverse(u,"lengthMs")),trackNumber:parseInt(this.traverse(u,"albumTrackIndex"))})}}catch(t){a.e(t)}finally{a.f()}return i}}])&&a(e.prototype,r),Object.defineProperty(e,"prototype",{writable:!1}),f}(c(r(445)).default);e.default=l},573:function(t,e,r){function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function o(){o=function(){return e};var t,e={},r=Object.prototype,i=r.hasOwnProperty,a=Object.defineProperty||function(t,e,r){t[e]=r.value},u="function"==typeof Symbol?Symbol:{},s=u.iterator||"@@iterator",c=u.asyncIterator||"@@asyncIterator",l=u.toStringTag||"@@toStringTag";function f(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{f({},"")}catch(t){f=function(t,e,r){return t[e]=r}}function h(t,e,r,n){var o=e&&e.prototype instanceof g?e:g,i=Object.create(o.prototype),u=new A(n||[]);return a(i,"_invoke",{value:j(t,r,u)}),i}function p(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=h;var y="suspendedStart",v="suspendedYield",d="executing",m="completed",b={};function g(){}function w(){}function _(){}var k={};f(k,s,(function(){return this}));var x=Object.getPrototypeOf,S=x&&x(x(L([])));S&&S!==r&&i.call(S,s)&&(k=S);var O=_.prototype=g.prototype=Object.create(k);function E(t){["next","throw","return"].forEach((function(e){f(t,e,(function(t){return this._invoke(e,t)}))}))}function P(t,e){function r(o,a,u,s){var c=p(t[o],t,a);if("throw"!==c.type){var l=c.arg,f=l.value;return f&&"object"==n(f)&&i.call(f,"__await")?e.resolve(f.__await).then((function(t){r("next",t,u,s)}),(function(t){r("throw",t,u,s)})):e.resolve(f).then((function(t){l.value=t,u(l)}),(function(t){return r("throw",t,u,s)}))}s(c.arg)}var o;a(this,"_invoke",{value:function(t,n){function i(){return new e((function(e,o){r(t,n,e,o)}))}return o=o?o.then(i,i):i()}})}function j(e,r,n){var o=y;return function(i,a){if(o===d)throw new Error("Generator is already running");if(o===m){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var u=n.delegate;if(u){var s=R(u,n);if(s){if(s===b)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===y)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=d;var c=p(e,r,n);if("normal"===c.type){if(o=n.done?m:v,c.arg===b)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o=m,n.method="throw",n.arg=c.arg)}}}function R(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,R(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),b;var i=p(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,b;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,b):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,b)}function T(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function I(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function A(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(T,this),this.reset(!0)}function L(e){if(e||""===e){var r=e[s];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,a=function r(){for(;++o=0;--o){var a=this.tryEntries[o],u=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=i.call(a,"catchLoc"),c=i.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),I(r),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;I(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:L(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),b}},e}function i(t,e){var r="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!r){if(Array.isArray(t)||(r=u(t))||e&&t&&"number"==typeof t.length){r&&(t=r);var n=0,o=function(){};return{s:o,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,s=!1;return{s:function(){r=r.call(t)},n:function(){var t=r.next();return a=t.done,t},e:function(t){s=!0,i=t},f:function(){try{a||null==r.return||r.return()}finally{if(s)throw i}}}}function a(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var n,o,i,a,u=[],s=!0,c=!1;try{if(i=(r=r.call(t)).next,0===e){if(Object(r)!==r)return;s=!1}else for(;!(s=(n=i.call(r)).done)&&(u.push(n.value),u.length!==e);s=!0);}catch(t){c=!0,o=t}finally{try{if(!s&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(c)throw o}}return u}}(t,e)||u(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function u(t,e){if(t){if("string"==typeof t)return s(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?s(t,e):void 0}}function s(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r0&&(t?u.push({id:c.toString(),name:h.musicShelfRenderer.title.runs[0].text,description:"Recently played from ".concat(h.musicShelfRenderer.title.runs[0].text),privacy:"PRIVATE",count:h.musicShelfRenderer.contents.length,tracks:this.trackParser.parseTrackDetails(h.musicShelfRenderer.contents)}):s.push(h.musicShelfRenderer.contents)),c++}catch(t){l.e(t)}finally{l.f()}if(!t){e.next=12;break}return e.abrupt("return",u);case 12:return p=s.flat(1),e.abrupt("return",{id:"FEmusic_history",name:"History",description:"Recently played music in reverse chronological order",privacy:"PRIVATE",count:p.length,tracks:this.trackParser.parseTrackDetails(p)});case 14:case"end":return e.stop()}}),e,this)})))}},{key:"createPlaylist",value:function(t,e,r,n){return p(this,void 0,void 0,o().mark((function i(){var a;return o().wrap((function(o){for(;;)switch(o.prev=o.next){case 0:return o.next=2,this.sendRequest("playlist/create",{title:t,description:e,privacyStatus:r||"PRIVATE",sourcePlaylistId:this.playlistIdTrim(n)});case 2:if((a=o.sent)&&a.playlistId){o.next=5;break}return o.abrupt("return",void 0);case 5:return o.abrupt("return",{id:a.playlistId,name:t,count:0});case 6:case"end":return o.stop()}}),i,this)})))}},{key:"deletePlaylist",value:function(t){return p(this,void 0,void 0,o().mark((function e(){var r;return o().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,this.sendRequest("playlist/delete",{playlistId:this.playlistIdTrim(t)});case 2:return r=e.sent,e.abrupt("return","STATUS_SUCCEEDED"===r.status);case 4:case"end":return e.stop()}}),e,this)})))}},{key:"addTracksToPlaylist",value:function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),n=1;n1?e-1:0),n=1;n=0;--o){var a=this.tryEntries[o],u=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=i.call(a,"catchLoc"),c=i.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),I(r),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;I(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:L(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),b}},e}function i(t,e){for(var r=0;r=0;--o){var a=this.tryEntries[o],u=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=i.call(a,"catchLoc"),c=i.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),I(r),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;I(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:L(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),b}},e}function i(t,e){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:0;return s(this,void 0,void 0,o().mark((function r(){var n,i,a,u,s,c=this;return o().wrap((function(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,this.getPlaylistInternal(t);case 2:n=r.sent;case 3:if(!(e>0)){r.next=18;break}if(i=n.tracks.length!==n.count,a=!!n.tracks.find((function(t){return c.trackParser.isTrackDataMissing(t)})),!i&&!a){r.next=15;break}return r.next=9,this.getPlaylistInternal(t);case 9:u=r.sent,s=this.playlistParser.mergeValidPlaylistTracks(n,u),n.tracks=s,e--,r.next=16;break;case 15:return r.abrupt("break",18);case 16:r.next=3;break;case 18:return r.abrupt("return",n);case 19:case"end":return r.stop()}}),r,this)})))}},{key:"getPlaylistInternal",value:function(t){return s(this,void 0,void 0,o().mark((function e(){var r,n,i,a;return o().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return r={browseId:this.playlistIdPad(t),browseEndpointContextSupportedConfigs:{browseEndpointContextMusicConfig:{pageType:"MUSIC_PAGE_TYPE_PLAYLIST"}}},e.next=3,this.sendRequest("browse",r);case 3:n=e.sent,i=this.playlistParser.parsePlaylistDetailResponse(n);case 5:if(!i.continuationToken){e.next=12;break}return e.next=8,this.sendRequest("browse",r,"ctoken=".concat(i.continuationToken,"&continuation=").concat(i.continuationToken));case 8:a=e.sent,this.playlistParser.parsePlaylistDetailContinuation(i,a),e.next=5;break;case 12:return e.abrupt("return",i);case 13:case"end":return e.stop()}}),e,this)})))}}],r&&i(e.prototype,r),Object.defineProperty(e,"prototype",{writable:!1}),h}(c(r(409)).default);e.default=l},365:function(t,e,r){function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},n(t)}function o(){o=function(){return e};var t,e={},r=Object.prototype,i=r.hasOwnProperty,a=Object.defineProperty||function(t,e,r){t[e]=r.value},u="function"==typeof Symbol?Symbol:{},s=u.iterator||"@@iterator",c=u.asyncIterator||"@@asyncIterator",l=u.toStringTag||"@@toStringTag";function f(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{f({},"")}catch(t){f=function(t,e,r){return t[e]=r}}function h(t,e,r,n){var o=e&&e.prototype instanceof g?e:g,i=Object.create(o.prototype),u=new A(n||[]);return a(i,"_invoke",{value:j(t,r,u)}),i}function p(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=h;var y="suspendedStart",v="suspendedYield",d="executing",m="completed",b={};function g(){}function w(){}function _(){}var k={};f(k,s,(function(){return this}));var x=Object.getPrototypeOf,S=x&&x(x(L([])));S&&S!==r&&i.call(S,s)&&(k=S);var O=_.prototype=g.prototype=Object.create(k);function E(t){["next","throw","return"].forEach((function(e){f(t,e,(function(t){return this._invoke(e,t)}))}))}function P(t,e){function r(o,a,u,s){var c=p(t[o],t,a);if("throw"!==c.type){var l=c.arg,f=l.value;return f&&"object"==n(f)&&i.call(f,"__await")?e.resolve(f.__await).then((function(t){r("next",t,u,s)}),(function(t){r("throw",t,u,s)})):e.resolve(f).then((function(t){l.value=t,u(l)}),(function(t){return r("throw",t,u,s)}))}s(c.arg)}var o;a(this,"_invoke",{value:function(t,n){function i(){return new e((function(e,o){r(t,n,e,o)}))}return o=o?o.then(i,i):i()}})}function j(e,r,n){var o=y;return function(i,a){if(o===d)throw new Error("Generator is already running");if(o===m){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var u=n.delegate;if(u){var s=R(u,n);if(s){if(s===b)continue;return s}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===y)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=d;var c=p(e,r,n);if("normal"===c.type){if(o=n.done?m:v,c.arg===b)continue;return{value:c.arg,done:n.done}}"throw"===c.type&&(o=m,n.method="throw",n.arg=c.arg)}}}function R(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,R(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),b;var i=p(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,b;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,b):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,b)}function T(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function I(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function A(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(T,this),this.reset(!0)}function L(e){if(e||""===e){var r=e[s];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,a=function r(){for(;++o=0;--o){var a=this.tryEntries[o],u=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var s=i.call(a,"catchLoc"),c=i.call(a,"finallyLoc");if(s&&c){if(this.prev=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),I(r),b}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;I(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:L(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),b}},e}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=new Array(e);r1&&void 0!==arguments[1]?arguments[1]:0,r=arguments.length>2?arguments[2]:void 0;return u(this,void 0,void 0,o().mark((function n(){var a,u,s;return o().wrap((function(n){for(;;)switch(n.prev=n.next){case 0:return n.prev=0,a=(0,c.parseAuth)(t,e),f=2,u=function(t){if(Array.isArray(t))return t}(o=a)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var n,o,i,a,u=[],s=!0,c=!1;try{if(i=(r=r.call(t)).next,0===e){if(Object(r)!==r)return;s=!1}else for(;!(s=(n=i.call(r)).done)&&(u.push(n.value),u.length!==e);s=!0);}catch(t){c=!0,o=t}finally{try{if(!s&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(c)throw o}}return u}}(o,f)||function(t,e){if(t){if("string"==typeof t)return i(t,e);var r=Object.prototype.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?i(t,e):void 0}}(o,f)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}(),s=u[0],u[1],n.abrupt("return",new l.default(s,e,r));case 5:throw n.prev=5,n.t0=n.catch(0),n.t0;case 8:case"end":return n.stop()}var o,f}),n,null,[[0,5]])})))}},{key:"guest",value:function(){return u(this,void 0,void 0,o().mark((function t(){return o().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.abrupt("return",new f.default);case 1:case"end":return t.stop()}}),t)})))}}],r&&a(e.prototype,r),Object.defineProperty(e,"prototype",{writable:!1}),t}();e.default=h},479:function(t,e,r){var n=this&&this.__createBinding||(Object.create?function(t,e,r,n){void 0===n&&(n=r);var o=Object.getOwnPropertyDescriptor(e,r);o&&!("get"in o?!e.__esModule:o.writable||o.configurable)||(o={enumerable:!0,get:function(){return e[r]}}),Object.defineProperty(t,n,o)}:function(t,e,r,n){void 0===n&&(n=r),t[n]=e[r]}),o=this&&this.__exportStar||function(t,e){for(var r in t)"default"===r||Object.prototype.hasOwnProperty.call(e,r)||n(e,t,r)},i=this&&this.__importDefault||function(t){return t&&t.__esModule?t:{default:t}};Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var a=r(365);Object.defineProperty(e,"default",{enumerable:!0,get:function(){return i(a).default}}),o(r(77),e),o(r(98),e)},799:e=>{e.exports=t},26:t=>{t.exports=e}},n={};return function t(e){var o=n[e];if(void 0!==o)return o.exports;var i=n[e]={exports:{}};return r[e].call(i.exports,i,i.exports,t),i.exports}(479)})())); -\ No newline at end of file diff --git a/src/backend/common/AbstractComponent.ts b/src/backend/common/AbstractComponent.ts index 3ae51e5f..3d42b7ee 100644 --- a/src/backend/common/AbstractComponent.ts +++ b/src/backend/common/AbstractComponent.ts @@ -318,4 +318,8 @@ export default abstract class AbstractComponent { return play; } } + + public additionalApiData(): Record { + return {}; + } } diff --git a/src/backend/common/infrastructure/config/source/ytmusic.ts b/src/backend/common/infrastructure/config/source/ytmusic.ts index 2e70513a..36802dcb 100644 --- a/src/backend/common/infrastructure/config/source/ytmusic.ts +++ b/src/backend/common/infrastructure/config/source/ytmusic.ts @@ -1,32 +1,24 @@ import { PollingOptions } from "../common.js"; import { CommonSourceConfig, CommonSourceData, CommonSourceOptions } from "./index.js"; -export interface YTMusicCredentials { - /** - * The cookie retrieved from the Request Headers of music.youtube.com after logging in. - * - * See https://github.com/nickp10/youtube-music-ts-api/blob/master/DOCUMENTATION.md#authenticate and https://ytmusicapi.readthedocs.io/en/latest/setup.html#copy-authentication-headers for how to retrieve this value. - * - * @examples ["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."] - * */ - cookie: string - /** - * If the 'X-Goog-AuthUser' header is present in the Request Headers for music.youtube.com it must also be included - * - * @example [0] - * */ - authUser?: number | string -} - -export interface YTMusicData extends YTMusicCredentials, CommonSourceData, PollingOptions { +export interface YTMusicData extends CommonSourceData, PollingOptions { } export interface YTMusicSourceConfig extends CommonSourceConfig { - data: YTMusicData + data?: YTMusicData options?: CommonSourceOptions & { /** - * When true MS will log to DEBUG what parts of the cookie are updated by YTM + * When true MS will log to DEBUG all of the credentials data it receives from YTM * */ - logAuthUpdateChanges?: boolean + logAuth?: boolean + /** + * Always log history diff + * + * By default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found) + * Set this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN` + * + * @default false + */ + logDiff?: boolean } } diff --git a/src/backend/server/api.ts b/src/backend/server/api.ts index af3b2923..b13ff285 100644 --- a/src/backend/server/api.ts +++ b/src/backend/server/api.ts @@ -191,7 +191,8 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, appLoggerStream: authed, players: 'players' in x ? (x as MemorySource).playersToObject() : {}, sot: ('playerSourceOfTruth' in x) ? x.playerSourceOfTruth : SOURCE_SOT.HISTORY, - supportsUpstreamRecentlyPlayed: x.supportsUpstreamRecentlyPlayed + supportsUpstreamRecentlyPlayed: x.supportsUpstreamRecentlyPlayed, + ...x.additionalApiData() }; if(!x.isReady()) { if(x.buildOK === false) { diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index 10898a1d..dc41fa0c 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -7,6 +7,7 @@ import ScrobbleClients from "../scrobblers/ScrobbleClients.js"; import LastfmSource from "../sources/LastfmSource.js"; import ScrobbleSources from "../sources/ScrobbleSources.js"; import SpotifySource from "../sources/SpotifySource.js"; +import YTMusicSource from "../sources/YTMusicSource.js"; export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMiddle: ExpressHandler, clientMiddle: ExpressHandler, scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => { app.use('/api/client/auth', clientMiddle); @@ -49,6 +50,10 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid // @ts-expect-error TS(2339): Property 'deezerSource' does not exist on type 'Se... Remove this comment to see the full error message req.session.deezerSource = name; return passport.authenticate(`deezer-${source.name}`)(req,res,next); + case 'ytmusic': + await (source as YTMusicSource).reauthenticate(); + res.redirect((source as YTMusicSource).verificationUrl); + break; default: return res.status(400).send(`Specified source does not have auth implemented (${source.type})`); } diff --git a/src/backend/sources/YTMusicSource.ts b/src/backend/sources/YTMusicSource.ts index 621c8ce1..e137c42a 100644 --- a/src/backend/sources/YTMusicSource.ts +++ b/src/backend/sources/YTMusicSource.ts @@ -1,107 +1,194 @@ import dayjs from "dayjs"; import EventEmitter from "events"; -import YouTubeMusic from "youtube-music-ts-api"; -import { IYouTubeMusicAuthenticated } from "youtube-music-ts-api/interfaces-primary"; -import { IPlaylistDetail, ITrackDetail } from "youtube-music-ts-api/interfaces-supplementary"; import { PlayObject } from "../../core/Atomic.js"; import { FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js"; -import { YTMusicCredentials, YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmusic.js"; -import { parseDurationFromTimestamp, readJson, writeFile } from "../utils.js"; +import { YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmusic.js"; +import { Innertube, UniversalCache, Parser, YTNodes, ApiResponse, IBrowseResponse } from 'youtubei.js'; +import {resolve} from 'path'; +import { sleep } from "../utils.js"; import { getPlaysDiff, humanReadableDiff, playsAreAddedOnly, + playsAreBumpedOnly, playsAreSortConsistent } from "../utils/PlayComparisonUtils.js"; import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js"; +import { ListDiff } from "@donedeal0/superdiff"; + +export const ytiHistoryResponseToListItems = (res: ApiResponse): YTNodes.MusicResponsiveListItem[] => { + const page = Parser.parseResponse(res.data); + const items = page.contents_memo.getType(YTNodes.MusicResponsiveListItem); + return Array.from(items); +} + +const maybeJsonErrorInfo = (err: Error): object | string | undefined => { + if('info' in err) { + try { + return JSON.parse(err.info as string); + } catch (e) { + return err.info as string; + } + } + return undefined; +} + +const loggedErrorExtra = (err: Error): object | undefined => { + const maybeInfo = maybeJsonErrorInfo(err); + if(maybeInfo === undefined) { + return undefined; + } + if(typeof maybeInfo === 'string') { + return {apiResponse: maybeInfo}; + } + return maybeInfo; +} + +export const ytiHistoryResponseFromShelfToPlays = (res: ApiResponse): PlayObject[] => { + const page = Parser.parseResponse(res.data); + const items: PlayObject[] = []; + const shelves = page.contents_memo.getType(YTNodes.MusicShelf); + shelves.forEach((shelf) => { + shelf.contents.forEach((listItem) => { + items.push(YTMusicSource.formatPlayObj(listItem, {shelf: shelf.title.text})); + }); + }); + return items; +} export default class YTMusicSource extends AbstractSource { - apiInstance?: IYouTubeMusicAuthenticated requiresAuth = true; + requiresAuthInteraction = true; declare config: YTMusicSourceConfig recentlyPlayed: PlayObject[] = []; + yti: Innertube; + userCode?: string; + verificationUrl?: string; + workingCredsPath: string; - currentCreds!: YTMusicCredentials; constructor(name: string, config: YTMusicSourceConfig, internal: InternalConfig, emitter: EventEmitter) { super('ytmusic', name, config, internal, emitter); this.canPoll = true; this.supportsUpstreamRecentlyPlayed = true; - this.workingCredsPath = `${this.configDir}/currentAuth-ytm-${name}.json`; + this.workingCredsPath = resolve(this.configDir, `yti-${this.name}`); } - protected writeCurrentAuth = async (cookie: string, authUser: number) => { - await writeFile(this.workingCredsPath, JSON.stringify({ - cookie, - authUser - })); + public additionalApiData(): Record { + const data: Record = {}; + if(this.userCode !== undefined) { + data.userCode = this.userCode; + } + return data; } protected async doBuildInitData(): Promise { - let creds: YTMusicCredentials; - try { - creds = await readJson(this.workingCredsPath, {throwOnNotFound: false}) as YTMusicCredentials; - if(creds !== undefined) { - this.currentCreds = creds; - return `Read updated credentials from file currentAuth-ytm-${this.name}.json`; + this.yti = await Innertube.create({ + cache: new UniversalCache(true, this.workingCredsPath) + }); + this.yti.session.on('update-credentials', async ({ credentials }) => { + if(this.config.options?.logAuth) { + this.logger.debug(credentials, 'Credentials updated'); + } else { + this.logger.debug('Credentials updated'); } - } catch (e) { - this.logger.warn('Current YTMusic credentials file exists but could not be parsed', { path: this.workingCredsPath }); - } - if(creds === undefined) { - if(this.config.data.cookie === undefined) { - throw new Error('No YTM cookies were found in configuration'); + await this.yti.session.oauth.cacheCredentials(); + }); + this.yti.session.on('auth-pending', async (data) => { + this.userCode = data.user_code; + this.verificationUrl = data.verification_url; + }); + this.yti.session.on('auth-error', async (data) => { + this.logger.error(new Error('YTM Authentication error', {cause: data})); + }); + this.yti.session.on('auth', async ({ credentials }) => { + if(this.config.options?.logAuth) { + this.logger.debug(credentials, 'Auth success'); + } else { + this.logger.debug('Auth success'); } - this.currentCreds = this.config.data; - return 'Read initial credentials from config'; + await this.yti.session.oauth.cacheCredentials(); + this.userCode = undefined; + this.verificationUrl = undefined; + this.authed = true; + }); + return true; + } + + reauthenticate = async () => { + await this.tryStopPolling(); + await this.clearCredentials(); + this.authed = false; + await this.testAuth(); + } + + clearCredentials = async () => { + if(this.yti.session.logged_in) { + await this.yti.session.signOut(); } } doAuthentication = async () => { try { - await this.getRecentlyPlayed(); - return true; - } catch (e) { - if(e.message.includes('Status code: 401')) { - let hint = 'Verify your cookie and authUser are correct.'; - if(this.currentCreds.authUser === undefined) { - hint = `${hint} TIP: 'authUser' is not defined your credentials. If you are using Chrome to retrieve credentials from music.youtube.com make sure the value from the 'X-Goog-AuthUser' is used as 'authUser'.`; + await Promise.race([ + sleep(300), + this.yti.session.signIn() + ]); + if(this.authed === false && this.userCode !== undefined) { + if(this.userCode !== undefined) { + throw new Error(`Sign in with the code '${this.userCode}' using the authentication link on the dashboard or ${this.verificationUrl}`) + } else { + throw new Error('Waited too long for auth response from YTM!'); + } + } + try { + await this.yti.account.getInfo() + } catch (e) { + const info = loggedErrorExtra(e); + if(info !== undefined) { + this.logger.error(info, 'Additional API response details') } - this.logger.error(`Authentication failed with the given credentials. ${hint} | Error => ${e.message}`); + throw new Error('Credentials exist but API calls are failing. Try re-authenticating?', {cause: e}); } + return true; + } catch (e) { throw e; } } - static formatPlayObj(obj: ITrackDetail, options: FormatPlayObjectOptions = {}): PlayObject { - const {newFromSource = false} = options; + static formatPlayObj(obj: YTNodes.MusicResponsiveListItem, options: FormatPlayObjectOptions = {}): PlayObject { + const {newFromSource = false, shelf = undefined} = options; const { id, title, album: albumData, artists: artistsData, - duration: durTimestamp, // string timestamp + authors: authorData, + duration: dur, // string timestamp } = obj; - let artists = undefined, + let artists = [], album = undefined, duration = undefined; if(artistsData !== undefined) { artists = artistsData.map(x => x.name) as string[]; + } else if(authorData !== undefined) { + artists = authorData.map(x => x.name) as string[]; } + let albumArtists: string[] = []; + if(artistsData !== undefined && authorData !== undefined) { + albumArtists = authorData.map(x => x.name) as string[]; + } if(albumData !== undefined) { album = albumData.name; - if(albumData.artist !== undefined) { - albumArtists = [albumData.artist.name]; - } } - if(durTimestamp !== undefined) { - const durObj = parseDurationFromTimestamp(durTimestamp); + if(dur!== undefined) { + const durObj = dayjs.duration(dur.seconds, 's') duration = durObj.asSeconds(); } return { @@ -118,57 +205,14 @@ export default class YTMusicSource extends AbstractSource { source: 'YTMusic', trackId: id, newFromSource, + comment: shelf } } } recentlyPlayedTrackIsValid = (playObj: PlayObject) => playObj.meta.newFromSource - protected onAuthUpdate = (cookieStr: string, authUser: number, updated: Map) => { - const { - options: { - logAuthUpdateChanges = false - } = {} - } = this.config; - - if(logAuthUpdateChanges) { - const parts: string[] = []; - if(authUser !== this.currentCreds.authUser) { - parts.push(`X-Goog-Authuser: ${authUser}`); - } - for(const [k,v] of updated) { - parts.push(`Cookie ${k}: Old => ${v.old} | New => ${v.new}`); - } - this.logger.info(`Updated Auth -->\n${parts.join('\n')}`); - } else { - this.logger.verbose(`Updated Auth`); - } - - this.currentCreds = { - cookie: cookieStr, - authUser - }; - - - this.writeCurrentAuth(cookieStr, authUser).then(() => {}); - } - - api = async (): Promise => { - if(this.apiInstance !== undefined) { - return this.apiInstance; - } - // @ts-expect-error default does exist - const ytm = new YouTubeMusic.default() as YouTubeMusic; - try { - this.apiInstance = await ytm.authenticate(this.currentCreds.cookie, typeof this.config.data.authUser === 'string' ? Number.parseInt(this.config.data.authUser) : this.config.data.authUser, this.onAuthUpdate); - } catch (e: any) { - this.logger.error('Failed to authenticate', e); - throw e; - } - return this.apiInstance; - } - - protected getLibraryHistory = async (): Promise => { + protected getLibraryHistory = async (): Promise => { // internally for this call YT returns a *list* of playlists with decreasing granularity from most recent to least recent like this: // * Today // * Yesterday @@ -177,14 +221,17 @@ export default class YTMusicSource extends AbstractSource { // // the playlist returned can therefore change abruptly IE MS started yesterday and new music listened to today -> "today" playlist is cleared try { - const playlist = await (await this.api()).getLibraryHistory(); - return {tracks: [], ...playlist}; + const res = await this.yti.actions.execute('/browse', { + browse_id: 'FEmusic_history', + client: 'YTMUSIC' + }); + return res; } catch (e) { throw e; } } - protected getLibraryHistoryPlaylists = async (): Promise => { + protected getLibraryHistoryPlaylists = async (): Promise => { // internally for this call YT returns a *list* of playlists with decreasing granularity from most recent to least recent like this: // * Today // * Yesterday @@ -193,26 +240,19 @@ export default class YTMusicSource extends AbstractSource { // // the playlist returned can therefore change abruptly IE MS started yesterday and new music listened to today -> "today" playlist is cleared try { - return await (await this.api()).getLibraryHistory(true) as IPlaylistDetail[]; + const res = await this.getLibraryHistory(); + return ytiHistoryResponseFromShelfToPlays(res); } catch (e) { + const info = loggedErrorExtra(e); + if(info !== undefined) { + this.logger.error(info, 'Additional API response details') + } throw e; } } getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise => { - let playlists: IPlaylistDetail[]; - try { - playlists = await this.getLibraryHistoryPlaylists() - } catch (e) { - throw e; - } - const playlistAwareTracks: PlayObject[][] = []; - - for(const playlist of playlists) { - playlistAwareTracks.push(playlist.tracks.map((x) => YTMusicSource.formatPlayObj(x, {newFromSource: false})).map((x) => ({...x,meta: {...x.meta, comment: playlist.name}}))) - } - - return playlistAwareTracks.flat(1).slice(0, 100); + return (await this.getLibraryHistoryPlaylists()).slice(0, 100); } /** @@ -221,7 +261,7 @@ export default class YTMusicSource extends AbstractSource { getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => { const { display = false } = options; - let playlistDetail: IPlaylistDetail; + let playlistDetail: ApiResponse; try { playlistDetail = await this.getLibraryHistory(); } catch (e) { @@ -230,7 +270,10 @@ export default class YTMusicSource extends AbstractSource { let newPlays: PlayObject[] = []; - const plays = playlistDetail.tracks.map((x) => YTMusicSource.formatPlayObj(x, {newFromSource: false})).slice(0, 20); + const page = Parser.parseResponse(playlistDetail.data); + const shelfPlays = ytiHistoryResponseFromShelfToPlays(playlistDetail); + const listPlays = ytiHistoryResponseToListItems(playlistDetail).map((x) => YTMusicSource.formatPlayObj(x, {newFromSource: false})); + const plays = listPlays.slice(0, 20); if(this.polling === false) { this.recentlyPlayed = plays; newPlays = plays; @@ -238,31 +281,45 @@ export default class YTMusicSource extends AbstractSource { if(playsAreSortConsistent(this.recentlyPlayed, plays)) { return newPlays; } - const [ok, diff, addType] = playsAreAddedOnly(this.recentlyPlayed, plays); - if(!ok || addType === 'insert' || addType === 'append') { + + let warnMsg: string; + const bumpResults = playsAreBumpedOnly(this.recentlyPlayed, plays); + if(bumpResults[0] === true) { + newPlays = bumpResults[1]; + } else { + const addResults = playsAreAddedOnly(this.recentlyPlayed, plays); + if(addResults[0] === true) { + newPlays = [...addResults[1]].reverse(); + } else { + warnMsg = 'YTM History returned temporally inconsistent order, resetting watched history to new list.'; + } + } + + if(warnMsg !== undefined || (newPlays.length > 0 && this.config.options?.logDiff === true)) { const playsDiff = getPlaysDiff(this.recentlyPlayed, plays) const humanDiff = humanReadableDiff(this.recentlyPlayed, plays, playsDiff); - this.logger.warn('YTM History returned temporally inconsistent order, resetting watched history to new list.'); - this.logger.warn(`Changes from last seen list: -${humanDiff}`); - this.recentlyPlayed = plays; - return newPlays; - } else { - // new plays - newPlays = [...diff].reverse(); - this.recentlyPlayed = plays; + const diffMsg = `Changes from last seen list: + ${humanDiff}`; + if(warnMsg !== undefined) { + this.logger.warn(warnMsg); + this.logger.warn(diffMsg); + } else { + this.logger.debug(diffMsg); + } + } + + this.recentlyPlayed = plays; - newPlays = newPlays.map((x) => ({ + newPlays = newPlays.map((x, index) => ({ data: { ...x.data, - playDate: dayjs().startOf('minute') + playDate: dayjs().startOf('minute').add(index + 1, 's') }, meta: { ...x.meta, newFromSource: true } })); - } } return newPlays; diff --git a/src/backend/tests/utils/playComparisons.test.ts b/src/backend/tests/utils/playComparisons.test.ts index f6cfc869..25879fbe 100644 --- a/src/backend/tests/utils/playComparisons.test.ts +++ b/src/backend/tests/utils/playComparisons.test.ts @@ -2,8 +2,9 @@ import { loggerTest } from "@foxxmd/logging"; import { assert } from 'chai'; import clone from "clone"; import { describe, it } from 'mocha'; -import { playsAreAddedOnly, playsAreSortConsistent } from "../../utils/PlayComparisonUtils.js"; +import { playsAreAddedOnly, playsAreBumpedOnly, playsAreSortConsistent } from "../../utils/PlayComparisonUtils.js"; import { generatePlay, generatePlays } from "./PlayTestUtils.js"; +import { PlayObject } from "../../../core/Atomic.js"; const logger = loggerTest; @@ -33,69 +34,219 @@ describe('Compare lists by order', function () { }); }); - describe('Added Only', function () { + describe('Non-identical lists', function() { + let candidateList: PlayObject[]; - it('Non-identical lists are not add only', function () { - const [ok, diff, addType] = playsAreAddedOnly(existingList, generatePlays(10)) + before(function() { + candidateList = generatePlays(10); + }); + + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + + it('are not bump only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) assert.isFalse(ok); }); + }); - it('Lists with only prepended additions are detected', function () { - const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...existingList]) + describe('Lists with only prepended additions', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [generatePlay(), generatePlay(), ...existingList]; + }); + + it('are add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) assert.isTrue(ok); assert.equal(addType, 'prepend'); }); - it('Lists with only appended additions are detected', function () { - const [ok, diff, addType] = playsAreAddedOnly(existingList, [...existingList, generatePlay(), generatePlay()]) + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + }); + + describe('Lists with only appended additions', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList, generatePlay(), generatePlay()]; + }); + + it('are add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) assert.isTrue(ok); assert.equal(addType, 'append'); }); - it('Lists of fixed length with prepends are correctly detected', function () { - const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...existingList].slice(0, 9)) + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + + }); + + describe('Lists of fixed length with prepends', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [generatePlay(), generatePlay(), ...existingList].slice(0, 9); + }); + + it('are add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) assert.isTrue(ok); assert.equal(addType, 'prepend'); }); - it('Lists with inserts are detected', function () { - const splicedList1 = [...existingList.map(x => clone(x))]; - splicedList1.splice(4, 0, generatePlay()) - const [ok, diff, addType] = playsAreAddedOnly(existingList, splicedList1) + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + }); + + + describe('Lists with inserts', function() { + let candidateList: PlayObject[], + candidateList2: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + candidateList.splice(4, 0, generatePlay()) + + candidateList2 = [...existingList.map(x => clone(x))]; + candidateList2.splice(2, 0, generatePlay()) + candidateList2.splice(6, 0, generatePlay()) + }); + + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) + assert.isFalse(ok) + + const [ok2, diff2, addType2] = playsAreAddedOnly(existingList, candidateList2) + assert.isFalse(ok2) + }); + + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) assert.isFalse(ok) - //assert.equal(addType, 'insert'); - const splicedList2 = [...existingList.map(x => clone(x))]; - splicedList2.splice(2, 0, generatePlay()) - splicedList2.splice(6, 0, generatePlay()) - const [ok2, diff2, addType2] = playsAreAddedOnly(existingList, splicedList2) + const [ok2, diff2, addType2] = playsAreBumpedOnly(existingList, candidateList2) assert.isFalse(ok2) - //assert.equal(addType2, 'insert'); + }); + + }); + + describe('Lists with inserts and prepends', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + candidateList.splice(2, 0, generatePlay()) + candidateList.splice(6, 0, generatePlay()) + candidateList = [generatePlay(), generatePlay(), ...candidateList] }); - it('Lists with inserts and prepends are detected as inserts', function () { - const splicedList = [...existingList.map(x => clone(x))]; - splicedList.splice(2, 0, generatePlay()) - splicedList.splice(6, 0, generatePlay()) - const [ok, diff3, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...splicedList]) + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) assert.isFalse(ok); - //assert.equal(addType, 'insert'); }); - it('Lists with inserts and appends are detected as inserts', function () { - const splicedList = [...existingList.map(x => clone(x))]; - splicedList.splice(2, 0, generatePlay()) - splicedList.splice(6, 0, generatePlay()) - const [ok, diff4, addType] = playsAreAddedOnly(existingList, [...splicedList, generatePlay(), generatePlay()]) + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) assert.isFalse(ok); - //assert.equal(addType, 'insert'); + }); + + }); + + describe('Lists with inserts and appends', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + candidateList.splice(2, 0, generatePlay()) + candidateList.splice(6, 0, generatePlay()) + candidateList = [...candidateList, generatePlay(), generatePlay()] + }); + + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + }); + + describe('Lists with inserts and appends and prepends', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + candidateList = [generatePlay(), generatePlay(), ...candidateList, generatePlay(), generatePlay()] + }); + + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + + it('are not bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList) + assert.isFalse(ok); + }); + }); + + describe('Lists with plays bumped-by-prepend', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + const bumped = candidateList[6]; + candidateList.splice(6, 1); + candidateList.unshift(bumped); + }); + + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList); + assert.isFalse(ok); + }); + + it('are bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList); + assert.isTrue(ok); + assert.equal(addType, 'prepend'); + }); + + }); + + describe('Lists with plays bumped-by-append', function() { + let candidateList: PlayObject[]; + + before(function() { + candidateList = [...existingList.map(x => clone(x))]; + const bumped = candidateList[6]; + candidateList.splice(6, 1); + candidateList.push(bumped); }); - it('Lists with inserts and appends and prepends are detected as inserts', function () { - const splicedList = [...existingList.map(x => clone(x))]; - const [ok, diff, addType] = playsAreAddedOnly(existingList, [generatePlay(), generatePlay(), ...splicedList, generatePlay(), generatePlay()]) + it('are not add only', function () { + const [ok, diff, addType] = playsAreAddedOnly(existingList, candidateList); assert.isFalse(ok); - //assert.equal(addType, 'insert'); }); - }) + + it('are bump only', function () { + const [ok, diff, addType] = playsAreBumpedOnly(existingList, candidateList); + assert.isTrue(ok); + assert.equal(addType, 'append'); + }); + + }); }); diff --git a/src/backend/tests/ytm/ytm.test.ts b/src/backend/tests/ytm/ytm.test.ts new file mode 100644 index 00000000..9ea14d1f --- /dev/null +++ b/src/backend/tests/ytm/ytm.test.ts @@ -0,0 +1,21 @@ +import { after, before, describe, it } from 'mocha'; +import chai, { assert, expect } from 'chai'; +import asPromised from 'chai-as-promised'; +import { Innertube, UniversalCache, Parser, YTNodes, IBrowseResponse } from 'youtubei.js'; +import { ytiHistoryResponseFromShelfToPlays, ytiHistoryResponseToListItems } from "../../sources/YTMusicSource.js"; +import ytHistoryRes from './ytres.json'; + +chai.use(asPromised); + +describe('Parses History', function () { + + it(`Parses a history response to tracks`, async function () { + const items = ytiHistoryResponseToListItems(ytHistoryRes); + expect(items).length(10); + }); + + it(`Parses a history response plays with shelf name`, async function () { + const items = ytiHistoryResponseFromShelfToPlays(ytHistoryRes); + expect(items[0]?.meta?.comment).to.eq('March 2023'); + }); +}); diff --git a/src/backend/tests/ytm/ytres.json b/src/backend/tests/ytm/ytres.json new file mode 100644 index 00000000..3d1187f8 --- /dev/null +++ b/src/backend/tests/ytm/ytres.json @@ -0,0 +1,5480 @@ +{ + "success": true, + "status_code": 200, + "data": { + "responseContext": { + "serviceTrackingParams": [ + { + "service": "GFEEDBACK", + "params": [ + { + "key": "browse_id", + "value": "FEmusic_history" + }, + { + "key": "browse_id_prefix", + "value": "" + }, + { + "key": "logged_in", + "value": "1" + }, + { + "key": "e", + "value": "9406004,23804281,23966208,24004644,24077241,24181174,24241378,24439361,24459435,24542367,24548629,24566687,51009781,51010235,51017346,51020570,51025415,51041512,51050361,51053689,51063643,51064835,51065188,51089007,51098297,51098299,51111738,51115184,51117319,51124104,51125020,51133103,51152050,51157411,51157841,51157895,51158514,51160545,51162170,51165467,51169118,51176511,51177817,51178982,51183909,51186528,51190652,51195231,51204329,51217504,51221011,51223962,51224135,51225967,51227037,51228350,51230241,51230478,51231814,51237842,51239093,51241028,51242447,51243940,51248255,51248734,51251836,51255676,51255680,51255743,51256084,51258066,51266454,51267568,51268362,51275782,51276557,51276565,51276640,51281227,51284653,51286051,51287196,51287500,51289938,51295132,51295408,51296439,51298018,51298829,51299710,51299724,51300414,51300530,51300699,51300760,51302359,51302492,51302680,51303666,51303667,51303669,51303790,51304121,51304155,51305840,51305952,51307723,51309313,51310323,51312153,51313148" + } + ] + }, + { + "service": "CSI", + "params": [ + { + "key": "c", + "value": "WEB_REMIX" + }, + { + "key": "cver", + "value": "1.20211213.00.00" + }, + { + "key": "yt_li", + "value": "1" + }, + { + "key": "GetBrowseHistoryPage_rid", + "value": "0xdaafccf3094c8a75" + } + ] + }, + { + "service": "ECATCHER", + "params": [ + { + "key": "client.version", + "value": "1.20000101" + }, + { + "key": "client.name", + "value": "WEB_REMIX" + } + ] + } + ], + "maxAgeSeconds": 0 + }, + "contents": { + "singleColumnBrowseResultsRenderer": { + "tabs": [ + { + "tabRenderer": { + "title": "Recently played", + "selected": true, + "content": { + "sectionListRenderer": { + "contents": [ + { + "musicShelfRenderer": { + "title": { + "runs": [ + { + "text": "March 2023" + } + ] + }, + "contents": [ + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CJoBEMn0AhgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/lK5HVlcs0og/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3n6IBQoqXjxzwft7oN7A06fHgAzLQ", + "width": 400, + "height": 225 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CKcBEIS_AiITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CKYBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "watchEndpoint": { + "videoId": "lK5HVlcs0og", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CKYBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Oil - Gorillaz" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Oil - Gorillaz" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Oil (feat. Stevie Nicks)", + "navigationEndpoint": { + "clickTrackingParams": "CJoBEMn0AhgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "lK5HVlcs0og", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Gorillaz", + "navigationEndpoint": { + "clickTrackingParams": "CJoBEMn0AhgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "UCNIV5B_aJnLrKDSnW_MOmcQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + }, + { + "text": " • " + }, + { + "text": "2.1M views" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": {}, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:51" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CKUBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "lK5HVlcs0og", + "playlistId": "RDAMVMlK5HVlcs0og", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1sSzVIVmxjczBvZw%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CKUBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CKMBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "lK5HVlcs0og", + "onEmptyQueue": { + "clickTrackingParams": "CKMBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "lK5HVlcs0og" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CKMBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CKQBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CKMBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CKEBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "lK5HVlcs0og", + "onEmptyQueue": { + "clickTrackingParams": "CKEBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "lK5HVlcs0og" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CKEBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CKIBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CKEBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CKABEMOUBhgDIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToPlaylistEndpoint": { + "videoId": "lK5HVlcs0og" + } + }, + "trackingParams": "CKABEMOUBhgDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJ8BEJD7BRgEIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "UCNIV5B_aJnLrKDSnW_MOmcQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CJ8BEJD7BRgEIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJ4BEJH7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtsSzVIVmxjczBvZw%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CJ4BEJH7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CJsBEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpK44OPe9xRf5FyjrM14f9l0jy-shAx9lcuoSa1AXxSX8teMWvyCmPZhJp1CLrGGTSF-CAe7NG2FwuuFHCKpzdP9aWH6PA", + "actions": [ + { + "clickTrackingParams": "CJsBEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CJsBEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CJ0BEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CJsBEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + ], + "trackingParams": "CJsBEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "lK5HVlcs0og" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CJwBEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CJwBEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "lK5HVlcs0og" + } + } + }, + { + "clickTrackingParams": "CJwBEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "lK5HVlcs0og" + } + } + }, + { + "clickTrackingParams": "CJwBEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "lK5HVlcs0og" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "lK5HVlcs0og" + } + } + }, + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CIwBEMn0AhgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/S03T47hapAc/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3mpyeV7fqoxVkHya4TPBGkZvJpuBg", + "width": 400, + "height": 225 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CJkBEIS_AiITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CJgBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "watchEndpoint": { + "videoId": "S03T47hapAc", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CJgBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Cracker Island - Gorillaz" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Cracker Island - Gorillaz" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Cracker Island (feat. Thundercat)", + "navigationEndpoint": { + "clickTrackingParams": "CIwBEMn0AhgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "S03T47hapAc", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Gorillaz", + "navigationEndpoint": { + "clickTrackingParams": "CIwBEMn0AhgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "UCNIV5B_aJnLrKDSnW_MOmcQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + }, + { + "text": " • " + }, + { + "text": "47M views" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": {}, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:39" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJcBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "S03T47hapAc", + "playlistId": "RDAMVMS03T47hapAc", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1TMDNUNDdoYXBBYw%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CJcBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CJUBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "S03T47hapAc", + "onEmptyQueue": { + "clickTrackingParams": "CJUBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "S03T47hapAc" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CJUBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CJYBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CJUBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CJMBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "S03T47hapAc", + "onEmptyQueue": { + "clickTrackingParams": "CJMBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "S03T47hapAc" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CJMBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CJQBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CJMBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJIBEMOUBhgDIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToPlaylistEndpoint": { + "videoId": "S03T47hapAc" + } + }, + "trackingParams": "CJIBEMOUBhgDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJEBEJD7BRgEIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "UCNIV5B_aJnLrKDSnW_MOmcQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CJEBEJD7BRgEIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CJABEJH7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtTMDNUNDdoYXBBYw%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CJABEJH7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CI0BEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJEZGr5oUyd1nMJS28h8PuaYqLd1991E-0MR78jcHoAq5vXzOB7bqAi4IXaPoVOPketJdtJCMvn89R8j4Xyb3etB-xKSQ", + "actions": [ + { + "clickTrackingParams": "CI0BEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CI0BEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CI8BEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CI0BEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + ], + "trackingParams": "CI0BEKc7IhMIt_TqieGBiQMVxdVyCR1QITDt", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "S03T47hapAc" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CI4BEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CI4BEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "S03T47hapAc" + } + } + }, + { + "clickTrackingParams": "CI4BEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "S03T47hapAc" + } + } + }, + { + "clickTrackingParams": "CI4BEKVBGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "S03T47hapAc" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "S03T47hapAc" + } + } + } + ], + "trackingParams": "CIsBEPleGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shelfDivider": { + "musicShelfDividerRenderer": { + "hidden": true + } + } + } + }, + { + "musicShelfRenderer": { + "title": { + "runs": [ + { + "text": "February 2023" + } + ] + }, + "contents": [ + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CHsQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/aZlTfoNytQ8aa6nJkrN3GclMwvPpTWoWNjaIhH4k1IZ_FB1olspeJWZELsABC032Gcm1Vq7VWUx7x2gXug=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/aZlTfoNytQ8aa6nJkrN3GclMwvPpTWoWNjaIhH4k1IZ_FB1olspeJWZELsABC032Gcm1Vq7VWUx7x2gXug=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CIoBEIS_AiITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CIkBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "watchEndpoint": { + "videoId": "LlAhFxf-BV0", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CIkBEMjeAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Gourmet Race (From \"Kirby Superstar\") - The 8-Bit Big Band" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Gourmet Race (From \"Kirby Superstar\") - The 8-Bit Big Band" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Gourmet Race (From \"Kirby Superstar\") (feat. Sam Dillon)", + "navigationEndpoint": { + "clickTrackingParams": "CHsQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "LlAhFxf-BV0", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "The 8-Bit Big Band", + "navigationEndpoint": { + "clickTrackingParams": "CHsQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Backwards Compatible", + "navigationEndpoint": { + "clickTrackingParams": "CHsQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_QwI9tAIYSEf", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:35" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CIgBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "LlAhFxf-BV0", + "playlistId": "RDAMVMLlAhFxf-BV0", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1MbEFoRnhmLUJWMA%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CIgBEJvzBRgAIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CIYBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "LlAhFxf-BV0", + "onEmptyQueue": { + "clickTrackingParams": "CIYBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "LlAhFxf-BV0" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CIYBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CIcBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CIYBEL7uBRgBIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CIQBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "LlAhFxf-BV0", + "onEmptyQueue": { + "clickTrackingParams": "CIQBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "LlAhFxf-BV0" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CIQBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CIUBEMrHAyITCLf06onhgYkDFcXVcgkdUCEw7Q==" + } + } + } + } + ] + } + }, + "trackingParams": "CIQBEPvvBRgCIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CIMBEIT_BRgDIhMIt_TqieGBiQMVxdVyCR1QITDt", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLE7NQR22Wj326Z7Wfc8g1V-OT7QenQFJR82j_TpPlWdLnw9IgVKXsv1t3rJJB6fkfMIaBf_Q3joFZn1UZlsL8nzyXnFg" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CIMBEIT_BRgDIhMIt_TqieGBiQMVxdVyCR1QITDt", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpK6N-46lmyH--aqaRbTL0De2JmFIYc8m5GQMnI5kIjpCElMko97vSDSlXqBMo5rJShJVGEFJNu1-xisheQ455C_DcV2Uw" + } + }, + "trackingParams": "CIMBEIT_BRgDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CIIBEMOUBhgEIhMIt_TqieGBiQMVxdVyCR1QITDt", + "addToPlaylistEndpoint": { + "videoId": "LlAhFxf-BV0" + } + }, + "trackingParams": "CIIBEMOUBhgEIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CIEBEI_7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "MPREb_QwI9tAIYSEf", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CIEBEI_7BRgFIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CIABEJD7BRgGIhMIt_TqieGBiQMVxdVyCR1QITDt", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CIABEJD7BRgGIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CH8QkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtMbEFoRnhmLUJWMA%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CH8QkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CHwQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpL2De8IUZhbv7DfcRX76elE4nbKHRdVVrf1PwRPlaJrakFmOzxj-qllXWTWRvrAEMi9fn04rzYAJbzDEW3wm5FXlyCvqw", + "actions": [ + { + "clickTrackingParams": "CHwQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CHwQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CH4QyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CHwQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CHwQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "LlAhFxf-BV0" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CH0QpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CH0QpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "LlAhFxf-BV0" + }, + "actions": [ + { + "clickTrackingParams": "CH0QpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpLE7NQR22Wj326Z7Wfc8g1V-OT7QenQFJR82j_TpPlWdLnw9IgVKXsv1t3rJJB6fkfMIaBf_Q3joFZn1UZlsL8nzyXnFg" + } + } + ] + } + }, + { + "clickTrackingParams": "CH0QpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "LlAhFxf-BV0" + } + } + }, + { + "clickTrackingParams": "CH0QpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "LlAhFxf-BV0" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "LlAhFxf-BV0" + } + } + }, + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CGoQyfQCGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/TyYSkfW7KK5oCV0rO0_NL9TZyhre2KffMonya2ll6L5xsARE1MYCMxKwIICNzDk9Cxd_L9Kxw3c8SmjjBw=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/TyYSkfW7KK5oCV0rO0_NL9TZyhre2KffMonya2ll6L5xsARE1MYCMxKwIICNzDk9Cxd_L9Kxw3c8SmjjBw=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CHoQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CHkQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "UFFa0QoHWvE", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CHkQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Tank! - Seatbelts" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Tank! - Seatbelts" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Tank!", + "navigationEndpoint": { + "clickTrackingParams": "CGoQyfQCGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "UFFa0QoHWvE", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Seatbelts", + "navigationEndpoint": { + "clickTrackingParams": "CGoQyfQCGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCFhMA8ygjzH72UqPf7PmlUg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "COWBOY BEBOP (Original Motion Picture Soundtrack)", + "navigationEndpoint": { + "clickTrackingParams": "CGoQyfQCGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_asHLU4Jr7nQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:31" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CHgQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "UFFa0QoHWvE", + "playlistId": "RDAMVMUFFa0QoHWvE", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1VRkZhMFFvSFd2RQ%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CHgQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CHYQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "UFFa0QoHWvE", + "onEmptyQueue": { + "clickTrackingParams": "CHYQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "UFFa0QoHWvE" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CHYQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CHcQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CHYQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CHQQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "UFFa0QoHWvE", + "onEmptyQueue": { + "clickTrackingParams": "CHQQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "UFFa0QoHWvE" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CHQQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CHUQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CHQQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CHMQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLZbVGiTSImuqmPNeXJmm2W-Y1TyQzRvdo5NpgVtyiRGffScEIAt5IJNuo7JMCJIgZM3tYogOBrBw9xXYvD7AdX3-U-Ww" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CHMQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLAw74GT5zUOhXXJYMV1uZjXKUMqOfqXWa11MVrTMeqJvKd5zMQuEExBq9spm18mvDFBo8KUzK3Xfo5ICvjwuMWRRLQXw" + } + }, + "trackingParams": "CHMQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CHIQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "UFFa0QoHWvE" + } + }, + "trackingParams": "CHIQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CHEQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_asHLU4Jr7nQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CHEQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CHAQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCFhMA8ygjzH72UqPf7PmlUg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CHAQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "View song credits" + } + ] + }, + "icon": { + "iconType": "PEOPLE_GROUP" + }, + "navigationEndpoint": { + "clickTrackingParams": "CG8Qr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPTCUFFa0QoHWvE", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_TRACK_CREDITS" + } + } + } + }, + "trackingParams": "CG8Qr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CG4QkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtVRkZhMFFvSFd2RQ%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CG4QkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CGsQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpIvqzNn3JiRi5Ie3KsGTM3Ls0y1L9FYXSd7L2D4NEOlMbbTKry-AkWQLZSVAfFFFiUt4vlFjagf3vuq8YeLmeyaENEnsg", + "actions": [ + { + "clickTrackingParams": "CGsQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CGsQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CG0QyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CGsQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CGsQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "UFFa0QoHWvE" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CGwQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CGwQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "UFFa0QoHWvE" + }, + "actions": [ + { + "clickTrackingParams": "CGwQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpLZbVGiTSImuqmPNeXJmm2W-Y1TyQzRvdo5NpgVtyiRGffScEIAt5IJNuo7JMCJIgZM3tYogOBrBw9xXYvD7AdX3-U-Ww" + } + } + ] + } + }, + { + "clickTrackingParams": "CGwQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "UFFa0QoHWvE" + } + } + }, + { + "clickTrackingParams": "CGwQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "UFFa0QoHWvE" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "UFFa0QoHWvE" + } + } + }, + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CFgQyfQCGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/QgFG5RHpesQcNq4JDY51ot0i2GRRf7GFpiO49zyAVO2C5YyKxCTglY0A9c4zBCHWkLrtg7mi-0UY5Uc=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/QgFG5RHpesQcNq4JDY51ot0i2GRRf7GFpiO49zyAVO2C5YyKxCTglY0A9c4zBCHWkLrtg7mi-0UY5Uc=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CGkQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CGgQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "Cg90gxYZ1C0", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CGgQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Dang! - Mac Miller" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Dang! - Mac Miller" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Dang! (feat. Anderson .Paak)", + "navigationEndpoint": { + "clickTrackingParams": "CFgQyfQCGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Cg90gxYZ1C0", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Mac Miller", + "navigationEndpoint": { + "clickTrackingParams": "CFgQyfQCGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC52ZqHVQz5OoGhvbWiRal6g", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "The Divine Feminine", + "navigationEndpoint": { + "clickTrackingParams": "CFgQyfQCGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_kCZ6WUs8Rg3", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "5:06" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CGcQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Cg90gxYZ1C0", + "playlistId": "RDAMVMCg90gxYZ1C0", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1DZzkwZ3hZWjFDMA%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CGcQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CGUQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "Cg90gxYZ1C0", + "onEmptyQueue": { + "clickTrackingParams": "CGUQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Cg90gxYZ1C0" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CGUQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CGYQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CGUQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CGMQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "Cg90gxYZ1C0", + "onEmptyQueue": { + "clickTrackingParams": "CGMQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Cg90gxYZ1C0" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CGMQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CGQQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CGMQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_SAVED" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CGIQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpKCoTcJoGS7RGWGTxtTbZPe-SIBzyeO79F_h1Wv4Mq1PJK6JQrNoNsg-oJax_Qt-gjVL72Lm3o1A25yTP_kg_xIliwJQw" + } + }, + "toggledText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_ADD" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CGIQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLanq6fYOBQp2MpLP0zOfqAu9vm5oGQoiD5EhyUTSkBowi6IvsQ_vk1Chd-gUnoo1ESxOw4svtrC-i_shCEm1hh_R96ng" + } + }, + "trackingParams": "CGIQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CGEQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "Cg90gxYZ1C0" + } + }, + "trackingParams": "CGEQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CGAQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_kCZ6WUs8Rg3", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CGAQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CF8QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC52ZqHVQz5OoGhvbWiRal6g", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CF8QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "View song credits" + } + ] + }, + "icon": { + "iconType": "PEOPLE_GROUP" + }, + "navigationEndpoint": { + "clickTrackingParams": "CF4Qr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPTCCg90gxYZ1C0", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_TRACK_CREDITS" + } + } + } + }, + "trackingParams": "CF4Qr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CF0QkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtDZzkwZ3hZWjFDMA%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CF0QkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CFoQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpKD66WoJdanHu2afRa1D2sUJ2KBQEIQkXYQqw-bVqTwgblrd0Y2Yrk4sakynVmcj8iAV-JKHahNbnRyMIUe_aqsT9wvpA", + "actions": [ + { + "clickTrackingParams": "CFoQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CFoQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CFwQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CFoQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CFoQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "Cg90gxYZ1C0" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CFsQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CFsQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "Cg90gxYZ1C0" + }, + "actions": [ + { + "clickTrackingParams": "CFsQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpLanq6fYOBQp2MpLP0zOfqAu9vm5oGQoiD5EhyUTSkBowi6IvsQ_vk1Chd-gUnoo1ESxOw4svtrC-i_shCEm1hh_R96ng" + } + } + ] + } + }, + { + "clickTrackingParams": "CFsQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "Cg90gxYZ1C0" + } + } + }, + { + "clickTrackingParams": "CFsQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "Cg90gxYZ1C0" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "badges": [ + { + "musicInlineBadgeRenderer": { + "trackingParams": "CFkQoe0CGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "icon": { + "iconType": "MUSIC_EXPLICIT_BADGE" + }, + "accessibilityData": { + "accessibilityData": { + "label": "Explicit" + } + } + } + } + ], + "playlistItemData": { + "videoId": "Cg90gxYZ1C0" + } + } + }, + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CEgQyfQCGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/aZlTfoNytQ8aa6nJkrN3GclMwvPpTWoWNjaIhH4k1IZ_FB1olspeJWZELsABC032Gcm1Vq7VWUx7x2gXug=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/aZlTfoNytQ8aa6nJkrN3GclMwvPpTWoWNjaIhH4k1IZ_FB1olspeJWZELsABC032Gcm1Vq7VWUx7x2gXug=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CFcQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CFYQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "2SqWRLU32BU", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CFYQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Want You Gone (From \"Portal 2\") - The 8-Bit Big Band" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Want You Gone (From \"Portal 2\") - The 8-Bit Big Band" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Want You Gone (From \"Portal 2\") (feat. Benny Benack III)", + "navigationEndpoint": { + "clickTrackingParams": "CEgQyfQCGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "2SqWRLU32BU", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "The 8-Bit Big Band", + "navigationEndpoint": { + "clickTrackingParams": "CEgQyfQCGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Backwards Compatible", + "navigationEndpoint": { + "clickTrackingParams": "CEgQyfQCGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_QwI9tAIYSEf", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:41" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CFUQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "2SqWRLU32BU", + "playlistId": "RDAMVM2SqWRLU32BU", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk0yU3FXUkxVMzJCVQ%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CFUQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CFMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "2SqWRLU32BU", + "onEmptyQueue": { + "clickTrackingParams": "CFMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "2SqWRLU32BU" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CFMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CFQQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CFMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CFEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "2SqWRLU32BU", + "onEmptyQueue": { + "clickTrackingParams": "CFEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "2SqWRLU32BU" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CFEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CFIQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CFEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CFAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLlPWHHrxpeMjCiq0OOYwhvoD09J3NyLMsf-FOjvxjEM4z73aluqVrtTi1caj7mlc7mNzjvfQ8-YHRywKUdAAvMKXcVBQ" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CFAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpI0qa28UYPekFWZkSvvEgFGDH79RQF6vYaxiT62Y_jtsqlCSr7pxiEplJrSjuTtNNaPSdvRZuDCMg4HPzg0x36eOErmKg" + } + }, + "trackingParams": "CFAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CE8Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "2SqWRLU32BU" + } + }, + "trackingParams": "CE8Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CE4Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_QwI9tAIYSEf", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CE4Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CE0QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CE0QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CEwQkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgsyU3FXUkxVMzJCVQ%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CEwQkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CEkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJ8N3kDghKreUTXjJNCA4M3mYI8JJ5Fwubaj0sgNejydirRhHEEp_zLnXMRxuwptDlmLl7whMofuuj7PimO7s2G5Vmk4w", + "actions": [ + { + "clickTrackingParams": "CEkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CEkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CEsQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CEkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CEkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "2SqWRLU32BU" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CEoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CEoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "2SqWRLU32BU" + }, + "actions": [ + { + "clickTrackingParams": "CEoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpLlPWHHrxpeMjCiq0OOYwhvoD09J3NyLMsf-FOjvxjEM4z73aluqVrtTi1caj7mlc7mNzjvfQ8-YHRywKUdAAvMKXcVBQ" + } + } + ] + } + }, + { + "clickTrackingParams": "CEoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "2SqWRLU32BU" + } + } + }, + { + "clickTrackingParams": "CEoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "2SqWRLU32BU" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "2SqWRLU32BU" + } + } + }, + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CDgQyfQCGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/Ks-HeVbqttdGlZO1T78EaBW7IAnh1jqjL7hNrN6FP5if_GnKplO5kPq2LEI826hmEfMwk0_OizVEBCTT=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/Ks-HeVbqttdGlZO1T78EaBW7IAnh1jqjL7hNrN6FP5if_GnKplO5kPq2LEI826hmEfMwk0_OizVEBCTT=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CEcQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CEYQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "oHk4YYGjmHw", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CEYQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Still Alive (From \"Portal\") - The 8-Bit Big Band" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Still Alive (From \"Portal\") - The 8-Bit Big Band" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Still Alive (From \"Portal\")", + "navigationEndpoint": { + "clickTrackingParams": "CDgQyfQCGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "oHk4YYGjmHw", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "The 8-Bit Big Band", + "navigationEndpoint": { + "clickTrackingParams": "CDgQyfQCGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Choose Your Character!", + "navigationEndpoint": { + "clickTrackingParams": "CDgQyfQCGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_c367wl7Zw7K", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:12" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CEUQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "oHk4YYGjmHw", + "playlistId": "RDAMVMoHk4YYGjmHw", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1vSGs0WVlHam1Idw%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CEUQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CEMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "oHk4YYGjmHw", + "onEmptyQueue": { + "clickTrackingParams": "CEMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "oHk4YYGjmHw" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CEMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CEQQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CEMQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CEEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "oHk4YYGjmHw", + "onEmptyQueue": { + "clickTrackingParams": "CEEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "oHk4YYGjmHw" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CEEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CEIQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CEEQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CEAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJSgYpvN9UPyc35j80lfhua3PI4HDiHZhFvW0JYRiviCt8jDSi1Z_IBqrQRi6r6SyOwFF7S_hydAKnEeBB3wKPPHFG4mA" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CEAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpIH8l8P0YOBro7HQXuq8iSR5kmNXF4XB1UTr7uzQUjPGGVmZLy25JtttODE658jDS2HmJWIaKQ0S2mvLHtdC5JnNcdJlw" + } + }, + "trackingParams": "CEAQhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CD8Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "oHk4YYGjmHw" + } + }, + "trackingParams": "CD8Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CD4Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_c367wl7Zw7K", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CD4Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CD0QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UC4gpHwG5SQKJzV2qObgJdJg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CD0QkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CDwQkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtvSGs0WVlHam1Idw%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CDwQkfsFGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CDkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLzTm-h1Kkqs50pWrr3wO8sBRvhrQG-nXtPo5pRhvbuAyvOzayjptqjneqX1K905qkW08-qRS6dnfvRvQHG01WC_QMuiA", + "actions": [ + { + "clickTrackingParams": "CDkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CDkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CDsQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CDkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CDkQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "oHk4YYGjmHw" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CDoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CDoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "oHk4YYGjmHw" + }, + "actions": [ + { + "clickTrackingParams": "CDoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpJSgYpvN9UPyc35j80lfhua3PI4HDiHZhFvW0JYRiviCt8jDSi1Z_IBqrQRi6r6SyOwFF7S_hydAKnEeBB3wKPPHFG4mA" + } + } + ] + } + }, + { + "clickTrackingParams": "CDoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "oHk4YYGjmHw" + } + } + }, + { + "clickTrackingParams": "CDoQpUEYCSITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "oHk4YYGjmHw" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "oHk4YYGjmHw" + } + } + } + ], + "trackingParams": "CDcQ-V4YASITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "shelfDivider": { + "musicShelfDividerRenderer": { + "hidden": true + } + } + } + }, + { + "musicShelfRenderer": { + "title": { + "runs": [ + { + "text": "January 2023" + } + ] + }, + "contents": [ + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CCYQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/2ORsVnMoDz54nNIDYYE14YP5r7C1NQXsIVFgO5IMs6y64rY2u4VRQYsvjpaiXlew0hQf9IFmfq4gw6-u=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/2ORsVnMoDz54nNIDYYE14YP5r7C1NQXsIVFgO5IMs6y64rY2u4VRQYsvjpaiXlew0hQf9IFmfq4gw6-u=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CDYQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CDUQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "bsL1wgy1j-Q", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CDUQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Shadow Stabbing - CAKE" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Shadow Stabbing - CAKE" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Shadow Stabbing", + "navigationEndpoint": { + "clickTrackingParams": "CCYQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "bsL1wgy1j-Q", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "CAKE", + "navigationEndpoint": { + "clickTrackingParams": "CCYQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCPEA0kfpI53U9vmqnc9lJ-A", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Comfort Eagle", + "navigationEndpoint": { + "clickTrackingParams": "CCYQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_sHzavCc6vDo", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:08" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CDQQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "bsL1wgy1j-Q", + "playlistId": "RDAMVMbsL1wgy1j-Q", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1ic0wxd2d5MWotUQ%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CDQQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CDIQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "bsL1wgy1j-Q", + "onEmptyQueue": { + "clickTrackingParams": "CDIQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "bsL1wgy1j-Q" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CDIQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CDMQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CDIQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CDAQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "bsL1wgy1j-Q", + "onEmptyQueue": { + "clickTrackingParams": "CDAQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "bsL1wgy1j-Q" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CDAQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CDEQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CDAQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CC8QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpL_ryR1M99Jzih_0ldsLXcBwa4YGduyKopUYo8Ka7aMdQNQWnj8v3eZIlKFqQHyuXwZtz1vbGuO7yjwir5jA34vnBAagA" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CC8QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJ8B1uAfrhorrrb3LNFyLRKK6pd4PVOAd17lrSURsNhvdD81oVCsQv3C_aNcDZg34A5Xiq5M3OnX1AFXYLmSNk4OiCEyg" + } + }, + "trackingParams": "CC8QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CC4Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "bsL1wgy1j-Q" + } + }, + "trackingParams": "CC4Qw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CC0Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_sHzavCc6vDo", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CC0Qj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CCwQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCPEA0kfpI53U9vmqnc9lJ-A", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CCwQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "View song credits" + } + ] + }, + "icon": { + "iconType": "PEOPLE_GROUP" + }, + "navigationEndpoint": { + "clickTrackingParams": "CCsQr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPTCbsL1wgy1j-Q", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_TRACK_CREDITS" + } + } + } + }, + "trackingParams": "CCsQr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CCoQkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "Cgtic0wxd2d5MWotUQ%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CCoQkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CCcQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpKVzCu4tmZ5slH81PeCcXKhErbHcynfcKk116yMYjUcQGST276b7LBoU3gJl0tAHdC-LPkZOViU_hzzX71pkrp5DQ1XFw", + "actions": [ + { + "clickTrackingParams": "CCcQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CCcQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CCkQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CCcQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CCcQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "bsL1wgy1j-Q" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CCgQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CCgQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "bsL1wgy1j-Q" + }, + "actions": [ + { + "clickTrackingParams": "CCgQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpL_ryR1M99Jzih_0ldsLXcBwa4YGduyKopUYo8Ka7aMdQNQWnj8v3eZIlKFqQHyuXwZtz1vbGuO7yjwir5jA34vnBAagA" + } + } + ] + } + }, + { + "clickTrackingParams": "CCgQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "bsL1wgy1j-Q" + } + } + }, + { + "clickTrackingParams": "CCgQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "bsL1wgy1j-Q" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "bsL1wgy1j-Q" + } + } + } + ], + "trackingParams": "CCUQ-V4YAiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "shelfDivider": { + "musicShelfDividerRenderer": { + "hidden": true + } + } + } + }, + { + "musicShelfRenderer": { + "title": { + "runs": [ + { + "text": "December 2022" + } + ] + }, + "contents": [ + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CBQQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://lh3.googleusercontent.com/kSPVOVNh0C9kfkYFD8UuurMsnCB8r5WKzvxSP68HxM1fZnOs-FetIJlatZpXeTuoTIBFd_jrK-Y8W6wn=w60-h60-l90-rj", + "width": 60, + "height": 60 + }, + { + "url": "https://lh3.googleusercontent.com/kSPVOVNh0C9kfkYFD8UuurMsnCB8r5WKzvxSP68HxM1fZnOs-FetIJlatZpXeTuoTIBFd_jrK-Y8W6wn=w120-h120-l90-rj", + "width": 120, + "height": 120 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CCQQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CCMQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "Bcus42ihkTI", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CCMQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play I'd Rather Go Blind - Etta James" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause I'd Rather Go Blind - Etta James" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "I'd Rather Go Blind", + "navigationEndpoint": { + "clickTrackingParams": "CBQQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Bcus42ihkTI", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Etta James", + "navigationEndpoint": { + "clickTrackingParams": "CBQQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCieyolnCbsWzyEt6wsd7lgQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Tell Mama", + "navigationEndpoint": { + "clickTrackingParams": "CBQQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_zqisM59IhFI", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "2:37" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CCIQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Bcus42ihkTI", + "playlistId": "RDAMVMBcus42ihkTI", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1CY3VzNDJpaGtUSQ%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_ATV" + } + } + } + }, + "trackingParams": "CCIQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CCAQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "Bcus42ihkTI", + "onEmptyQueue": { + "clickTrackingParams": "CCAQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Bcus42ihkTI" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CCAQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CCEQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CCAQvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CB4Q--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "Bcus42ihkTI", + "onEmptyQueue": { + "clickTrackingParams": "CB4Q--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "Bcus42ihkTI" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CB4Q--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CB8QyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CB4Q--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "toggleMenuServiceItemRenderer": { + "defaultText": { + "runs": [ + { + "text": "Save to library" + } + ] + }, + "defaultIcon": { + "iconType": "LIBRARY_ADD" + }, + "defaultServiceEndpoint": { + "clickTrackingParams": "CB0QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpLHMvI9NTAH1b72E3E8M_TiRHfeAV2QQG99zKHXwLc1s4bS1bRiAuxjZcPZa8eCR9-LkFBlnpMLPwhsWd5JH6-jwLOk0Q" + } + }, + "toggledText": { + "runs": [ + { + "text": "Remove from library" + } + ] + }, + "toggledIcon": { + "iconType": "LIBRARY_SAVED" + }, + "toggledServiceEndpoint": { + "clickTrackingParams": "CB0QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJmCHH0N61R-ck4nVDG4wtOOshhZc320HAfVdKraBseoCJBO23YO04iwGWaW3Ss4OOjDmHbiQo67BXYjioJ6O5s3e5uLg" + } + }, + "trackingParams": "CB0QhP8FGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBwQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "Bcus42ihkTI" + } + }, + "trackingParams": "CBwQw5QGGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to album" + } + ] + }, + "icon": { + "iconType": "ALBUM" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBsQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPREb_zqisM59IhFI", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ALBUM" + } + } + } + }, + "trackingParams": "CBsQj_sFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBoQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCieyolnCbsWzyEt6wsd7lgQ", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CBoQkPsFGAYiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "View song credits" + } + ] + }, + "icon": { + "iconType": "PEOPLE_GROUP" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBkQr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "MPTCBcus42ihkTI", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_TRACK_CREDITS" + } + } + } + }, + "trackingParams": "CBkQr6MKGAciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBgQkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtCY3VzNDJpaGtUSQ%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CBgQkfsFGAgiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CBUQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJej_VTpKrs8fIOKS7t0oBIpfXVkIK34ypeEKoP6dsYm3e-A0EiO80JiEWv-k0dVGYDMAiyJz_BmNEvHFta1l-1f_eYoQ", + "actions": [ + { + "clickTrackingParams": "CBUQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CBUQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CBcQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CBUQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CBUQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "Bcus42ihkTI" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CBYQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CBYQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "Bcus42ihkTI" + }, + "actions": [ + { + "clickTrackingParams": "CBYQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "musicLibraryStatusUpdateCommand": { + "libraryStatus": "MUSIC_LIBRARY_STATUS_IN_LIBRARY", + "addToLibraryFeedbackToken": "AB9zfpLHMvI9NTAH1b72E3E8M_TiRHfeAV2QQG99zKHXwLc1s4bS1bRiAuxjZcPZa8eCR9-LkFBlnpMLPwhsWd5JH6-jwLOk0Q" + } + } + ] + } + }, + { + "clickTrackingParams": "CBYQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "Bcus42ihkTI" + } + } + }, + { + "clickTrackingParams": "CBYQpUEYCiITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "Bcus42ihkTI" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "Bcus42ihkTI" + } + } + } + ], + "trackingParams": "CBMQ-V4YAyITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "shelfDivider": { + "musicShelfDividerRenderer": { + "hidden": true + } + } + } + }, + { + "musicShelfRenderer": { + "title": { + "runs": [ + { + "text": "July 2022" + } + ] + }, + "contents": [ + { + "musicResponsiveListItemRenderer": { + "trackingParams": "CAUQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "thumbnail": { + "musicThumbnailRenderer": { + "thumbnail": { + "thumbnails": [ + { + "url": "https://i.ytimg.com/vi/iBs8XgoNe1c/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AMzJL3kTwvgU5Fs-nyi0thvefssM2_6h3g", + "width": 400, + "height": 225 + } + ] + }, + "thumbnailCrop": "MUSIC_THUMBNAIL_CROP_UNSPECIFIED", + "thumbnailScale": "MUSIC_THUMBNAIL_SCALE_ASPECT_FIT", + "trackingParams": "CBIQhL8CIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + }, + "overlay": { + "musicItemThumbnailOverlayRenderer": { + "background": { + "verticalGradient": { + "gradientLayerColors": [ + "3422552064", + "3422552064" + ] + } + }, + "content": { + "musicPlayButtonRenderer": { + "playNavigationEndpoint": { + "clickTrackingParams": "CBEQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "watchEndpoint": { + "videoId": "iBs8XgoNe1c", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CBEQyN4CIhMIt_TqieGBiQMVxdVyCR1QITDt", + "playIcon": { + "iconType": "PLAY_ARROW" + }, + "pauseIcon": { + "iconType": "PAUSE" + }, + "iconColor": 4294967295, + "backgroundColor": 0, + "activeBackgroundColor": 0, + "loadingIndicatorColor": 14745645, + "playingIcon": { + "iconType": "VOLUME_UP" + }, + "iconLoadingColor": 0, + "activeScaleFactor": 1, + "buttonSize": "MUSIC_PLAY_BUTTON_SIZE_SMALL", + "rippleTarget": "MUSIC_PLAY_BUTTON_RIPPLE_TARGET_SELF", + "accessibilityPlayData": { + "accessibilityData": { + "label": "Play Generous Heart - Maya Hawke" + } + }, + "accessibilityPauseData": { + "accessibilityData": { + "label": "Pause Generous Heart - Maya Hawke" + } + } + } + }, + "contentPosition": "MUSIC_ITEM_THUMBNAIL_OVERLAY_CONTENT_POSITION_CENTERED", + "displayStyle": "MUSIC_ITEM_THUMBNAIL_OVERLAY_DISPLAY_STYLE_PERSISTENT" + } + }, + "flexColumns": [ + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Generous Heart", + "navigationEndpoint": { + "clickTrackingParams": "CAUQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "iBs8XgoNe1c", + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + } + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": { + "runs": [ + { + "text": "Maya Hawke", + "navigationEndpoint": { + "clickTrackingParams": "CAUQyfQCGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCpCdLTLfDakfXAI29jeytkg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + } + }, + { + "text": " • " + }, + { + "text": "1.1M views" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH" + } + }, + { + "musicResponsiveListItemFlexColumnRenderer": { + "text": {}, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_MEDIUM" + } + } + ], + "fixedColumns": [ + { + "musicResponsiveListItemFixedColumnRenderer": { + "text": { + "runs": [ + { + "text": "3:49" + } + ] + }, + "displayPriority": "MUSIC_RESPONSIVE_LIST_ITEM_COLUMN_DISPLAY_PRIORITY_HIGH", + "size": "MUSIC_RESPONSIVE_LIST_ITEM_FIXED_COLUMN_SIZE_SMALL" + } + } + ], + "menu": { + "menuRenderer": { + "items": [ + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Start radio" + } + ] + }, + "icon": { + "iconType": "MIX" + }, + "navigationEndpoint": { + "clickTrackingParams": "CBAQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "iBs8XgoNe1c", + "playlistId": "RDAMVMiBs8XgoNe1c", + "params": "wAEB", + "loggingContext": { + "vssLoggingContext": { + "serializedContextData": "GhFSREFNVk1pQnM4WGdvTmUxYw%3D%3D" + } + }, + "watchEndpointMusicSupportedConfigs": { + "watchEndpointMusicConfig": { + "musicVideoType": "MUSIC_VIDEO_TYPE_OMV" + } + } + } + }, + "trackingParams": "CBAQm_MFGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Play next" + } + ] + }, + "icon": { + "iconType": "QUEUE_PLAY_NEXT" + }, + "serviceEndpoint": { + "clickTrackingParams": "CA4Qvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "iBs8XgoNe1c", + "onEmptyQueue": { + "clickTrackingParams": "CA4Qvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "iBs8XgoNe1c" + } + } + }, + "queueInsertPosition": "INSERT_AFTER_CURRENT_VIDEO", + "commands": [ + { + "clickTrackingParams": "CA4Qvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song will play next" + } + ] + }, + "trackingParams": "CA8QyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CA4Qvu4FGAEiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Add to queue" + } + ] + }, + "icon": { + "iconType": "ADD_TO_REMOTE_QUEUE" + }, + "serviceEndpoint": { + "clickTrackingParams": "CAwQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "queueAddEndpoint": { + "queueTarget": { + "videoId": "iBs8XgoNe1c", + "onEmptyQueue": { + "clickTrackingParams": "CAwQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "watchEndpoint": { + "videoId": "iBs8XgoNe1c" + } + } + }, + "queueInsertPosition": "INSERT_AT_END", + "commands": [ + { + "clickTrackingParams": "CAwQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "Song added to queue" + } + ] + }, + "trackingParams": "CA0QyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CAwQ--8FGAIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Save to playlist" + } + ] + }, + "icon": { + "iconType": "ADD_TO_PLAYLIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CAsQw5QGGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToPlaylistEndpoint": { + "videoId": "iBs8XgoNe1c" + } + }, + "trackingParams": "CAsQw5QGGAMiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Go to artist" + } + ] + }, + "icon": { + "iconType": "ARTIST" + }, + "navigationEndpoint": { + "clickTrackingParams": "CAoQkPsFGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "browseEndpoint": { + "browseId": "UCpCdLTLfDakfXAI29jeytkg", + "browseEndpointContextSupportedConfigs": { + "browseEndpointContextMusicConfig": { + "pageType": "MUSIC_PAGE_TYPE_ARTIST" + } + } + } + }, + "trackingParams": "CAoQkPsFGAQiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuNavigationItemRenderer": { + "text": { + "runs": [ + { + "text": "Share" + } + ] + }, + "icon": { + "iconType": "SHARE" + }, + "navigationEndpoint": { + "clickTrackingParams": "CAkQkfsFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "shareEntityEndpoint": { + "serializedShareEntity": "CgtpQnM4WGdvTmUxYw%3D%3D", + "sharePanelType": "SHARE_PANEL_TYPE_UNIFIED_SHARE_PANEL" + } + }, + "trackingParams": "CAkQkfsFGAUiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + { + "menuServiceItemRenderer": { + "text": { + "runs": [ + { + "text": "Remove from history" + } + ] + }, + "icon": { + "iconType": "REMOVE_FROM_HISTORY" + }, + "serviceEndpoint": { + "clickTrackingParams": "CAYQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "feedbackEndpoint": { + "feedbackToken": "AB9zfpJ3thZOEUQswqS4m4Lzmd53sj00aZ0LU4nzWKeZhzICUgXTnd6_HTpAYp-SaerIhrB2gYL5Y_evqwErFfx8qiUAGzpHZA", + "actions": [ + { + "clickTrackingParams": "CAYQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "hideEnclosingAction": { + "hack": true + } + }, + { + "clickTrackingParams": "CAYQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "addToToastAction": { + "item": { + "notificationTextRenderer": { + "successResponseText": { + "runs": [ + { + "text": "This item has been removed from your history." + } + ] + }, + "trackingParams": "CAgQyscDIhMIt_TqieGBiQMVxdVyCR1QITDt" + } + } + } + } + ] + } + }, + "trackingParams": "CAYQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ], + "trackingParams": "CAYQpzsiEwi39OqJ4YGJAxXF1XIJHVAhMO0=", + "topLevelButtons": [ + { + "likeButtonRenderer": { + "target": { + "videoId": "iBs8XgoNe1c" + }, + "likeStatus": "INDIFFERENT", + "trackingParams": "CAcQpUEYByITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likesAllowed": true, + "serviceEndpoints": [ + { + "clickTrackingParams": "CAcQpUEYByITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "LIKE", + "target": { + "videoId": "iBs8XgoNe1c" + } + } + }, + { + "clickTrackingParams": "CAcQpUEYByITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "DISLIKE", + "target": { + "videoId": "iBs8XgoNe1c" + } + } + }, + { + "clickTrackingParams": "CAcQpUEYByITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "likeEndpoint": { + "status": "INDIFFERENT", + "target": { + "videoId": "iBs8XgoNe1c" + } + } + } + ] + } + } + ], + "accessibility": { + "accessibilityData": { + "label": "Action menu" + } + } + } + }, + "playlistItemData": { + "videoId": "iBs8XgoNe1c" + } + } + } + ], + "trackingParams": "CAQQ-V4YBCITCLf06onhgYkDFcXVcgkdUCEw7Q==", + "shelfDivider": { + "musicShelfDividerRenderer": { + "hidden": true + } + } + } + } + ], + "trackingParams": "CAMQui8iEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + "tabIdentifier": "FEmusic_history", + "trackingParams": "CAIQ8JMBGAAiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + } + ] + } + }, + "header": { + "musicHeaderRenderer": { + "title": { + "runs": [ + { + "text": "History" + } + ] + }, + "trackingParams": "CAEQ4HIiEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } + }, + "trackingParams": "CAAQhGciEwi39OqJ4YGJAxXF1XIJHVAhMO0=" + } +} diff --git a/src/backend/utils/PlayComparisonUtils.ts b/src/backend/utils/PlayComparisonUtils.ts index c81f5f2f..330fe449 100644 --- a/src/backend/utils/PlayComparisonUtils.ts +++ b/src/backend/utils/PlayComparisonUtils.ts @@ -1,4 +1,4 @@ -import { getListDiff } from "@donedeal0/superdiff"; +import { getListDiff, ListDiff } from "@donedeal0/superdiff"; import { PlayObject } from "../../core/Atomic.js"; import { buildTrackString } from "../../core/StringUtils.js"; @@ -38,7 +38,7 @@ export type ListTransformers = PlayTransformer[]; export const defaultListTransformers: ListTransformers = [metaInvariantTransform, playDateInvariantTransform]; -export const getPlaysDiff = (aPlays: PlayObject[], bPlays: PlayObject[], transformers: ListTransformers = defaultListTransformers) => { +export const getPlaysDiff = (aPlays: PlayObject[], bPlays: PlayObject[], transformers: ListTransformers = defaultListTransformers): ListDiff => { const cleanAPlays = transformers === undefined ? aPlays : transformers.reduce((acc: PlayObject[], curr) => acc.map(curr), aPlays); const cleanBPlays = transformers === undefined ? bPlays : transformers.reduce((acc: PlayObject[], curr) => acc.map(curr), bPlays); @@ -114,13 +114,69 @@ export const playsAreAddedOnly = (aPlays: PlayObject[], bPlays: PlayObject[], tr } } const added = results.diff.filter(x => x.status === 'added'); - return [addType !== 'insert', added.map(x => bPlays[x.newIndex]), addType]; + return [addType !== 'insert' && addType !== undefined, added.map(x => bPlays[x.newIndex]), addType]; } -export const humanReadableDiff = (aPlay: PlayObject[], bPlay: PlayObject[], result: any): string => { +export const playsAreBumpedOnly = (aPlays: PlayObject[], bPlays: PlayObject[], transformers: ListTransformers = defaultListTransformers): [boolean, PlayObject[]?, ('append' | 'prepend')?] => { + const results = getPlaysDiff(aPlays, bPlays, transformers); + if(results.status === 'equal' || results.status === 'deleted') { + return [false]; + } + if(aPlays.length !== bPlays.length) { + return [false]; + } + + let addTypeShouldBe: 'append' | 'prepend'; + let cursor: 'moved' | 'equal'; + + for(const [index, diffData] of results.diff.entries()) { + if(diffData.status !== 'moved' && diffData.status !== 'equal') { + return [false]; + } + + if(index === 0) { + if(diffData.status === 'moved' && diffData.indexDiff < 0) { + addTypeShouldBe = 'prepend'; + } else if(diffData.status === 'equal') { + addTypeShouldBe = 'append'; + } else { + return [false]; + } + } else { + + if(index === results.diff.length - 1) { + if(addTypeShouldBe === 'append' && diffData.status !== 'moved') { + return [false]; + } + } else { + + if(![-1,0,1].includes(diffData.indexDiff)) { + return [false]; // shifted more than one spot in list which isn't a bump + } + if(cursor === undefined) { // first non-initial item + cursor = diffData.status; + continue; + } else if( + (addTypeShouldBe === 'prepend' && cursor === 'equal' && diffData.status === 'moved') + || (addTypeShouldBe === 'append' && cursor === 'moved' && diffData.status === 'equal') + ) { + // can't go back from equal (passed bump point) to moved b/c would mean more than one item moved and not just one bump + return [false]; + } + + // otherwise intermediate + cursor = diffData.status; + } + } + } + + return [true, addTypeShouldBe === 'prepend' ? [bPlays[0]] : [bPlays[bPlays.length - 1]], addTypeShouldBe]; +} + +export const humanReadableDiff = (aPlay: PlayObject[], bPlay: PlayObject[], result: ListDiff): string => { const changes: [string, string?][] = []; for(const [index, play] of bPlay.entries()) { - const ab: [string, string?] = [`${index + 1}. ${buildTrackString(play)}`]; + const ab: [string, string?] = [`${index + 1}. ${buildTrackString(play, {include: ['artist', 'track', 'trackId', 'album']})}`]; const isEqual = result.diff.some(x => x.status === 'equal' && x.prevIndex === index && x.newIndex === index); if(!isEqual) { @@ -141,7 +197,7 @@ export const humanReadableDiff = (aPlay: PlayObject[], bPlay: PlayObject[], resu // was updated, probably?? const updated = result.diff.filter(x => x.status === 'deleted' && x.prevIndex === index); if(updated.length > 0) { - ab.push(`Updated - Original => ${buildTrackString( aPlay[updated[0].preIndex])}`); + ab.push(`Updated - Original => ${buildTrackString( aPlay[updated[0].prevIndex])}`); } else { ab.push('Should not have gotten this far!'); } diff --git a/src/client/components/statusCard/SourceStatusCard.tsx b/src/client/components/statusCard/SourceStatusCard.tsx index 269db1db..475ae06e 100644 --- a/src/client/components/statusCard/SourceStatusCard.tsx +++ b/src/client/components/statusCard/SourceStatusCard.tsx @@ -81,6 +81,7 @@ const SourceStatusCard = (props: SourceStatusCardData) => {
{discovered}: {tracksDiscovered}
{upstreamRecent} {canPoll && hasAuthInteraction ? (Re)authenticate : null} + {type === 'ytmusic' && 'userCode' in data ?
Code: {data.userCode as string}
: null} ); } return (