diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index cfc82f0a..f94b853b 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -320,6 +320,13 @@ class _AlbumScreenState extends ConsumerState totalTracksFallback ?? _albumTotalTracks, composer: data['composer']?.toString(), + genre: data['genre']?.toString(), + label: data['label']?.toString(), + copyright: data['copyright']?.toString(), + comment: data['comment']?.toString(), + upc: normalizeOptionalString( + (data['upc'] ?? data['barcode'])?.toString(), + ), audioQuality: data['audio_quality']?.toString(), audioModes: data['audio_modes']?.toString(), previewUrl: data['preview_url']?.toString(), diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 677d0928..a821270d 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -428,6 +428,15 @@ class _ArtistScreenState extends ConsumerState album?.albumType, totalTracks: data['total_tracks'] as int? ?? album?.totalTracks, composer: data['composer']?.toString(), + genre: data['genre']?.toString(), + label: data['label']?.toString(), + copyright: data['copyright']?.toString(), + comment: data['comment']?.toString(), + upc: normalizeOptionalString( + (data['upc'] ?? data['barcode'])?.toString(), + ), + audioQuality: data['audio_quality']?.toString(), + audioModes: data['audio_modes']?.toString(), source: data['provider_id']?.toString() ?? widget.extensionId, previewUrl: data['preview_url']?.toString(), explicit: parseExplicitFlag(data['explicit']), diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index 27febbbc..a08afdfd 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -1018,10 +1018,8 @@ class _ExtensionAlbumScreenState extends ConsumerState { int? totalTracksFallback, }) { final base = Track.fromBackendMap(data, source: widget.extensionId); - return Track( + return base.copyWith( id: (data['id'] ?? '').toString(), - name: base.name, - artistName: base.artistName, albumName: (data['album_name'] ?? widget.albumName).toString(), albumArtist: normalizeOptionalString(data['album_artist']?.toString()), artistId: base.artistId ?? _artistId, @@ -1030,20 +1028,9 @@ class _ExtensionAlbumScreenState extends ConsumerState { data['cover_url']?.toString(), widget.coverUrl, ), - isrc: base.isrc, duration: (_legacyTrackDurationMs(data) / 1000).round(), - trackNumber: base.trackNumber, - discNumber: base.discNumber, - totalDiscs: base.totalDiscs, - releaseDate: base.releaseDate, albumType: base.albumType ?? albumTypeFallback ?? _albumType, totalTracks: base.totalTracks ?? totalTracksFallback ?? _albumTotalTracks, - composer: base.composer, - source: base.source, - audioQuality: base.audioQuality, - audioModes: base.audioModes, - previewUrl: base.previewUrl, - explicit: base.explicit, ); } @@ -1170,30 +1157,13 @@ class _ExtensionPlaylistScreenState source: widget.extensionId, playlistName: widget.playlistName, ); - return Track( + return base.copyWith( id: (data['id'] ?? '').toString(), - name: base.name, - artistName: base.artistName, - albumName: base.albumName, - artistId: base.artistId, - albumId: base.albumId, coverUrl: _resolveTrackCoverUrl( data['cover_url']?.toString(), widget.coverUrl, ), - isrc: base.isrc, duration: (_legacyTrackDurationMs(data) / 1000).round(), - trackNumber: base.trackNumber, - discNumber: base.discNumber, - totalDiscs: base.totalDiscs, - releaseDate: base.releaseDate, - totalTracks: base.totalTracks, - composer: base.composer, - source: base.source, - audioQuality: base.audioQuality, - audioModes: base.audioModes, - previewUrl: base.previewUrl, - explicit: base.explicit, ); } @@ -1341,26 +1311,11 @@ class _ExtensionArtistScreenState extends ConsumerState Track _parseTrack(Map data) { final base = Track.fromBackendMap(data); - return Track( + return base.copyWith( id: (data['id'] ?? data['spotify_id'] ?? '').toString(), - name: base.name, - artistName: base.artistName, - albumName: base.albumName, - albumArtist: base.albumArtist, artistId: base.artistId ?? widget.artistId, - albumId: base.albumId, - coverUrl: base.coverUrl, - isrc: base.isrc, duration: (_legacyTrackDurationMs(data) / 1000).round(), - trackNumber: base.trackNumber, - discNumber: base.discNumber, - totalDiscs: base.totalDiscs, - releaseDate: base.releaseDate, - totalTracks: base.totalTracks, - composer: base.composer, source: (data['provider_id'] ?? widget.extensionId).toString(), - previewUrl: base.previewUrl, - explicit: base.explicit, ); } diff --git a/test/album_track_metadata_test.dart b/test/album_track_metadata_test.dart new file mode 100644 index 00000000..ee5c177a --- /dev/null +++ b/test/album_track_metadata_test.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; +import 'package:spotiflac_android/models/track.dart'; +import 'package:spotiflac_android/screens/album_screen.dart'; +import 'package:spotiflac_android/widgets/track_list_tile.dart'; + +void main() { + testWidgets('album tracks retain the extended tags supplied in search', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + await tester.binding.setSurfaceSize(const Size(430, 1200)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + const channel = MethodChannel('com.zarz.spotiflac/backend'); + const payload = { + 'id': 'song-1', + 'spotify_id': 'song-1', + 'name': 'Example Song', + 'artists': 'Example Artist', + 'album_name': 'Example Album', + 'duration_ms': 180000, + 'track_number': 1, + 'genre': 'Example Genre', + 'label': 'Example Label', + 'copyright': 'Example Copyright', + 'comment': 'Example Comment', + 'upc': '0123456789012', + }; + var albumRequested = false; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + if (call.method == 'getProviderMetadata') { + expect(call.arguments, { + 'provider_id': 'example-metadata', + 'resource_type': 'album', + 'resource_id': 'metadata-album', + }); + albumRequested = true; + return jsonEncode({ + 'track_list': [payload], + 'album_info': { + 'name': 'Example Album', + 'total_tracks': 1, + 'album_type': 'album', + }, + }); + } + return null; + }); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + final searchTrack = Track.fromBackendMap(payload); + expect(searchTrack.genre, 'Example Genre'); + expect(searchTrack.label, 'Example Label'); + expect(searchTrack.copyright, 'Example Copyright'); + + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: const AlbumScreen( + albumId: 'metadata-album', + albumName: 'Example Album', + extensionId: 'example-metadata', + ), + ), + ), + ); + await tester.pumpAndSettle(); + expect(albumRequested, isTrue); + final albumTrack = tester + .widget(find.byType(TrackListTile).first) + .track; + expect(albumTrack.id, searchTrack.id); + expect(albumTrack.name, searchTrack.name); + expect(albumTrack.genre, searchTrack.genre); + expect(albumTrack.label, searchTrack.label); + expect(albumTrack.copyright, searchTrack.copyright); + expect(albumTrack.comment, searchTrack.comment); + expect(albumTrack.upc, searchTrack.upc); + expect(albumTrack.albumId, 'metadata-album'); + expect(albumTrack.albumType, 'album'); + expect(albumTrack.totalTracks, 1); + await tester.pumpWidget(const SizedBox()); + await tester.pumpAndSettle(); + }); +} diff --git a/test/collection_track_metadata_test.dart b/test/collection_track_metadata_test.dart new file mode 100644 index 00000000..098a1d4b --- /dev/null +++ b/test/collection_track_metadata_test.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; +import 'package:spotiflac_android/screens/artist_screen.dart'; +import 'package:spotiflac_android/screens/home_tab.dart'; +import 'package:spotiflac_android/screens/playlist_screen.dart'; +import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; + +void main() { + final screens = { + 'artist': const ArtistScreen( + artistId: 'direct-artist', + artistName: 'Example Artist', + extensionId: 'example-metadata', + ), + 'extension album': const ExtensionAlbumScreen( + extensionId: 'example-metadata', + albumId: 'extension-album', + albumName: 'Example Album', + ), + 'extension playlist': const ExtensionPlaylistScreen( + extensionId: 'example-metadata', + playlistId: 'extension-playlist', + playlistName: 'Example Playlist', + ), + 'extension artist': const ExtensionArtistScreen( + extensionId: 'example-metadata', + artistId: 'extension-artist', + artistName: 'Example Artist', + ), + 'playlist': const PlaylistScreen( + playlistId: 'direct-playlist', + playlistName: 'Example Playlist', + metadataProviderId: 'example-metadata', + tracks: [], + ), + }; + + for (final entry in screens.entries) { + testWidgets('${entry.key} preserves supplied track metadata', ( + tester, + ) async { + SharedPreferences.setMockInitialValues({}); + await tester.binding.setSurfaceSize(const Size(800, 1600)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + addTearDown(() async { + await tester.pumpWidget(const SizedBox()); + // Flush the debounced platform metadata cache before the next case. + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + }); + const payload = { + 'id': 'example-song', + 'spotify_id': 'example-song', + 'provider_id': 'example-metadata', + 'name': 'Example Song', + 'artists': 'Example Artist', + 'album_name': 'Example Album', + 'album_artist': 'Example Album Artist', + 'album_type': 'album', + 'duration_ms': 180000, + 'track_number': 1, + 'genre': 'Example Genre', + 'label': 'Example Label', + 'copyright': 'Example Copyright', + 'comment': 'Example Comment', + 'upc': '0123456789012', + 'audio_quality': '24-bit/96000Hz', + 'audio_modes': 'STEREO', + }; + const channel = MethodChannel('com.zarz.spotiflac/backend'); + var requested = false; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + if (call.method == 'getProviderMetadata') { + requested = true; + return jsonEncode({ + 'track_list': [payload], + 'top_tracks': [payload], + 'albums': >[], + 'album_info': {'name': 'Example Album', 'total_tracks': 1}, + 'playlist_info': {'name': 'Example Playlist'}, + 'artist_info': {'name': 'Example Artist'}, + }); + } + return null; + }); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + await PlatformBridge.clearTrackCache(); + await tester.pumpWidget( + ProviderScope( + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: entry.value, + ), + ), + ); + await tester.pumpAndSettle(); + expect(requested, isTrue); + final track = tester + .widget( + find.byType(TrackCollectionQuickActions).first, + ) + .track; + expect(track.id, 'example-song'); + expect(track.duration, 180); + expect(track.genre, payload['genre']); + expect(track.label, payload['label']); + expect(track.copyright, payload['copyright']); + expect(track.comment, payload['comment']); + expect(track.upc, payload['upc']); + expect(track.albumArtist, payload['album_artist']); + expect(track.albumType, payload['album_type']); + expect(track.audioQuality, payload['audio_quality']); + expect(track.audioModes, payload['audio_modes']); + await tester.pumpWidget(const SizedBox()); + await PlatformBridge.clearTrackCache(); + await tester.pumpAndSettle(); + }); + } +}