From 1d363d516654fe67ce24556a170c6a28d9925625 Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 11 Aug 2026 17:02:17 +0700 Subject: [PATCH] fix(playlist): ignore container title during album verification #519 --- lib/models/track.dart | 17 +++++++++++++++-- lib/providers/track_provider.dart | 6 ++++-- lib/screens/home_tab_widgets.dart | 8 ++++++-- lib/screens/playlist_screen.dart | 13 ++++++++++--- test/models_and_utils_test.dart | 20 ++++++++++++++++++++ 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/lib/models/track.dart b/lib/models/track.dart index a1216f2b..be3b1c5b 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -82,7 +82,11 @@ class Track { Map toJson() => _$TrackToJson(this); /// Builds a [Track] from a backend/extension search payload map. - factory Track.fromBackendMap(Map data, {String? source}) { + factory Track.fromBackendMap( + Map data, { + String? source, + String? playlistName, + }) { final durationMs = extractDurationMs(data); final itemType = data['item_type']?.toString(); @@ -93,12 +97,21 @@ class Track { final preferredId = effectiveSource != null && effectiveSource.isNotEmpty ? (nativeId.isNotEmpty ? nativeId : spotifyId) : (spotifyId.isNotEmpty ? spotifyId : nativeId); + final rawAlbumName = (data['album_name'] ?? data['album'] ?? '').toString(); + final normalizedAlbumName = normalizeOptionalString(rawAlbumName); + final normalizedPlaylistName = normalizeOptionalString(playlistName); + final albumName = + normalizedPlaylistName != null && + normalizedAlbumName?.toLowerCase() == + normalizedPlaylistName.toLowerCase() + ? '' + : rawAlbumName; return Track( id: preferredId, name: (data['name'] ?? '').toString(), artistName: (data['artists'] ?? data['artist'] ?? '').toString(), - albumName: (data['album_name'] ?? data['album'] ?? '').toString(), + albumName: albumName, albumArtist: data['album_artist']?.toString(), artistId: (data['artist_id'] ?? data['artistId'])?.toString(), albumId: data['album_id']?.toString(), diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index 530020df..e8c3d99a 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -224,11 +224,13 @@ class TrackNotifier extends Notifier { } else if ((type == 'album' || type == 'playlist') && result['tracks'] != null) { final trackList = result['tracks'] as List; + final collectionName = result['name'] as String?; final tracks = trackList .map( (t) => Track.fromBackendMap( t as Map, source: extensionId, + playlistName: type == 'playlist' ? collectionName : null, ), ) .toList(); @@ -238,9 +240,9 @@ class TrackNotifier extends Notifier { albumId: (result['album'] as Map?)?['id'] as String?, albumName: - result['name'] as String? ?? + collectionName ?? (result['album'] as Map?)?['name'] as String?, - playlistName: type == 'playlist' ? result['name'] as String? : null, + playlistName: type == 'playlist' ? collectionName : null, coverUrl: normalizeCoverReference(result['cover_url']?.toString()), headerVideoUrl: normalizeRemoteHttpUrl( result['header_video']?.toString(), diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index e2bdfbf5..4be5a2e9 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -1155,12 +1155,16 @@ class _ExtensionPlaylistScreenState } Track _parseTrack(Map data) { - final base = Track.fromBackendMap(data, source: widget.extensionId); + final base = Track.fromBackendMap( + data, + source: widget.extensionId, + playlistName: widget.playlistName, + ); return Track( id: (data['id'] ?? '').toString(), name: base.name, artistName: base.artistName, - albumName: (data['album_name'] ?? '').toString(), + albumName: base.albumName, artistId: base.artistId, albumId: base.albumId, coverUrl: _resolveTrackCoverUrl( diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 73dc37d5..334145c4 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -207,18 +207,25 @@ class _PlaylistScreenState extends ConsumerState final playlistInfo = result['playlist_info'] as Map?; final owner = playlistInfo?['owner'] as Map?; + final resolvedPlaylistName = + (playlistInfo?['name'] ?? owner?['name'])?.toString() ?? + _playlistName; final trackList = result['track_list'] as List? ?? []; final tracks = trackList - .map((t) => Track.fromBackendMap(t as Map)) + .map( + (t) => Track.fromBackendMap( + t as Map, + playlistName: resolvedPlaylistName, + ), + ) .toList(); final headerVideo = playlistInfo?['header_video']?.toString(); setState(() { _fetchedTracks = tracks; - _resolvedPlaylistName = (playlistInfo?['name'] ?? owner?['name']) - ?.toString(); + _resolvedPlaylistName = resolvedPlaylistName; _resolvedCoverUrl = (playlistInfo?['images'] ?? owner?['images']) ?.toString(); _resolvedHeaderVideoUrl = diff --git a/test/models_and_utils_test.dart b/test/models_and_utils_test.dart index b0ff77aa..de89cd28 100644 --- a/test/models_and_utils_test.dart +++ b/test/models_and_utils_test.dart @@ -443,6 +443,26 @@ void main() { expect(track.isExplicit, isTrue); }); + test('does not treat a playlist container name as a track album', () { + final playlistTrack = Track.fromBackendMap({ + 'id': 'playlist-track-1', + 'name': 'Song', + 'artists': 'Artist', + 'album_name': ' Road Trip ', + 'duration_ms': 180000, + }, playlistName: 'road trip'); + final releaseTrack = Track.fromBackendMap({ + 'id': 'playlist-track-2', + 'name': 'Song', + 'artists': 'Artist', + 'album_name': 'Real Album', + 'duration_ms': 180000, + }, playlistName: 'Road Trip'); + + expect(playlistTrack.albumName, isEmpty); + expect(releaseTrack.albumName, 'Real Album'); + }); + test('exposes collection, source, and quality flags', () { const album = Track( id: 'album-1',