@@ -211,6 +211,7 @@ const ARTIST_CACHE_VERSION = "v3";
211211const ARTIST_SUBSCRIPTION_OVERRIDE_MS = 60_000 ;
212212const PLAYLIST_PAGE_SESSION_TTL_MS = 10 * 60_000 ;
213213const PLAYLIST_TRACK_CACHE_VERSION = "v4" ;
214+ const PLAYLIST_EMPTY_RETRY_DELAYS_MS = [ 0 , 600 , 1_500 ] ;
214215
215216class YouTubeMusicAuthError extends Error {
216217 constructor ( message : string ) {
@@ -1191,6 +1192,25 @@ export class YouTubeMusicDataSource extends DataSource {
11911192 } ) ;
11921193 }
11931194
1195+ private async updateCachedAlbumSaved ( album : Album , saved : boolean ) : Promise < void > {
1196+ const cachedLibrary = await getCachedJson < LibrarySnapshot > ( LIBRARY_CACHE_KEY ) ;
1197+ if ( ! cachedLibrary ) return ;
1198+
1199+ const sameAlbum = ( item : Album ) =>
1200+ item . id === album . id
1201+ || Boolean ( album . playlistId && item . playlistId === album . playlistId )
1202+ || Boolean ( album . playlistId && item . id === album . playlistId )
1203+ || Boolean ( item . playlistId && item . playlistId === album . id ) ;
1204+ const albums = saved
1205+ ? [ album , ...cachedLibrary . albums . filter ( ( item ) => ! sameAlbum ( item ) ) ]
1206+ : cachedLibrary . albums . filter ( ( item ) => ! sameAlbum ( item ) ) ;
1207+
1208+ await setCachedJson ( LIBRARY_CACHE_KEY , {
1209+ ...cachedLibrary ,
1210+ albums,
1211+ } ) ;
1212+ }
1213+
11941214 private getActionableServiceEndpoint ( endpoint : RawServiceEndpoint ) : RawServiceEndpoint {
11951215 const commands = endpoint . commandExecutorCommand ?. commands ;
11961216 if ( ! commands ?. length ) return endpoint ;
@@ -1493,6 +1513,39 @@ export class YouTubeMusicDataSource extends DataSource {
14931513 return tracks ;
14941514 }
14951515
1516+ private async waitForPlaylistEmptyRetry ( attempt : number ) : Promise < void > {
1517+ const delayMs = PLAYLIST_EMPTY_RETRY_DELAYS_MS [ attempt ] ?? 0 ;
1518+ if ( delayMs <= 0 ) return ;
1519+ await new Promise < void > ( ( resolve ) => globalThis . setTimeout ( resolve , delayMs ) ) ;
1520+ }
1521+
1522+ private async collectPlaylistTracksWithEmptyRetries (
1523+ client : Innertube ,
1524+ playlistId : string ,
1525+ source : string ,
1526+ ) : Promise < Track [ ] > {
1527+ for ( let attempt = 0 ; attempt < PLAYLIST_EMPTY_RETRY_DELAYS_MS . length ; attempt += 1 ) {
1528+ await this . waitForPlaylistEmptyRetry ( attempt ) ;
1529+
1530+ const tracks = await this . collectPlaylistTracks ( client , playlistId ) ;
1531+ if ( tracks . length > 0 ) return tracks ;
1532+
1533+ const browseId = playlistId . startsWith ( "VL" ) ? playlistId : `VL${ playlistId } ` ;
1534+ logInternalWarn ( "YouTubeMusicDataSource.collectPlaylistTracksWithEmptyRetries retrying empty response" , {
1535+ playlistId,
1536+ browseId,
1537+ source,
1538+ attempt : attempt + 1 ,
1539+ } ) ;
1540+
1541+ const response = await this . executeMusicBrowse ( client , { browseId } ) ;
1542+ const fallbackTracks = await this . collectAllTracks ( client , response ) ;
1543+ if ( fallbackTracks . length > 0 ) return fallbackTracks ;
1544+ }
1545+
1546+ return [ ] ;
1547+ }
1548+
14961549 private createPlaylistPageKey ( playlistId : string ) : string {
14971550 return `${ playlistId } :${ Date . now ( ) } :${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ;
14981551 }
@@ -2171,6 +2224,7 @@ export class YouTubeMusicDataSource extends DataSource {
21712224 albumPlaylistId,
21722225 saved,
21732226 } ) ;
2227+ await this . updateCachedAlbumSaved ( album , saved ) ;
21742228 return ;
21752229 } catch ( directError ) {
21762230 logInternalWarn ( "YouTubeMusicDataSource.setAlbumSaved direct like command failed" , {
@@ -2185,7 +2239,10 @@ export class YouTubeMusicDataSource extends DataSource {
21852239 const rawToggle = this . findRawLibraryToggle ( albumResponse ) ;
21862240
21872241 if ( rawToggle ) {
2188- if ( rawToggle . isToggled === saved ) return ;
2242+ if ( rawToggle . isToggled === saved ) {
2243+ await this . updateCachedAlbumSaved ( album , saved ) ;
2244+ return ;
2245+ }
21892246 const endpoint = saved
21902247 ? rawToggle . defaultServiceEndpoint
21912248 : rawToggle . toggledServiceEndpoint ;
@@ -2205,12 +2262,16 @@ export class YouTubeMusicDataSource extends DataSource {
22052262 if ( response . success === false ) {
22062263 throw new Error ( `Album library update returned HTTP ${ response . status_code } .` ) ;
22072264 }
2265+ await this . updateCachedAlbumSaved ( album , saved ) ;
22082266 return ;
22092267 }
22102268
22112269 const rawMenuToggle = this . findRawLibraryMenuToggle ( albumResponse ) ;
22122270 if ( rawMenuToggle ) {
2213- if ( rawMenuToggle . isToggled === saved ) return ;
2271+ if ( rawMenuToggle . isToggled === saved ) {
2272+ await this . updateCachedAlbumSaved ( album , saved ) ;
2273+ return ;
2274+ }
22142275 const endpoint = saved
22152276 ? rawMenuToggle . defaultServiceEndpoint
22162277 : rawMenuToggle . toggledServiceEndpoint ;
@@ -2231,6 +2292,7 @@ export class YouTubeMusicDataSource extends DataSource {
22312292 if ( response . success === false ) {
22322293 throw new Error ( `Album library menu update returned HTTP ${ response . status_code } .` ) ;
22332294 }
2295+ await this . updateCachedAlbumSaved ( album , saved ) ;
22342296 return ;
22352297 }
22362298
@@ -2239,7 +2301,10 @@ export class YouTubeMusicDataSource extends DataSource {
22392301 if ( ! toggle ) {
22402302 throw new Error ( "YouTube Music did not return a library command for this album." ) ;
22412303 }
2242- if ( toggle . isToggled === saved ) return ;
2304+ if ( toggle . isToggled === saved ) {
2305+ await this . updateCachedAlbumSaved ( album , saved ) ;
2306+ return ;
2307+ }
22432308
22442309 const endpoint = saved ? toggle . endpoint : toggle . toggledEndpoint ;
22452310 if ( ! endpoint ) {
@@ -2258,6 +2323,7 @@ export class YouTubeMusicDataSource extends DataSource {
22582323 if ( response . success === false ) {
22592324 throw new Error ( `Album library update returned HTTP ${ response . status_code } .` ) ;
22602325 }
2326+ await this . updateCachedAlbumSaved ( album , saved ) ;
22612327 } catch ( error ) {
22622328 logInternalError ( "YouTubeMusicDataSource.setAlbumSaved failed" , error , {
22632329 albumId : album . id ,
@@ -2728,7 +2794,11 @@ export class YouTubeMusicDataSource extends DataSource {
27282794 logInternalWarn ( "YouTubeMusicDataSource.getPlaylistTrackPage verifying empty first page" , {
27292795 playlistId : playlist . id ,
27302796 } ) ;
2731- const fallbackTracks = await this . collectPlaylistTracks ( client , playlist . id ) ;
2797+ const fallbackTracks = await this . collectPlaylistTracksWithEmptyRetries (
2798+ client ,
2799+ playlist . id ,
2800+ "paged-first-load" ,
2801+ ) ;
27322802 if ( fallbackTracks . length > 0 ) {
27332803 const cachedFallbackTracks = await this . cachePlaylistTracks ( playlist . id , fallbackTracks ) ;
27342804 return { tracks : cachedFallbackTracks , hasMore : false } ;
@@ -2787,16 +2857,7 @@ export class YouTubeMusicDataSource extends DataSource {
27872857
27882858 private async fetchPlaylistTracksFresh ( playlist : Playlist ) : Promise < Track [ ] > {
27892859 const client = await this . getMusicClient ( ) ;
2790- const tracks = await this . collectPlaylistTracks ( client , playlist . id ) ;
2791- if ( tracks . length > 0 ) return tracks ;
2792-
2793- const browseId = playlist . id . startsWith ( "VL" ) ? playlist . id : `VL${ playlist . id } ` ;
2794- logInternalWarn ( "YouTubeMusicDataSource.fetchPlaylistTracksFresh retrying empty playlist response" , {
2795- playlistId : playlist . id ,
2796- browseId,
2797- } ) ;
2798- const response = await this . executeMusicBrowse ( client , { browseId } ) ;
2799- return this . collectAllTracks ( client , response ) ;
2860+ return this . collectPlaylistTracksWithEmptyRetries ( client , playlist . id , "fresh-load" ) ;
28002861 }
28012862
28022863 async addTrackToPlaylist (
0 commit comments