fix(metadata): preserve track tags across collection screens

This commit is contained in:
zarzet
2026-09-07 18:01:37 +07:00
parent c27dc7c1b6
commit cadd60ce8d
5 changed files with 247 additions and 48 deletions
+7
View File
@@ -320,6 +320,13 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
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(),
+9
View File
@@ -428,6 +428,15 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen>
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']),
+3 -48
View File
@@ -1018,10 +1018,8 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
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<ExtensionAlbumScreen> {
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<ExtensionArtistScreen>
Track _parseTrack(Map<String, dynamic> 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,
);
}
+97
View File
@@ -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 = <String, dynamic>{
'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<TrackListTile>(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();
});
}
+131
View File
@@ -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 = <String, Widget>{
'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 = <String, dynamic>{
'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': <Map<String, dynamic>>[],
'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<TrackCollectionQuickActions>(
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();
});
}
}