@@ -45,7 +45,7 @@ class SyncAlbum extends DefaultFeeder {
4545 debug ( `get ${ albums . items . length } albums` ) ;
4646 debug ( albums . items ) ;
4747 let albumId = null ;
48- total = albums . items . length ;
48+ total = albums . total || albums . items . length ;
4949 switch ( albums . items . length ) {
5050 case 0 :
5151 // TODO: there is no such album
@@ -78,13 +78,17 @@ class SyncAlbum extends DefaultFeeder {
7878 albumId = albums . items [ 0 ] . identifier ;
7979 break ;
8080 }
81- return albumId ;
81+ return { albumId, total , slots } ;
8282 } )
83- . then ( ( albumId ) => {
83+ . then ( ( { albumId, total, slots } ) => {
84+ // Store total and slots in playlist extra for fetching random albums later
85+ playlist . setExtra ( app , { total, slots } ) ;
86+
8487 if ( albumId ) {
8588 debug ( 'id of album:' , albumId ) ;
8689 return albumsProvider . fetchAlbumDetails ( app , albumId ) ;
8790 }
91+ return null ;
8892 } )
8993 . then ( album => {
9094 if ( ! album ) {
@@ -101,9 +105,10 @@ class SyncAlbum extends DefaultFeeder {
101105 // the only place where we modify state
102106 // so maybe we can put it out of this function?
103107 debug ( 'let\'s create playlist for songs' ) ;
104- playlist . create ( app , songs ) ;
108+ const extra = playlist . getExtra ( app ) ;
109+ playlist . create ( app , songs , extra ) ;
105110
106- return { total } ;
111+ return { total : extra . total } ;
107112 } ) ;
108113 // TODO:
109114 // .catch(err => {
@@ -112,6 +117,236 @@ class SyncAlbum extends DefaultFeeder {
112117 // });
113118 // });
114119 }
120+
121+ /**
122+ * Move to the next song
123+ * Called by both "next" and "skip" commands
124+ * If playlist runs out, fetch a random album from the collection
125+ *
126+ * @param ctx
127+ * @param move
128+ * @returns {Promise.<T> }
129+ */
130+ next ( ctx , move = true ) {
131+ const { app, playlist } = ctx ;
132+
133+ // Check if there are more songs in the current playlist
134+ if ( playlist . hasNextSong ( app ) ) {
135+ // Just move to the next song
136+ if ( move ) {
137+ debug ( 'move to the next song' ) ;
138+ playlist . next ( app ) ;
139+ }
140+ return Promise . resolve ( ctx ) ;
141+ }
142+
143+ // No more songs, fetch a random album
144+ debug ( 'playlist ran out, fetching random album' ) ;
145+ let extra = playlist . getExtra ( app ) ;
146+ let slots = extra && extra . slots ;
147+ let total = extra && extra . total ;
148+
149+ // Fallback: try to get slots from query if extra data is missing
150+ if ( ! slots && ctx . query ) {
151+ try {
152+ slots = objToLowerCase ( ctx . query . getSlots ( app ) ) ;
153+ debug ( 'got slots from query as fallback:' , slots ) ;
154+ } catch ( e ) {
155+ warning ( 'error getting slots from query:' , e ) ;
156+ }
157+ }
158+
159+ if ( ! slots ) {
160+ warning ( 'no slots found, cannot fetch random album' ) ;
161+ return Promise . resolve ( ctx ) ;
162+ }
163+
164+ // If we don't have total, try to fetch it first
165+ if ( ! total ) {
166+ debug ( 'total not found in extra, fetching to get total count' ) ;
167+ return albumsProvider
168+ . fetchAlbumsByQuery ( app , slots )
169+ . then ( albums => {
170+ if ( albums && albums . total ) {
171+ total = albums . total ;
172+ // Update extra with total for future use
173+ playlist . setExtra ( app , { ...extra , total, slots } ) ;
174+ debug ( `got total from API: ${ total } ` ) ;
175+ }
176+ // Continue with random fetch even if we couldn't get total
177+ return this . fetchRandomAlbum ( ctx , slots , total , move ) ;
178+ } )
179+ . catch ( error => {
180+ warning ( 'error fetching total:' , error ) ;
181+ // Still try to fetch random album with estimated total
182+ return this . fetchRandomAlbum ( ctx , slots , 1000 , move ) ; // Use a reasonable default
183+ } ) ;
184+ }
185+
186+ return this . fetchRandomAlbum ( ctx , slots , total , move ) ;
187+ }
188+
189+ /**
190+ * Fetch a random album and add it to the playlist
191+ *
192+ * @private
193+ * @param ctx
194+ * @param slots
195+ * @param total
196+ * @param move
197+ * @returns {Promise }
198+ */
199+ fetchRandomAlbum ( ctx , slots , total , move = true ) {
200+ const { app, playlist } = ctx ;
201+
202+ // Calculate random page number
203+ // Each page has limit items, so max page = Math.floor(total / limit)
204+ const limit = 3 ; // Fetch multiple albums to pick randomly from
205+ const maxPage = total ? Math . max ( 0 , Math . floor ( ( total - 1 ) / limit ) ) : 100 ; // Default to page 100 if total unknown
206+ const randomPage = Math . floor ( Math . random ( ) * ( maxPage + 1 ) ) ;
207+
208+ debug ( `fetching random album from page ${ randomPage } (total: ${ total || 'unknown' } , maxPage: ${ maxPage } )` ) ;
209+
210+ return albumsProvider
211+ . fetchAlbumsByQuery ( app , {
212+ ...slots ,
213+ limit : limit , // Fetch multiple albums for better randomness
214+ page : randomPage ,
215+ order : 'best' , // Use best order to get random page
216+ } )
217+ . then ( albums => {
218+ if ( ! albums || ! albums . items || albums . items . length === 0 ) {
219+ warning ( 'no albums found for random fetch' ) ;
220+ return ctx ;
221+ }
222+
223+ // Pick a random album from the results for better randomness
224+ const randomAlbum = albums . items [ Math . floor ( Math . random ( ) * albums . items . length ) ] ;
225+ debug ( `selected random album: ${ randomAlbum . identifier } from ${ albums . items . length } albums` ) ;
226+
227+ return albumsProvider . fetchAlbumDetails ( app , randomAlbum . identifier ) ;
228+ } )
229+ . then ( album => {
230+ if ( ! album ) {
231+ warning ( 'failed to fetch random album details' ) ;
232+ return ctx ;
233+ }
234+
235+ debug ( 'fetched random album' , album ) ;
236+ const songs = this . processAlbumSongs ( app , album ) ;
237+ debug ( `adding ${ songs . length } songs from random album to playlist` ) ;
238+
239+ // IMPORTANT: Ensure extra data is set BEFORE updating items
240+ // This ensures it's preserved even if updateItems has issues
241+ const currentExtra = playlist . getExtra ( app ) ;
242+ const extraToPreserve = currentExtra || ( slots && total ? { total, slots } : null ) ;
243+ if ( extraToPreserve ) {
244+ playlist . setExtra ( app , extraToPreserve ) ;
245+ debug ( 'ensuring extra data is set before updating items:' , extraToPreserve ) ;
246+ }
247+
248+ // Append new songs to existing playlist
249+ const currentItems = playlist . getItems ( app ) ;
250+ const newItems = currentItems . concat ( songs ) ;
251+ playlist . updateItems ( app , newItems ) ;
252+
253+ // Double-check extra data is still there after updateItems
254+ const extraAfterUpdate = playlist . getExtra ( app ) ;
255+ if ( ! extraAfterUpdate && extraToPreserve ) {
256+ warning ( 'extra data was lost after updateItems, restoring it' ) ;
257+ playlist . setExtra ( app , extraToPreserve ) ;
258+ } else {
259+ debug ( 'extra data preserved after updateItems:' , extraAfterUpdate ) ;
260+ }
261+
262+ // Move to the first new song
263+ if ( move && songs . length > 0 ) {
264+ playlist . moveTo ( app , songs [ 0 ] ) ;
265+ }
266+
267+ // Final check: ensure extra data is still there after moveTo
268+ const extraAfterMove = playlist . getExtra ( app ) ;
269+ if ( ! extraAfterMove && extraToPreserve ) {
270+ warning ( 'extra data was lost after moveTo, restoring it' ) ;
271+ playlist . setExtra ( app , extraToPreserve ) ;
272+ }
273+
274+ return ctx ;
275+ } )
276+ . catch ( error => {
277+ warning ( 'error fetching random album:' , error ) ;
278+ return ctx ;
279+ } ) ;
280+ }
281+
282+ /**
283+ * Do we have next item?
284+ * Always return true if we can fetch more albums from the collection
285+ *
286+ * @param ctx
287+ * @returns {boolean }
288+ */
289+ hasNext ( ctx ) {
290+ const { app, query, playlist } = ctx ;
291+
292+ // If playlist is looped, always have next
293+ if ( playlist . isLoop ( app ) ) {
294+ debug ( 'hasNext: playlist is looped' ) ;
295+ return true ;
296+ }
297+
298+ // If there are more songs in current playlist, we have next
299+ if ( playlist . hasNextSong ( app ) ) {
300+ debug ( 'hasNext: has more songs in current playlist' ) ;
301+ return true ;
302+ }
303+
304+ // Always have something when songs in shuffle/random order
305+ try {
306+ if ( query && typeof query . getSlot === 'function' ) {
307+ const order = query . getSlot ( app , 'order' ) ;
308+ if ( order === 'random' ) {
309+ debug ( 'hasNext: order is random' ) ;
310+ return true ;
311+ }
312+ }
313+ } catch ( e ) {
314+ debug ( 'hasNext: error checking order slot:' , e ) ;
315+ }
316+
317+ // Check if we can fetch more albums from the collection
318+ let extra = playlist . getExtra ( app ) ;
319+ debug ( 'hasNext check - extra from playlist:' , extra ) ;
320+
321+ // Fallback: try to get slots from query if extra data is missing
322+ if ( ( ! extra || ! extra . slots ) && query && typeof query . getSlots === 'function' ) {
323+ try {
324+ const querySlots = query . getSlots ( app ) ;
325+ if ( querySlots && Object . keys ( querySlots ) . length > 0 ) {
326+ debug ( 'hasNext: got slots from query as fallback:' , querySlots ) ;
327+ // If we have query slots, we can always fetch more albums
328+ return true ;
329+ }
330+ } catch ( e ) {
331+ debug ( 'hasNext: error getting slots from query:' , e ) ;
332+ }
333+ }
334+
335+ if ( extra && extra . total && typeof extra . total === 'number' && extra . total > 0 ) {
336+ // We can always fetch more random albums from the collection
337+ debug ( `hasNext: can fetch more albums (total: ${ extra . total } )` ) ;
338+ return true ;
339+ }
340+
341+ // If we have slots stored, we can try to fetch more (even if total is unknown)
342+ if ( extra && extra . slots && Object . keys ( extra . slots ) . length > 0 ) {
343+ debug ( 'hasNext: have slots stored, can try to fetch more albums' ) ;
344+ return true ;
345+ }
346+
347+ debug ( 'hasNext: no more songs available and cannot fetch more' ) ;
348+ return false ;
349+ }
115350}
116351
117352module . exports = new SyncAlbum ( ) ;
0 commit comments