fix(download): align native metadata preparation and ReplayGain support

This commit is contained in:
zarzet
2026-09-28 12:21:15 +07:00
parent c0997de19c
commit e61e54f93f
11 changed files with 288 additions and 117 deletions
@@ -44,6 +44,7 @@ import 'package:spotiflac_android/utils/progress_stream_poller.dart';
import 'package:spotiflac_android/providers/download_history_provider.dart';
import 'package:spotiflac_android/services/native_download_history.dart';
import 'package:spotiflac_android/services/download_track_metadata.dart';
export 'package:spotiflac_android/providers/download_history_provider.dart';
export 'package:spotiflac_android/providers/download_queue_state.dart';
@@ -22,6 +22,13 @@ class _DeezerExtendedMetadataFields {
}
extension _DownloadQueueEmbedding on DownloadQueueNotifier {
Future<Track> _prepareDownloadSourceTrack(Track track) async {
if (!track.id.startsWith('deezer:')) return track;
final id = track.id.substring('deezer:'.length);
final enriched = await enrichIncompleteDownloadTrack(track, 'deezer', id);
return identical(enriched, track) ? track : enriched.copyWith(deezerId: id);
}
Future<Track> _resolveDownloadAlbumCredit(
Track track,
AppSettings settings,
@@ -877,8 +877,9 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
return null;
}
final sourceTrack = await _prepareDownloadSourceTrack(item.track);
item = item.copyWith(
track: await _resolveDownloadAlbumCredit(item.track, settings),
track: await _resolveDownloadAlbumCredit(sourceTrack, settings),
);
final isSafMode = _isSafMode(settings);
@@ -915,12 +916,6 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
}
final outputExt = _determineOutputExt(quality, item.service);
if (settings.embedReplayGain &&
outputExt != '.flac' &&
outputExt != '.m4a') {
return null;
}
String? safFileName;
final safOutputExt = isSafMode ? outputExt : '';
final baseFilenameFormat = _shouldTreatAsSingleRelease(item.track)
@@ -992,7 +987,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
).withStrategy(useExtensions: true, useFallback: state.autoFallback);
return _NativeWorkerRequestContext(
item: item,
item: item.copyWith(track: trackForPayload),
requestJson: jsonEncode(payload.toJson()),
outputDir: outputDir,
quality: quality,
@@ -307,107 +307,8 @@ class _DownloadRun {
}
Future<bool> _enrichDeezerTrackIfNeeded() async {
final needsEnrichment =
trackToDownload.id.startsWith('deezer:') &&
(trackToDownload.isrc == null ||
trackToDownload.isrc!.isEmpty ||
trackToDownload.trackNumber == null ||
trackToDownload.trackNumber == 0 ||
trackToDownload.totalTracks == null ||
trackToDownload.totalTracks == 0 ||
(trackToDownload.composer == null ||
trackToDownload.composer!.isEmpty));
if (needsEnrichment) {
try {
_log.d(
'Enriching incomplete metadata for Deezer track: ${trackToDownload.name}',
);
_log.d(
'Current ISRC: ${trackToDownload.isrc}, TrackNumber: ${trackToDownload.trackNumber}',
);
final rawId = trackToDownload.id.split(':')[1];
_log.d('Fetching full metadata for Deezer ID: $rawId');
final fullData = await PlatformBridge.getProviderMetadata(
'deezer',
'track',
rawId,
);
_log.d('Got response keys: ${fullData.keys.toList()}');
if (fullData.containsKey('track')) {
final trackData = fullData['track'];
_log.d('Track data type: ${trackData.runtimeType}');
if (trackData is Map<String, dynamic>) {
final data = trackData;
_log.d('Track data keys: ${data.keys.toList()}');
_log.d('ISRC from API: ${data['isrc']}');
_log.d('album_type from API: ${data['album_type']}');
final enrichedTotalTracks = readPositiveInt(data['total_tracks']);
final enrichedTotalDiscs = readPositiveInt(data['total_discs']);
final enrichedComposer = normalizeOptionalString(
data['composer']?.toString(),
);
trackToDownload = trackToDownload.copyWith(
id: (data['spotify_id'] as String?) ?? trackToDownload.id,
name: (data['name'] as String?) ?? trackToDownload.name,
artistName:
(data['artists'] as String?) ?? trackToDownload.artistName,
albumName:
(data['album_name'] as String?) ?? trackToDownload.albumName,
albumArtist: data['album_artist'] as String?,
artistId:
(data['artist_id'] ?? data['artistId'])?.toString() ??
trackToDownload.artistId,
albumId: data['album_id']?.toString() ?? trackToDownload.albumId,
coverUrl: data['images'] as String?,
duration:
((data['duration_ms'] as int?) ??
(trackToDownload.duration * 1000)) ~/
1000,
isrc: (data['isrc'] as String?) ?? trackToDownload.isrc,
trackNumber: data['track_number'] as int?,
discNumber: data['disc_number'] as int?,
totalDiscs: enrichedTotalDiscs ?? trackToDownload.totalDiscs,
releaseDate: data['release_date'] as String?,
deezerId: rawId,
albumType:
(data['album_type'] as String?) ?? trackToDownload.albumType,
totalTracks: enrichedTotalTracks ?? trackToDownload.totalTracks,
composer: enrichedComposer ?? trackToDownload.composer,
genre: data['genre']?.toString() ?? trackToDownload.genre,
label: data['label']?.toString() ?? trackToDownload.label,
copyright:
data['copyright']?.toString() ?? trackToDownload.copyright,
comment: data['comment']?.toString() ?? trackToDownload.comment,
explicit:
parseExplicitFlag(data['explicit']) ??
trackToDownload.explicit,
upc:
(data['upc'] ?? data['barcode'])?.toString() ??
trackToDownload.upc,
);
_log.d(
'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}, AlbumType ${trackToDownload.albumType}',
);
} else {
_log.w('Unexpected track data type: ${trackData.runtimeType}');
}
} else {
_log.w('Response does not contain track key');
}
} catch (e, stack) {
_log.w('Failed to enrich metadata: $e');
_log.w('Stack trace: $stack');
}
if (await _shouldAbort('during metadata enrichment')) {
return false;
}
}
_log.d('Track coverUrl after enrichment: ${trackToDownload.coverUrl}');
return true;
trackToDownload = await n._prepareDownloadSourceTrack(trackToDownload);
return !await _shouldAbort('during metadata enrichment');
}
Future<void> _resolveOutputTarget() async {
+59
View File
@@ -0,0 +1,59 @@
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/logger.dart';
import 'package:spotiflac_android/utils/string_utils.dart';
/// Shared preparation before either queue chooses folders, filenames or tags.
Future<Track> enrichIncompleteDownloadTrack(
Track track,
String providerId,
String providerTrackId, {
Future<Map<String, dynamic>> Function(String, String, String)? loadMetadata,
}) async {
if (normalizeOptionalString(track.isrc) != null &&
(track.trackNumber ?? 0) > 0 &&
(track.totalTracks ?? 0) > 0 &&
normalizeOptionalString(track.composer) != null) {
return track;
}
try {
final response = await (loadMetadata ?? PlatformBridge.getProviderMetadata)(
providerId,
'track',
providerTrackId,
).timeout(const Duration(seconds: 8));
final data = response['track'];
if (data is! Map<String, dynamic>) return track;
String? text(String key) => normalizeOptionalString(data[key]?.toString());
final durationMs = readPositiveInt(data['duration_ms']);
return track.copyWith(
id: text('spotify_id') ?? track.id,
name: text('name') ?? track.name,
artistName: text('artists') ?? track.artistName,
albumName: text('album_name') ?? track.albumName,
albumArtist: text('album_artist') ?? track.albumArtist,
artistId: text('artist_id') ?? text('artistId') ?? track.artistId,
albumId: text('album_id') ?? track.albumId,
coverUrl: text('images') ?? track.coverUrl,
duration: durationMs == null ? track.duration : durationMs ~/ 1000,
isrc: text('isrc') ?? track.isrc,
trackNumber: readPositiveInt(data['track_number']) ?? track.trackNumber,
discNumber: readPositiveInt(data['disc_number']) ?? track.discNumber,
totalDiscs: readPositiveInt(data['total_discs']) ?? track.totalDiscs,
releaseDate: text('release_date') ?? track.releaseDate,
albumType: text('album_type') ?? track.albumType,
totalTracks: readPositiveInt(data['total_tracks']) ?? track.totalTracks,
composer: text('composer') ?? track.composer,
genre: text('genre') ?? track.genre,
label: text('label') ?? track.label,
copyright: text('copyright') ?? track.copyright,
comment: text('comment') ?? track.comment,
explicit: parseExplicitFlag(data['explicit']) ?? track.explicit,
upc: text('upc') ?? text('barcode') ?? track.upc,
);
} catch (error) {
AppLogger('DownloadMetadata').w('Track enrichment failed: $error');
return track;
}
}