@@ -303,7 +303,7 @@ def get_active_queue(self, player_id: str) -> PlayerQueue | None:
303303 # Queue commands
304304
305305 @api_command ("player_queues/shuffle" )
306- def set_shuffle (self , queue_id : str , shuffle_enabled : bool ) -> None :
306+ async def set_shuffle (self , queue_id : str , shuffle_enabled : bool ) -> None :
307307 """Configure shuffle setting on the the queue."""
308308 queue = self ._queues [queue_id ]
309309 if queue .shuffle_enabled == shuffle_enabled :
@@ -320,7 +320,7 @@ def set_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None:
320320 if not shuffle_enabled :
321321 # shuffle disabled, try to restore original sort order of the remaining items
322322 next_items .sort (key = lambda x : x .sort_index , reverse = False )
323- self .load (
323+ await self .load (
324324 queue_id = queue_id ,
325325 queue_items = next_items ,
326326 insert_at_index = next_index ,
@@ -517,7 +517,7 @@ async def play_media(
517517
518518 # handle replace: clear all items and replace with the new items
519519 if option == QueueOption .REPLACE :
520- self .load (
520+ await self .load (
521521 queue_id ,
522522 queue_items = queue_items ,
523523 keep_remaining = False ,
@@ -528,15 +528,15 @@ async def play_media(
528528 return
529529 # handle next: add item(s) in the index next to the playing/loaded/buffered index
530530 if option == QueueOption .NEXT :
531- self .load (
531+ await self .load (
532532 queue_id ,
533533 queue_items = queue_items ,
534534 insert_at_index = insert_at_index ,
535535 shuffle = shuffle ,
536536 )
537537 return
538538 if option == QueueOption .REPLACE_NEXT :
539- self .load (
539+ await self .load (
540540 queue_id ,
541541 queue_items = queue_items ,
542542 insert_at_index = insert_at_index ,
@@ -546,7 +546,7 @@ async def play_media(
546546 return
547547 # handle play: replace current loaded/playing index with new item(s)
548548 if option == QueueOption .PLAY :
549- self .load (
549+ await self .load (
550550 queue_id ,
551551 queue_items = queue_items ,
552552 insert_at_index = insert_at_index ,
@@ -557,7 +557,7 @@ async def play_media(
557557 return
558558 # handle add: add/append item(s) to the remaining queue items
559559 if option == QueueOption .ADD :
560- self .load (
560+ await self .load (
561561 queue_id = queue_id ,
562562 queue_items = queue_items ,
563563 insert_at_index = insert_at_index
@@ -1014,7 +1014,7 @@ async def transfer_queue(
10141014 target_queue .current_item .queue_id = target_queue_id
10151015 self .clear (source_queue_id )
10161016
1017- self .load (target_queue_id , source_items , keep_remaining = False , keep_played = False )
1017+ await self .load (target_queue_id , source_items , keep_remaining = False , keep_played = False )
10181018 for item in source_items :
10191019 item .queue_id = target_queue_id
10201020 self .update_items (target_queue_id , source_items )
@@ -1302,7 +1302,7 @@ def track_loaded_in_buffer(self, queue_id: str, item_id: str) -> None:
13021302
13031303 # Main queue manipulation methods
13041304
1305- def load (
1305+ async def load (
13061306 self ,
13071307 queue_id : str ,
13081308 queue_items : list [QueueItem ],
@@ -1331,7 +1331,7 @@ def load(
13311331 item .sort_index += insert_at_index + index
13321332 # (re)shuffle the final batch if needed
13331333 if shuffle :
1334- next_items = _smart_shuffle (next_items )
1334+ next_items = await _smart_shuffle (next_items )
13351335 self .update_items (queue_id , prev_items + next_items )
13361336
13371337 def update_items (self , queue_id : str , queue_items : list [QueueItem ]) -> None :
@@ -1693,7 +1693,7 @@ async def _fill_radio_tracks(self, queue_id: str) -> None:
16931693 tracks = await self ._get_radio_tracks (queue_id = queue_id , is_initial_radio_mode = False )
16941694 # fill queue - filter out unavailable items
16951695 queue_items = [QueueItem .from_media_item (queue_id , x ) for x in tracks if x .available ]
1696- self .load (
1696+ await self .load (
16971697 queue_id ,
16981698 queue_items ,
16991699 insert_at_index = len (self ._queue_items [queue_id ]) + 1 ,
@@ -2476,96 +2476,36 @@ def _handle_playback_progress_report(
24762476 )
24772477
24782478
2479- def _smart_shuffle (items : list [QueueItem ]) -> list [QueueItem ]:
2480- """Shuffle queue items with smart spacing rules .
2479+ async def _smart_shuffle (items : list [QueueItem ]) -> list [QueueItem ]:
2480+ """Shuffle queue items, avoiding identical tracks next to each other .
24812481
2482- This shuffle tries to prevent the same track and artist from appearing
2483- too close together. Spacing requirements scale with playlist size:
2484- - >1000 items: track spacing 15, artist spacing 10
2485- - >500 items: track spacing 10, artist spacing 6
2486- - >100 items: track spacing 5, artist spacing 3
2487- - <=100 items: track spacing 2, no artist spacing
2488-
2489- This is a best-effort approach - when playing an album where all tracks
2490- are from the same artist, artist spacing won't be possible.
2482+ Best-effort approach to prevent the same track from appearing adjacent.
2483+ Does a random shuffle first, then makes a limited number of passes to
2484+ swap adjacent duplicates with a random item further in the list.
24912485
24922486 :param items: List of queue items to shuffle.
24932487 """
2494- if len (items ) <= 1 :
2495- return items
2496-
2497- # Determine spacing based on playlist size
2498- num_items = len (items )
2499- if num_items > 1000 :
2500- track_spacing , artist_spacing = 15 , 10
2501- elif num_items > 500 :
2502- track_spacing , artist_spacing = 10 , 6
2503- elif num_items > 100 :
2504- track_spacing , artist_spacing = 5 , 3
2505- else :
2506- track_spacing , artist_spacing = 2 , 0
2507-
2508- # Extract artist from name format "<artist(s)> - <title>"
2509- def get_artist (name : str ) -> str | None :
2510- return name .split (" - " , 1 )[0 ] if " - " in name else None
2488+ if len (items ) <= 2 :
2489+ return random .sample (items , len (items )) if len (items ) == 2 else items
25112490
25122491 # Start with a random shuffle
25132492 shuffled = random .sample (items , len (items ))
25142493
2515- # Iteratively fix violations
2516- max_attempts = len (items ) * 3
2517- for _ in range (max_attempts ):
2518- violation_found = False
2519-
2520- for i in range (1 , len (shuffled )):
2521- current = shuffled [i ]
2522- current_artist = get_artist (current .name )
2523-
2524- # Check for track collision
2525- has_violation = any (
2526- shuffled [j ].name == current .name for j in range (max (0 , i - track_spacing ), i )
2527- )
2528-
2529- # Check for artist collision (only if artist_spacing > 0)
2530- if not has_violation and artist_spacing and current_artist :
2531- has_violation = any (
2532- get_artist (shuffled [j ].name ) == current_artist
2533- for j in range (max (0 , i - artist_spacing ), i )
2534- )
2535-
2536- if has_violation :
2537- violation_found = True
2538- # Find best position after current by scoring distance from conflicts
2539- best_pos , best_score = i , - 1
2540- for pos in range (i + 1 , len (shuffled )):
2541- track_dist = min (
2542- (
2543- pos - j
2544- for j in range (max (0 , pos - track_spacing ), pos )
2545- if shuffled [j ].name == current .name
2546- ),
2547- default = track_spacing ,
2548- )
2549- artist_dist = artist_spacing
2550- if artist_spacing and current_artist :
2551- artist_dist = min (
2552- (
2553- pos - j
2554- for j in range (max (0 , pos - artist_spacing ), pos )
2555- if get_artist (shuffled [j ].name ) == current_artist
2556- ),
2557- default = artist_spacing ,
2558- )
2559- score = track_dist * 2 + artist_dist
2560- if score > best_score :
2561- best_score , best_pos = score , pos
2562-
2563- if best_pos != i :
2564- item = shuffled .pop (i )
2565- shuffled .insert (best_pos , item )
2566- break
2567-
2568- if not violation_found :
2494+ # Make a few passes to fix adjacent duplicates
2495+ max_passes = 3
2496+ for _ in range (max_passes ):
2497+ swapped = False
2498+ for i in range (len (shuffled ) - 1 ):
2499+ if shuffled [i ].name == shuffled [i + 1 ].name :
2500+ # Found adjacent duplicate - swap with random position at least 2 away
2501+ swap_candidates = [j for j in range (len (shuffled )) if abs (j - i - 1 ) >= 2 ]
2502+ if swap_candidates :
2503+ swap_pos = random .choice (swap_candidates )
2504+ shuffled [i + 1 ], shuffled [swap_pos ] = shuffled [swap_pos ], shuffled [i + 1 ]
2505+ swapped = True
2506+ if not swapped :
25692507 break
2508+ # Yield to event loop between passes
2509+ await asyncio .sleep (0 )
25702510
25712511 return shuffled
0 commit comments