feat(extensions): preserve provider metadata

This commit is contained in:
zarzet
2026-08-18 12:28:35 +07:00
parent 7fc27e8314
commit 48883d51d4
19 changed files with 260 additions and 69 deletions
+20
View File
@@ -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,
+8
View File
@@ -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,
+4 -3
View File
@@ -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,