mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
feat(extensions): preserve provider metadata
This commit is contained in:
@@ -27,6 +27,10 @@ class Track {
|
||||
final String? albumType;
|
||||
final int? totalTracks;
|
||||
final String? composer;
|
||||
final String? genre;
|
||||
final String? label;
|
||||
final String? copyright;
|
||||
final String? comment;
|
||||
final String? itemType;
|
||||
final String? audioQuality;
|
||||
final String? audioModes;
|
||||
@@ -54,6 +58,10 @@ class Track {
|
||||
this.albumType,
|
||||
this.totalTracks,
|
||||
this.composer,
|
||||
this.genre,
|
||||
this.label,
|
||||
this.copyright,
|
||||
this.comment,
|
||||
this.itemType,
|
||||
this.audioQuality,
|
||||
this.audioModes,
|
||||
@@ -128,6 +136,10 @@ class Track {
|
||||
source: effectiveSource,
|
||||
albumType: normalizeOptionalString(data['album_type']?.toString()),
|
||||
composer: data['composer']?.toString(),
|
||||
genre: data['genre']?.toString(),
|
||||
label: data['label']?.toString(),
|
||||
copyright: data['copyright']?.toString(),
|
||||
comment: data['comment']?.toString(),
|
||||
itemType: itemType,
|
||||
audioQuality: data['audio_quality']?.toString(),
|
||||
audioModes: data['audio_modes']?.toString(),
|
||||
@@ -158,6 +170,10 @@ class Track {
|
||||
String? albumType,
|
||||
int? totalTracks,
|
||||
String? composer,
|
||||
String? genre,
|
||||
String? label,
|
||||
String? copyright,
|
||||
String? comment,
|
||||
String? itemType,
|
||||
String? audioQuality,
|
||||
String? audioModes,
|
||||
@@ -185,6 +201,10 @@ class Track {
|
||||
albumType: albumType ?? this.albumType,
|
||||
totalTracks: totalTracks ?? this.totalTracks,
|
||||
composer: composer ?? this.composer,
|
||||
genre: genre ?? this.genre,
|
||||
label: label ?? this.label,
|
||||
copyright: copyright ?? this.copyright,
|
||||
comment: comment ?? this.comment,
|
||||
itemType: itemType ?? this.itemType,
|
||||
audioQuality: audioQuality ?? this.audioQuality,
|
||||
audioModes: audioModes ?? this.audioModes,
|
||||
|
||||
@@ -32,6 +32,10 @@ Track _$TrackFromJson(Map<String, dynamic> json) => Track(
|
||||
albumType: json['albumType'] as String?,
|
||||
totalTracks: (json['totalTracks'] as num?)?.toInt(),
|
||||
composer: json['composer'] as String?,
|
||||
genre: json['genre'] as String?,
|
||||
label: json['label'] as String?,
|
||||
copyright: json['copyright'] as String?,
|
||||
comment: json['comment'] as String?,
|
||||
itemType: json['itemType'] as String?,
|
||||
audioQuality: json['audioQuality'] as String?,
|
||||
audioModes: json['audioModes'] as String?,
|
||||
@@ -60,6 +64,10 @@ Map<String, dynamic> _$TrackToJson(Track instance) => <String, dynamic>{
|
||||
'albumType': instance.albumType,
|
||||
'totalTracks': instance.totalTracks,
|
||||
'composer': instance.composer,
|
||||
'genre': instance.genre,
|
||||
'label': instance.label,
|
||||
'copyright': instance.copyright,
|
||||
'comment': instance.comment,
|
||||
'itemType': instance.itemType,
|
||||
'audioQuality': instance.audioQuality,
|
||||
'audioModes': instance.audioModes,
|
||||
|
||||
@@ -721,10 +721,11 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
itemId: item.id,
|
||||
durationMs: track.duration * 1000,
|
||||
source: track.source ?? '',
|
||||
genre: genre ?? '',
|
||||
label: label ?? '',
|
||||
copyright: copyright ?? '',
|
||||
genre: genre ?? track.genre ?? '',
|
||||
label: label ?? track.label ?? '',
|
||||
copyright: copyright ?? track.copyright ?? '',
|
||||
composer: track.composer ?? '',
|
||||
comment: track.comment ?? '',
|
||||
qobuzId: payloadQobuzId,
|
||||
tidalId: payloadTidalId,
|
||||
deezerId: deezerTrackId ?? '',
|
||||
|
||||
@@ -59,6 +59,27 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldSkipMetadataEnrichment(
|
||||
ExtensionState extensionState,
|
||||
String? source,
|
||||
String? service,
|
||||
) {
|
||||
final candidates = <String>{};
|
||||
for (final value in [source, service]) {
|
||||
final normalized = value?.trim().toLowerCase();
|
||||
if (normalized != null && normalized.isNotEmpty) {
|
||||
candidates.add(normalized);
|
||||
}
|
||||
}
|
||||
if (candidates.isEmpty) return false;
|
||||
return extensionState.extensions.any(
|
||||
(extension) =>
|
||||
extension.enabled &&
|
||||
extension.skipMetadataEnrichment &&
|
||||
candidates.contains(extension.id.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
String? _extractKnownDeezerTrackId(Track track) {
|
||||
final deezerId = track.deezerId?.trim();
|
||||
if (deezerId != null && deezerId.isNotEmpty) {
|
||||
@@ -155,7 +176,14 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
composer: (track.composer != null && track.composer!.isNotEmpty)
|
||||
? track.composer
|
||||
: normalizedComposer,
|
||||
genre: track.genre,
|
||||
label: track.label,
|
||||
copyright: track.copyright,
|
||||
comment: track.comment,
|
||||
itemType: track.itemType,
|
||||
audioQuality: track.audioQuality,
|
||||
audioModes: track.audioModes,
|
||||
explicit: track.explicit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -482,6 +510,18 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
final sourceIsrc = normalizeOptionalString(baseTrack.isrc);
|
||||
final sourceReleaseDate = normalizeOptionalString(baseTrack.releaseDate);
|
||||
final sourceComposer = normalizeOptionalString(baseTrack.composer);
|
||||
final backendGenre = normalizeOptionalString(
|
||||
backendResult['genre']?.toString(),
|
||||
);
|
||||
final backendLabel = normalizeOptionalString(
|
||||
backendResult['label']?.toString(),
|
||||
);
|
||||
final backendCopyright = normalizeOptionalString(
|
||||
backendResult['copyright']?.toString(),
|
||||
);
|
||||
final backendComment = normalizeOptionalString(
|
||||
backendResult['comment']?.toString(),
|
||||
);
|
||||
final resolvedTotalTracks = _resolvePositiveMetadataInt(
|
||||
baseTrack.totalTracks,
|
||||
backendTotalTracks,
|
||||
@@ -541,7 +581,15 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
albumType: baseTrack.albumType,
|
||||
totalTracks: resolvedTotalTracks,
|
||||
composer: sourceComposer ?? backendComposer,
|
||||
genre: baseTrack.genre ?? backendGenre,
|
||||
label: baseTrack.label ?? backendLabel,
|
||||
copyright: baseTrack.copyright ?? backendCopyright,
|
||||
comment: baseTrack.comment ?? backendComment,
|
||||
source: baseTrack.source,
|
||||
itemType: baseTrack.itemType,
|
||||
audioQuality: baseTrack.audioQuality,
|
||||
audioModes: baseTrack.audioModes,
|
||||
explicit: baseTrack.explicit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,6 +607,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
String? genre,
|
||||
String? label,
|
||||
String? copyright,
|
||||
String? comment,
|
||||
String? downloadService,
|
||||
bool writeExternalLrc = true,
|
||||
}) async {
|
||||
@@ -573,6 +622,10 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
final isFlac = format == 'flac';
|
||||
final isM4a = format == 'm4a';
|
||||
final isMp3 = format == 'mp3';
|
||||
final resolvedGenre = genre ?? track.genre;
|
||||
final resolvedLabel = label ?? track.label;
|
||||
final resolvedCopyright = copyright ?? track.copyright;
|
||||
final resolvedComment = comment ?? track.comment;
|
||||
|
||||
Future<String?>? coverFuture;
|
||||
final coverUrl = normalizeRemoteHttpUrl(track.coverUrl);
|
||||
@@ -622,10 +675,17 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
}
|
||||
}
|
||||
if (track.isrc != null) metadata['ISRC'] = track.isrc!;
|
||||
if (genre != null && genre.isNotEmpty) metadata['GENRE'] = genre;
|
||||
if (label != null && label.isNotEmpty) metadata['ORGANIZATION'] = label;
|
||||
if (copyright != null && copyright.isNotEmpty) {
|
||||
metadata['COPYRIGHT'] = copyright;
|
||||
if (resolvedGenre != null && resolvedGenre.isNotEmpty) {
|
||||
metadata['GENRE'] = resolvedGenre;
|
||||
}
|
||||
if (resolvedLabel != null && resolvedLabel.isNotEmpty) {
|
||||
metadata['ORGANIZATION'] = resolvedLabel;
|
||||
}
|
||||
if (resolvedCopyright != null && resolvedCopyright.isNotEmpty) {
|
||||
metadata['COPYRIGHT'] = resolvedCopyright;
|
||||
}
|
||||
if (resolvedComment != null && resolvedComment.isNotEmpty) {
|
||||
metadata['COMMENT'] = resolvedComment;
|
||||
}
|
||||
if (track.composer != null && track.composer!.isNotEmpty) {
|
||||
metadata['COMPOSER'] = track.composer!;
|
||||
@@ -727,7 +787,8 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
'album': track.albumName,
|
||||
'albumArtist': ?albumArtist,
|
||||
if (track.releaseDate != null) 'date': track.releaseDate!,
|
||||
if (genre != null && genre.isNotEmpty) 'genre': genre,
|
||||
if (resolvedGenre != null && resolvedGenre.isNotEmpty)
|
||||
'genre': resolvedGenre,
|
||||
if (track.composer != null && track.composer!.isNotEmpty)
|
||||
'composer': track.composer!,
|
||||
if (track.trackNumber != null && track.trackNumber! > 0)
|
||||
@@ -739,9 +800,12 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
if (track.totalDiscs != null && track.totalDiscs! > 0)
|
||||
'totalDiscs': track.totalDiscs!.toString(),
|
||||
if (track.isrc != null) 'isrc': track.isrc!,
|
||||
if (label != null && label.isNotEmpty) 'label': label,
|
||||
if (copyright != null && copyright.isNotEmpty)
|
||||
'copyright': copyright,
|
||||
if (resolvedLabel != null && resolvedLabel.isNotEmpty)
|
||||
'label': resolvedLabel,
|
||||
if (resolvedCopyright != null && resolvedCopyright.isNotEmpty)
|
||||
'copyright': resolvedCopyright,
|
||||
if (resolvedComment != null && resolvedComment.isNotEmpty)
|
||||
'comment': resolvedComment,
|
||||
if (shouldEmbedLyrics) 'lyrics': ?lrcContent,
|
||||
};
|
||||
final ac4Result = await PlatformBridge.writeAC4Metadata(
|
||||
@@ -777,10 +841,14 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
if (track.isrc != null) 'isrc': track.isrc!,
|
||||
if (track.composer != null && track.composer!.isNotEmpty)
|
||||
'composer': track.composer!,
|
||||
if (genre != null && genre.isNotEmpty) 'genre': genre,
|
||||
if (label != null && label.isNotEmpty) 'label': label,
|
||||
if (copyright != null && copyright.isNotEmpty)
|
||||
'copyright': copyright,
|
||||
if (resolvedGenre != null && resolvedGenre.isNotEmpty)
|
||||
'genre': resolvedGenre,
|
||||
if (resolvedLabel != null && resolvedLabel.isNotEmpty)
|
||||
'label': resolvedLabel,
|
||||
if (resolvedCopyright != null && resolvedCopyright.isNotEmpty)
|
||||
'copyright': resolvedCopyright,
|
||||
if (resolvedComment != null && resolvedComment.isNotEmpty)
|
||||
'comment': resolvedComment,
|
||||
if (track.trackNumber != null && track.trackNumber! > 0)
|
||||
'track_number': track.trackNumber!.toString(),
|
||||
if (track.totalTracks != null && track.totalTracks! > 0)
|
||||
|
||||
@@ -148,6 +148,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
genre: result['genre'] as String?,
|
||||
label: result['label'] as String?,
|
||||
copyright: result['copyright'] as String?,
|
||||
comment: result['comment'] as String?,
|
||||
downloadService: downloadService,
|
||||
writeExternalLrc: storageMode != 'saf',
|
||||
);
|
||||
@@ -952,6 +953,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
genre: result['genre'] as String?,
|
||||
label: result['label'] as String?,
|
||||
copyright: result['copyright'] as String?,
|
||||
comment: result['comment'] as String?,
|
||||
downloadService: context.item.service,
|
||||
);
|
||||
}
|
||||
@@ -1088,6 +1090,7 @@ extension _DownloadQueueFinalization on DownloadQueueNotifier {
|
||||
genre: result['genre'] as String?,
|
||||
label: result['label'] as String?,
|
||||
copyright: result['copyright'] as String?,
|
||||
comment: result['comment'] as String?,
|
||||
downloadService: context.item.service,
|
||||
writeExternalLrc: context.storageMode != 'saf',
|
||||
);
|
||||
|
||||
@@ -790,24 +790,34 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
}
|
||||
|
||||
var trackForPayload = item.track;
|
||||
String? nativeDeezerTrackId = await _resolveDeezerIdFromKnownOrIsrc(
|
||||
trackForPayload,
|
||||
item.id,
|
||||
lookupContext: 'native worker ISRC',
|
||||
);
|
||||
final providerResolved = await _resolveDeezerIdViaProviderIfNeeded(
|
||||
trackForPayload,
|
||||
nativeDeezerTrackId,
|
||||
item.id,
|
||||
);
|
||||
trackForPayload = providerResolved.track;
|
||||
nativeDeezerTrackId = providerResolved.deezerTrackId;
|
||||
|
||||
final extendedMetadata = await _loadExtendedMetadataForDeezerId(
|
||||
nativeDeezerTrackId,
|
||||
);
|
||||
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
final skipMetadataEnrichment = _shouldSkipMetadataEnrichment(
|
||||
extensionState,
|
||||
trackForPayload.source,
|
||||
item.service,
|
||||
);
|
||||
String? nativeDeezerTrackId;
|
||||
if (skipMetadataEnrichment) {
|
||||
nativeDeezerTrackId = _extractKnownDeezerTrackId(trackForPayload);
|
||||
} else {
|
||||
nativeDeezerTrackId = await _resolveDeezerIdFromKnownOrIsrc(
|
||||
trackForPayload,
|
||||
item.id,
|
||||
lookupContext: 'native worker ISRC',
|
||||
);
|
||||
final providerResolved = await _resolveDeezerIdViaProviderIfNeeded(
|
||||
trackForPayload,
|
||||
nativeDeezerTrackId,
|
||||
item.id,
|
||||
);
|
||||
trackForPayload = providerResolved.track;
|
||||
nativeDeezerTrackId = providerResolved.deezerTrackId;
|
||||
}
|
||||
|
||||
final extendedMetadata = skipMetadataEnrichment
|
||||
? null
|
||||
: await _loadExtendedMetadataForDeezerId(nativeDeezerTrackId);
|
||||
|
||||
final payload = _buildDownloadRequestPayload(
|
||||
track: trackForPayload,
|
||||
item: item,
|
||||
|
||||
@@ -107,6 +107,7 @@ class _DownloadRun {
|
||||
late ExtensionState extensionState;
|
||||
late bool selectedExtensionDownloadProvider;
|
||||
late bool shouldSkipExtensionSongLinkPrelookup;
|
||||
late bool shouldSkipMetadataEnrichment;
|
||||
late bool useExtensions;
|
||||
|
||||
String? deezerTrackId;
|
||||
@@ -193,12 +194,14 @@ class _DownloadRun {
|
||||
// copy is just echoed back in the response), so this lookup overlaps
|
||||
// with the download instead of delaying its start; it is awaited right
|
||||
// after the download returns.
|
||||
final extendedMetadataFuture = n
|
||||
._loadExtendedMetadataForDeezerId(deezerTrackId)
|
||||
.catchError((Object e) {
|
||||
_log.w('Extended metadata lookup failed: $e');
|
||||
return null;
|
||||
});
|
||||
final extendedMetadataFuture = shouldSkipMetadataEnrichment
|
||||
? Future<_DeezerExtendedMetadataFields?>.value(null)
|
||||
: n._loadExtendedMetadataForDeezerId(deezerTrackId).catchError((
|
||||
Object e,
|
||||
) {
|
||||
_log.w('Extended metadata lookup failed: $e');
|
||||
return null;
|
||||
});
|
||||
|
||||
if (await _shouldAbort('before native download start')) {
|
||||
return;
|
||||
@@ -350,7 +353,16 @@ class _DownloadRun {
|
||||
(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,
|
||||
source: trackToDownload.source,
|
||||
itemType: trackToDownload.itemType,
|
||||
audioQuality: trackToDownload.audioQuality,
|
||||
audioModes: trackToDownload.audioModes,
|
||||
explicit: trackToDownload.explicit,
|
||||
);
|
||||
_log.d(
|
||||
'Metadata enriched: Track ${trackToDownload.trackNumber}, Disc ${trackToDownload.discNumber}, ISRC ${trackToDownload.isrc}, AlbumType ${trackToDownload.albumType}',
|
||||
@@ -456,11 +468,21 @@ class _DownloadRun {
|
||||
e.hasMetadataProvider &&
|
||||
e.id.toLowerCase() == trackSource,
|
||||
);
|
||||
shouldSkipMetadataEnrichment = n._shouldSkipMetadataEnrichment(
|
||||
extensionState,
|
||||
trackToDownload.source,
|
||||
item.service,
|
||||
);
|
||||
final hasActiveExtensions = extensionState.extensions.any((e) => e.enabled);
|
||||
useExtensions = settings.useExtensionProviders && hasActiveExtensions;
|
||||
}
|
||||
|
||||
Future<bool> _resolveTrackIdentifiers() async {
|
||||
if (shouldSkipMetadataEnrichment) {
|
||||
deezerTrackId = n._extractKnownDeezerTrackId(trackToDownload);
|
||||
_log.d('Skipping cross-provider metadata enrichment for ${item.service}');
|
||||
return true;
|
||||
}
|
||||
deezerTrackId = await n._resolveDeezerIdFromKnownOrIsrc(
|
||||
trackToDownload,
|
||||
item.id,
|
||||
@@ -1451,6 +1473,7 @@ class _DownloadRun {
|
||||
genre: (result['genre'] as String?) ?? genre,
|
||||
label: (result['label'] as String?) ?? label,
|
||||
copyright: result['copyright'] as String?,
|
||||
comment: result['comment'] as String?,
|
||||
downloadService: item.service,
|
||||
writeExternalLrc: writeExternalLrc,
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ class DownloadRequestPayload {
|
||||
final String label;
|
||||
final String copyright;
|
||||
final String composer;
|
||||
final String comment;
|
||||
final String tidalId;
|
||||
final String qobuzId;
|
||||
final String deezerId;
|
||||
@@ -96,6 +97,7 @@ class DownloadRequestPayload {
|
||||
this.label = '',
|
||||
this.copyright = '',
|
||||
this.composer = '',
|
||||
this.comment = '',
|
||||
this.tidalId = '',
|
||||
this.qobuzId = '',
|
||||
this.deezerId = '',
|
||||
@@ -156,6 +158,7 @@ class DownloadRequestPayload {
|
||||
'label': label,
|
||||
'copyright': copyright,
|
||||
'composer': composer,
|
||||
'comment': comment,
|
||||
'tidal_id': tidalId,
|
||||
'qobuz_id': qobuzId,
|
||||
'deezer_id': deezerId,
|
||||
@@ -220,6 +223,7 @@ class DownloadRequestPayload {
|
||||
label: label,
|
||||
copyright: copyright,
|
||||
composer: composer,
|
||||
comment: comment,
|
||||
tidalId: tidalId,
|
||||
qobuzId: qobuzId,
|
||||
deezerId: deezerId,
|
||||
|
||||
Reference in New Issue
Block a user