Add multi-disc album support with automatic disc subfolder organization

Multi-disc albums (e.g., deluxe editions, double albums) now automatically organize
  tracks into Disc 1/, Disc 2/ subfolders within the album folder. Detection uses the
  disc_number field from Spotify's API — when an album has total_discs > 1, subfolders
  are created. Single-disc albums are completely unaffected.

  - Plumb disc_number through all download paths (enhanced, non-enhanced, download
    missing modal, wishlist)
  - Compute total_discs from album tracklist and store on album context
  - Modify path builder to insert Disc N/ subfolder for multi-disc albums
  - Preserve disc_number when tracks fail and get re-added to wishlist
  - Preserve disc_number when adding tracks to wishlist from library page
  - Add visual disc separators in Soulseek search result track lists
main
Broque Thomas 2 days ago
parent 96f991a833
commit d1259c4b62

@ -1126,6 +1126,7 @@ class WatchlistScanner:
track_popularity = track.get('popularity', 0)
track_preview_url = track.get('preview_url', None)
track_number = track.get('track_number', 1)
disc_number = track.get('disc_number', 1)
track_uri = track.get('uri', '')
else:
track_id = track.id
@ -1137,6 +1138,7 @@ class WatchlistScanner:
track_popularity = getattr(track, 'popularity', 0)
track_preview_url = getattr(track, 'preview_url', None)
track_number = getattr(track, 'track_number', 1)
disc_number = getattr(track, 'disc_number', 1)
track_uri = getattr(track, 'uri', '')
if isinstance(album, dict):
@ -1173,6 +1175,7 @@ class WatchlistScanner:
'popularity': track_popularity,
'preview_url': track_preview_url,
'track_number': track_number,
'disc_number': disc_number,
'uri': track_uri,
'is_local': False
}

@ -107,39 +107,7 @@ class WishlistService:
try:
wishlist_tracks = self.database.get_wishlist_tracks(limit=limit)
formatted_tracks = []
# Sort by artist name, then track name for consistent display order
try:
def get_sort_key(track):
spotify_data = track['spotify_data']
# Parse JSON string if needed
if isinstance(spotify_data, str):
import json
try:
spotify_data = json.loads(spotify_data)
except:
return ('', '') # Fallback for invalid JSON
artist_name = ''
track_name = ''
if isinstance(spotify_data, dict):
artists = spotify_data.get('artists', [])
if artists and len(artists) > 0:
if isinstance(artists[0], dict):
artist_name = artists[0].get('name', '')
elif isinstance(artists[0], str):
artist_name = artists[0]
track_name = spotify_data.get('name', '')
return (artist_name.lower(), track_name.lower())
wishlist_tracks.sort(key=get_sort_key)
logger.debug(f"Successfully sorted {len(wishlist_tracks)} wishlist tracks by artist/track name")
except Exception as sort_error:
logger.warning(f"Failed to sort wishlist tracks, using original order: {sort_error}")
# Continue with original database order (date_added)
for wishlist_track in wishlist_tracks:
spotify_data = wishlist_track['spotify_data']
@ -394,7 +362,9 @@ class WishlistService:
'duration_ms': getattr(spotify_track, 'duration_ms', 0),
'preview_url': getattr(spotify_track, 'preview_url', None),
'external_urls': getattr(spotify_track, 'external_urls', {}),
'popularity': getattr(spotify_track, 'popularity', 0)
'popularity': getattr(spotify_track, 'popularity', 0),
'track_number': getattr(spotify_track, 'track_number', 1),
'disc_number': getattr(spotify_track, 'disc_number', 1)
}
logger.info(f"DEBUG: Spotify Track converted: {result['name']} by {[a['name'] for a in result['artists']]}")

@ -11326,6 +11326,7 @@ def add_album_track_to_wishlist():
},
'duration_ms': track.get('duration_ms', 0),
'track_number': track.get('track_number', 1),
'disc_number': track.get('disc_number', 1),
'explicit': track.get('explicit', False),
'popularity': track.get('popularity', 0),
'preview_url': track.get('preview_url'),
@ -12310,6 +12311,8 @@ def _ensure_spotify_track_format(track_info):
'artists': artists_list, # Proper Spotify format
'album': album,
'duration_ms': track_info.get('duration_ms', 0),
'track_number': track_info.get('track_number', 1),
'disc_number': track_info.get('disc_number', 1),
'preview_url': track_info.get('preview_url'),
'external_urls': track_info.get('external_urls', {}),
'popularity': track_info.get('popularity', 0),
@ -12892,6 +12895,27 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
if total_discs > 1:
print(f"💿 [Multi-Disc] Detected {total_discs} discs for album '{batch_album_context.get('name')}'")
# Pre-compute total_discs per album for wishlist tracks (grouped by album ID)
# Wishlist tracks aren't batch_is_album but each track has disc_number in spotify_data
wishlist_album_disc_counts = {}
if playlist_id == 'wishlist':
import json as _json
# First pass: collect disc_number from stored spotify_data
for t in tracks_json:
sp_data = t.get('spotify_data', {})
if isinstance(sp_data, str):
try:
sp_data = _json.loads(sp_data)
except:
sp_data = {}
album_id = (sp_data.get('album', {}) or {}).get('id')
disc_num = sp_data.get('disc_number', t.get('disc_number', 1))
if album_id:
wishlist_album_disc_counts[album_id] = max(
wishlist_album_disc_counts.get(album_id, 1), disc_num
)
for res in missing_tracks:
task_id = str(uuid.uuid4())
track_info = res['track'].copy()
@ -12932,11 +12956,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
# Construct minimal album context
# Ensure images are preserved (important for artwork)
album_id = s_album.get('id', 'wishlist_album')
album_ctx = {
'id': s_album.get('id', 'wishlist_album'),
'id': album_id,
'name': s_album.get('name'),
'release_date': s_album.get('release_date', ''),
'total_tracks': s_album.get('total_tracks', 1),
'total_discs': wishlist_album_disc_counts.get(album_id, 1),
'album_type': s_album.get('album_type', 'album'),
'images': s_album.get('images', []) # Pass images array directly
}

Loading…
Cancel
Save