diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 3985ff3..9c28852 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -2000,6 +2000,33 @@ class DownloadQueueNotifier extends Notifier { state = state.copyWith(outputDir: dir); } + bool _shouldTreatAsSingleRelease(Track track) { + if (track.isSingle) { + return true; + } + + final normalizedAlbumType = normalizeOptionalString( + track.albumType, + )?.toLowerCase(); + if (normalizedAlbumType != null && normalizedAlbumType.isNotEmpty) { + return false; + } + + final totalTracks = track.totalTracks; + if (totalTracks == 1) { + return true; + } + + final normalizedAlbumName = normalizeOptionalString( + track.albumName, + )?.toLowerCase(); + if (normalizedAlbumName == 'single' || normalizedAlbumName == 'singles') { + return totalTracks == null || totalTracks <= 2; + } + + return false; + } + Future _buildOutputDir( Track track, String folderOrganization, { @@ -2036,7 +2063,7 @@ class DownloadQueueNotifier extends Notifier { } if (separateSingles) { - final isSingle = track.isSingle; + final isSingle = _shouldTreatAsSingleRelease(track); final artistName = _sanitizeFolderName(folderArtist); if (albumFolderStructure == 'artist_album_singles') { @@ -2215,7 +2242,7 @@ class DownloadQueueNotifier extends Notifier { } if (separateSingles) { - final isSingle = track.isSingle; + final isSingle = _shouldTreatAsSingleRelease(track); final artistName = _sanitizeFolderName(folderArtist); if (albumFolderStructure == 'artist_album_singles') { @@ -4137,7 +4164,7 @@ class DownloadQueueNotifier extends Notifier { String? safBaseName; String safOutputExt = _determineOutputExt(quality, item.service); if (isSafMode) { - final effectiveFormat = trackToDownload.isSingle + final effectiveFormat = _shouldTreatAsSingleRelease(trackToDownload) ? state.singleFilenameFormat : state.filenameFormat; final baseName = await PlatformBridge.buildFilename(effectiveFormat, { @@ -4552,7 +4579,7 @@ class DownloadQueueNotifier extends Notifier { ? (trackToDownload.coverUrl ?? '') : '', outputDir: outputDir, - filenameFormat: trackToDownload.isSingle + filenameFormat: _shouldTreatAsSingleRelease(trackToDownload) ? state.singleFilenameFormat : state.filenameFormat, quality: quality, diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 99a9e9e..b1786dc 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -908,7 +908,7 @@ class TrackNotifier extends Notifier { discNumber: data['disc_number'] as int?, totalDiscs: data['total_discs'] as int?, releaseDate: data['release_date'] as String?, - albumType: data['album_type'] as String?, + albumType: normalizeOptionalString(data['album_type']?.toString()), totalTracks: data['total_tracks'] as int?, composer: data['composer']?.toString(), ); @@ -945,7 +945,7 @@ class TrackNotifier extends Notifier { releaseDate: data['release_date']?.toString(), totalTracks: data['total_tracks'] as int?, source: effectiveSource, - albumType: data['album_type']?.toString(), + albumType: normalizeOptionalString(data['album_type']?.toString()), composer: data['composer']?.toString(), itemType: itemType, ); diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index a08b1e9..fdde453 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -75,6 +75,8 @@ class _AlbumScreenState extends ConsumerState { String? _error; bool _showTitleInAppBar = false; String? _artistId; + String? _albumType; + int? _albumTotalTracks; final ScrollController _scrollController = ScrollController(); @override @@ -112,6 +114,8 @@ class _AlbumScreenState extends ConsumerState { _tracks = _AlbumCache.get(widget.albumId); } _artistId = widget.artistId; + _albumType = _tracks?.firstOrNull?.albumType; + _albumTotalTracks = _tracks?.firstOrNull?.totalTracks; if (_tracks == null || _tracks!.isEmpty) { _fetchTracks(); @@ -179,13 +183,22 @@ class _AlbumScreenState extends ConsumerState { deezerAlbumId, ); final trackList = metadata['track_list'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final albumInfo = metadata['album_info'] as Map?; final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId']) ?.toString(); + final albumType = normalizeOptionalString( + albumInfo?['album_type']?.toString(), + ); + final totalTracks = albumInfo?['total_tracks'] as int?; + final tracks = trackList + .map( + (t) => _parseTrack( + t as Map, + albumTypeFallback: albumType, + totalTracksFallback: totalTracks, + ), + ) + .toList(); _AlbumCache.set(widget.albumId, tracks); @@ -193,6 +206,8 @@ class _AlbumScreenState extends ConsumerState { setState(() { _tracks = tracks; _artistId = artistId; + _albumType = albumType; + _albumTotalTracks = totalTracks; _isLoading = false; }); } @@ -204,13 +219,22 @@ class _AlbumScreenState extends ConsumerState { qobuzAlbumId, ); final trackList = metadata['track_list'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final albumInfo = metadata['album_info'] as Map?; final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId']) ?.toString(); + final albumType = normalizeOptionalString( + albumInfo?['album_type']?.toString(), + ); + final totalTracks = albumInfo?['total_tracks'] as int?; + final tracks = trackList + .map( + (t) => _parseTrack( + t as Map, + albumTypeFallback: albumType, + totalTracksFallback: totalTracks, + ), + ) + .toList(); _AlbumCache.set(widget.albumId, tracks); @@ -218,6 +242,8 @@ class _AlbumScreenState extends ConsumerState { setState(() { _tracks = tracks; _artistId = artistId; + _albumType = albumType; + _albumTotalTracks = totalTracks; _isLoading = false; }); } @@ -229,13 +255,22 @@ class _AlbumScreenState extends ConsumerState { tidalAlbumId, ); final trackList = metadata['track_list'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final albumInfo = metadata['album_info'] as Map?; final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId']) ?.toString(); + final albumType = normalizeOptionalString( + albumInfo?['album_type']?.toString(), + ); + final totalTracks = albumInfo?['total_tracks'] as int?; + final tracks = trackList + .map( + (t) => _parseTrack( + t as Map, + albumTypeFallback: albumType, + totalTracksFallback: totalTracks, + ), + ) + .toList(); _AlbumCache.set(widget.albumId, tracks); @@ -243,6 +278,8 @@ class _AlbumScreenState extends ConsumerState { setState(() { _tracks = tracks; _artistId = artistId; + _albumType = albumType; + _albumTotalTracks = totalTracks; _isLoading = false; }); } @@ -255,13 +292,22 @@ class _AlbumScreenState extends ConsumerState { } final trackList = result['tracks'] as List; - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final albumInfo = result['album'] as Map?; final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId']) ?.toString(); + final albumType = normalizeOptionalString( + albumInfo?['album_type']?.toString(), + ); + final totalTracks = albumInfo?['total_tracks'] as int?; + final tracks = trackList + .map( + (t) => _parseTrack( + t as Map, + albumTypeFallback: albumType, + totalTracksFallback: totalTracks, + ), + ) + .toList(); _AlbumCache.set(widget.albumId, tracks); @@ -269,6 +315,8 @@ class _AlbumScreenState extends ConsumerState { setState(() { _tracks = tracks; _artistId = artistId; + _albumType = albumType; + _albumTotalTracks = totalTracks; _isLoading = false; }); } @@ -284,7 +332,11 @@ class _AlbumScreenState extends ConsumerState { } } - Track _parseTrack(Map data) { + Track _parseTrack( + Map data, { + String? albumTypeFallback, + int? totalTracksFallback, + }) { return Track( id: data['spotify_id'] as String? ?? '', name: data['name'] as String? ?? '', @@ -301,8 +353,14 @@ class _AlbumScreenState extends ConsumerState { discNumber: data['disc_number'] as int?, totalDiscs: data['total_discs'] as int?, releaseDate: data['release_date'] as String?, - albumType: data['album_type'] as String?, - totalTracks: data['total_tracks'] as int?, + albumType: + normalizeOptionalString(data['album_type']?.toString()) ?? + albumTypeFallback ?? + _albumType, + totalTracks: + data['total_tracks'] as int? ?? + totalTracksFallback ?? + _albumTotalTracks, composer: data['composer']?.toString(), ); } diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 7f61c30..165c4b0 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -412,7 +412,9 @@ class _ArtistScreenState extends ConsumerState { discNumber: data['disc_number'] as int?, totalDiscs: data['total_discs'] as int?, releaseDate: data['release_date']?.toString(), - albumType: data['album_type']?.toString() ?? album?.albumType, + albumType: + normalizeOptionalString(data['album_type']?.toString()) ?? + album?.albumType, totalTracks: data['total_tracks'] as int? ?? album?.totalTracks, composer: data['composer']?.toString(), source: data['provider_id']?.toString() ?? widget.extensionId, @@ -1057,9 +1059,10 @@ class _ArtistScreenState extends ConsumerState { ); if (result != null && result['tracks'] != null) { final tracksList = result['tracks'] as List; - return tracksList + final parsedTracks = tracksList .map((t) => _parseTrack(t as Map, album: album)) .toList(); + return parsedTracks; } } else if (album.id.startsWith('deezer:')) { final deezerId = album.id.replaceFirst('deezer:', ''); @@ -1934,6 +1937,8 @@ class _ArtistScreenState extends ConsumerState { albumId: album.id, albumName: album.name, coverUrl: album.coverUrl, + initialAlbumType: album.albumType, + initialTotalTracks: album.totalTracks, ), ), ); diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index fc79037..439cdad 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -3031,6 +3031,8 @@ class _HomeTabState extends ConsumerState albumId: albumItem.id, albumName: albumItem.name, coverUrl: albumItem.coverUrl, + initialAlbumType: albumItem.albumType, + initialTotalTracks: albumItem.totalTracks, ), ), ); @@ -4315,6 +4317,8 @@ class ExtensionAlbumScreen extends ConsumerStatefulWidget { final String albumId; final String albumName; final String? coverUrl; + final String? initialAlbumType; + final int? initialTotalTracks; const ExtensionAlbumScreen({ super.key, @@ -4322,6 +4326,8 @@ class ExtensionAlbumScreen extends ConsumerStatefulWidget { required this.albumId, required this.albumName, this.coverUrl, + this.initialAlbumType, + this.initialTotalTracks, }); @override @@ -4335,10 +4341,14 @@ class _ExtensionAlbumScreenState extends ConsumerState { String? _error; String? _artistId; String? _artistName; + String? _albumType; + int? _albumTotalTracks; @override void initState() { super.initState(); + _albumType = normalizeOptionalString(widget.initialAlbumType); + _albumTotalTracks = widget.initialTotalTracks; _fetchTracks(); } @@ -4372,17 +4382,28 @@ class _ExtensionAlbumScreenState extends ConsumerState { return; } - final tracks = trackList - .map((t) => _parseTrack(t as Map)) - .toList(); - final artistId = (result['artist_id'] ?? result['artistId'])?.toString(); final artistName = result['artists'] as String?; + final albumType = + normalizeOptionalString(result['album_type']?.toString()) ?? + _albumType; + final totalTracks = result['total_tracks'] as int? ?? _albumTotalTracks; + final tracks = trackList + .map( + (t) => _parseTrack( + t as Map, + albumTypeFallback: albumType, + totalTracksFallback: totalTracks, + ), + ) + .toList(); setState(() { _tracks = tracks; _artistId = artistId; _artistName = artistName; + _albumType = albumType; + _albumTotalTracks = totalTracks; _isLoading = false; }); } catch (e) { @@ -4394,7 +4415,11 @@ class _ExtensionAlbumScreenState extends ConsumerState { } } - Track _parseTrack(Map data) { + Track _parseTrack( + Map data, { + String? albumTypeFallback, + int? totalTracksFallback, + }) { int durationMs = 0; final durationValue = data['duration_ms']; if (durationValue is int) { @@ -4422,7 +4447,14 @@ class _ExtensionAlbumScreenState extends ConsumerState { discNumber: data['disc_number'] as int?, totalDiscs: data['total_discs'] as int?, releaseDate: data['release_date']?.toString(), - totalTracks: data['total_tracks'] as int?, + albumType: + normalizeOptionalString(data['album_type']?.toString()) ?? + albumTypeFallback ?? + _albumType, + totalTracks: + data['total_tracks'] as int? ?? + totalTracksFallback ?? + _albumTotalTracks, composer: data['composer']?.toString(), source: widget.extensionId, );