diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerMedia.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerMedia.kt index 1a4170c0..f922882c 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerMedia.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeFinalizerMedia.kt @@ -283,6 +283,9 @@ internal fun NativeDownloadFinalizer.embedBasicMetadata(context: Context, path: val genre = resultString(input, "genre").ifBlank { requestString(input, "genre") } val label = resultString(input, "label").ifBlank { requestString(input, "label") } val copyright = resultString(input, "copyright").ifBlank { requestString(input, "copyright") } + val comment = resultString(input, "comment").ifBlank { + trackString(input, "comment", requestString(input, "comment")) + } val lyricsMode = input.request.optString("lyrics_mode", "embed") val shouldResolveLyrics = input.request.optBoolean("embed_lyrics", false) && (lyricsMode == "embed" || lyricsMode == "both") @@ -308,6 +311,7 @@ internal fun NativeDownloadFinalizer.embedBasicMetadata(context: Context, path: .put("genre", genre) .put("label", label) .put("copyright", copyright) + .put("comment", comment) if (trackNumberValue > 0) fields.put("track_number", trackNumberValue.toString()) if (totalTracksValue > 0) fields.put("track_total", totalTracksValue.toString()) if (discNumberValue > 0) fields.put("disc_number", discNumberValue.toString()) @@ -356,6 +360,7 @@ internal fun NativeDownloadFinalizer.embedBasicMetadata(context: Context, path: "genre" to genre, labelKey to label, "copyright" to copyright, + "comment" to comment, "lyrics" to if (shouldEmbedLyrics) lyrics else "", "unsyncedlyrics" to if (shouldEmbedLyrics) lyrics else "", ) diff --git a/go_backend/coverage_test_helpers_test.go b/go_backend/coverage_test_helpers_test.go index c771912e..a6ab6b4c 100644 --- a/go_backend/coverage_test_helpers_test.go +++ b/go_backend/coverage_test_helpers_test.go @@ -72,6 +72,7 @@ function track(id) { copyright: "Copyright", genre: "Pop", composer: "Composer", + comment: "https://example.test/album/1", audioQuality: "FLAC 24-bit", audioModes: "DOLBY_ATMOS", explicit: true @@ -165,6 +166,7 @@ registerExtension({ label: "Label", copyright: "Copyright", composer: "Composer", + comment: "https://example.test/album/1", lyricsLrc: "[00:00.00]Hello", decryptionKey: "001122", decryption: { strategy: "mp4_decryption_key", options: { kid: "1" } } diff --git a/go_backend/exports_download.go b/go_backend/exports_download.go index 02d26beb..26652c5f 100644 --- a/go_backend/exports_download.go +++ b/go_backend/exports_download.go @@ -45,6 +45,7 @@ type DownloadRequest struct { Label string `json:"label,omitempty"` Copyright string `json:"copyright,omitempty"` Composer string `json:"composer,omitempty"` + Comment string `json:"comment,omitempty"` TidalID string `json:"tidal_id,omitempty"` QobuzID string `json:"qobuz_id,omitempty"` DeezerID string `json:"deezer_id,omitempty"` @@ -89,6 +90,7 @@ type DownloadResponse struct { Label string `json:"label,omitempty"` Copyright string `json:"copyright,omitempty"` Composer string `json:"composer,omitempty"` + Comment string `json:"comment,omitempty"` SkipMetadataEnrichment bool `json:"skip_metadata_enrichment,omitempty"` LyricsLRC string `json:"lyrics_lrc,omitempty"` DecryptionKey string `json:"decryption_key,omitempty"` @@ -114,6 +116,7 @@ type DownloadResult struct { Label string Copyright string Composer string + Comment string LyricsLRC string DecryptionKey string Decryption *DownloadDecryptionInfo @@ -175,6 +178,10 @@ func buildDownloadSuccessResponse( if composer == "" { composer = req.Composer } + comment := result.Comment + if comment == "" { + comment = req.Comment + } coverURL := strings.TrimSpace(result.CoverURL) if coverURL == "" { @@ -210,6 +217,7 @@ func buildDownloadSuccessResponse( Label: label, Copyright: copyright, Composer: composer, + Comment: comment, LyricsLRC: result.LyricsLRC, DecryptionKey: result.DecryptionKey, Decryption: normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey), diff --git a/go_backend/exports_extensions.go b/go_backend/exports_extensions.go index b41fd789..c04435b8 100644 --- a/go_backend/exports_extensions.go +++ b/go_backend/exports_extensions.go @@ -27,34 +27,39 @@ func normalizeExtensionTrackMetadataMap( } return map[string]any{ - "id": track.ID, - "name": track.Name, - "artists": track.Artists, - "album_name": track.AlbumName, - "album_artist": track.AlbumArtist, - "album_id": track.AlbumID, - "album_url": track.AlbumURL, - "artist_id": track.ArtistID, - "artist_url": track.ArtistURL, - "external_urls": track.ExternalURL, - "duration_ms": track.DurationMS, - "images": coverURL, - "cover_url": coverURL, - "preview_url": track.PreviewURL, - "release_date": track.ReleaseDate, - "track_number": trackNum, - "total_tracks": track.TotalTracks, - "disc_number": track.DiscNumber, - "total_discs": track.TotalDiscs, - "isrc": track.ISRC, - "provider_id": track.ProviderID, - "item_type": track.ItemType, - "album_type": track.AlbumType, - "spotify_id": track.SpotifyID, - "composer": track.Composer, - "audio_quality": track.AudioQuality, - "audio_modes": track.AudioModes, - "explicit": track.Explicit, + "id": track.ID, + "name": track.Name, + "artists": track.Artists, + "album_name": track.AlbumName, + "album_artist": track.AlbumArtist, + "album_id": track.AlbumID, + "album_url": track.AlbumURL, + "artist_id": track.ArtistID, + "artist_url": track.ArtistURL, + "external_urls": track.ExternalURL, + "duration_ms": track.DurationMS, + "images": coverURL, + "cover_url": coverURL, + "preview_url": track.PreviewURL, + "release_date": track.ReleaseDate, + "track_number": trackNum, + "total_tracks": track.TotalTracks, + "disc_number": track.DiscNumber, + "total_discs": track.TotalDiscs, + "isrc": track.ISRC, + "provider_id": track.ProviderID, + "item_type": track.ItemType, + "album_type": track.AlbumType, + "spotify_id": track.SpotifyID, + "external_links": track.ExternalLinks, + "genre": track.Genre, + "label": track.Label, + "copyright": track.Copyright, + "composer": track.Composer, + "comment": track.Comment, + "audio_quality": track.AudioQuality, + "audio_modes": track.AudioModes, + "explicit": track.Explicit, } } diff --git a/go_backend/extension_fallback.go b/go_backend/extension_fallback.go index 831275aa..82584ed4 100644 --- a/go_backend/extension_fallback.go +++ b/go_backend/extension_fallback.go @@ -88,7 +88,9 @@ func attemptExtensionDownload( if downloadSucceeded { metadataStartedAt := time.Now() - enrichRequestExtendedMetadata(&req) + if !ext.Manifest.SkipMetadataEnrichment { + enrichRequestExtendedMetadata(&req) + } LogDebug( "DownloadPipeline", "item=%s provider=%s post-transfer metadataMs=%.1f", diff --git a/go_backend/extension_fallback_helpers.go b/go_backend/extension_fallback_helpers.go index 7605733a..88611e44 100644 --- a/go_backend/extension_fallback_helpers.go +++ b/go_backend/extension_fallback_helpers.go @@ -191,6 +191,7 @@ func normalizeExtensionDownloadResult(result *ExtDownloadResult) (DownloadResult Label: result.Label, Copyright: result.Copyright, Composer: result.Composer, + Comment: result.Comment, LyricsLRC: result.LyricsLRC, DecryptionKey: result.DecryptionKey, Decryption: normalizeDownloadDecryptionInfo(result.Decryption, result.DecryptionKey), @@ -261,6 +262,7 @@ func overlayExtensionDownloadMetadata(resp *DownloadResponse, result *ExtDownloa overlayStrTrim(&resp.Label, result.Label) overlayStrTrim(&resp.Copyright, result.Copyright) overlayStrTrim(&resp.Composer, result.Composer) + overlayStrTrim(&resp.Comment, result.Comment) if result.LyricsLRC != "" { resp.LyricsLRC = result.LyricsLRC } @@ -295,6 +297,7 @@ func applyExtensionRequestFallbacks(resp *DownloadResponse, req DownloadRequest) overlayInt(&resp.DiscNumber, req.DiscNumber, "") overlayInt(&resp.TotalDiscs, req.TotalDiscs, "") overlayStr(&resp.CoverURL, req.CoverURL, "") + overlayStr(&resp.Comment, req.Comment, "") } func shouldStopProviderFallback(availability *ExtAvailabilityResult) bool { diff --git a/go_backend/extension_fallback_output.go b/go_backend/extension_fallback_output.go index 1bfafcca..72bbe697 100644 --- a/go_backend/extension_fallback_output.go +++ b/go_backend/extension_fallback_output.go @@ -170,6 +170,7 @@ func embedExtensionDownloadMetadata(resp DownloadResponse, req DownloadRequest, Label: firstNonEmptyTrimmed(resp.Label, req.Label), Copyright: firstNonEmptyTrimmed(resp.Copyright, req.Copyright), Composer: firstNonEmptyTrimmed(resp.Composer, req.Composer), + Comment: firstNonEmptyTrimmed(resp.Comment, req.Comment), } if req.EmbedLyrics { metadata.Lyrics = resp.LyricsLRC diff --git a/go_backend/extension_goja_convert.go b/go_backend/extension_goja_convert.go index f2e336b0..2295a718 100644 --- a/go_backend/extension_goja_convert.go +++ b/go_backend/extension_goja_convert.go @@ -184,6 +184,7 @@ func parseExtensionTrackValue(vm *goja.Runtime, value goja.Value) ExtTrackMetada Copyright: gojaObjectString(obj, "copyright"), Genre: gojaObjectString(obj, "genre"), Composer: gojaObjectString(obj, "composer"), + Comment: gojaObjectString(obj, "comment", "comments"), AudioQuality: gojaObjectString(obj, "audio_quality", "audioQuality"), AudioModes: gojaObjectString(obj, "audio_modes", "audioModes"), } @@ -504,6 +505,7 @@ func parseExtensionDownloadResultValue(vm *goja.Runtime, value goja.Value) ExtDo Label: gojaObjectString(obj, "label"), Copyright: gojaObjectString(obj, "copyright"), Composer: gojaObjectString(obj, "composer"), + Comment: gojaObjectString(obj, "comment", "comments"), LyricsLRC: gojaObjectString(obj, "lyrics_lrc", "lyricsLrc"), DecryptionKey: gojaObjectString(obj, "decryption_key", "decryptionKey"), Decryption: parseExtensionDownloadDecryptionValue(vm, gojaObjectValue(obj, "decryption")), diff --git a/go_backend/extension_provider_supplement_test.go b/go_backend/extension_provider_supplement_test.go index 1ef11ae9..160391af 100644 --- a/go_backend/extension_provider_supplement_test.go +++ b/go_backend/extension_provider_supplement_test.go @@ -21,7 +21,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) { if err != nil { t.Fatalf("GetTrack: %v", err) } - if track.Name != "Track track-1" || track.ProviderID != ext.ID || track.AudioQuality == "" { + if track.Name != "Track track-1" || track.ProviderID != ext.ID || track.AudioQuality == "" || track.Comment != "https://example.test/album/1" { t.Fatalf("track = %#v", track) } @@ -72,7 +72,7 @@ func TestExtensionProviderWrapperFullSurface(t *testing.T) { if err != nil { t.Fatalf("Download: %v", err) } - if !download.Success || download.Decryption == nil || download.DecryptionKey != "001122" || len(progress) != 1 || progress[0] != 100 { + if !download.Success || download.Decryption == nil || download.DecryptionKey != "001122" || download.Comment != "https://example.test/album/1" || len(progress) != 1 || progress[0] != 100 { t.Fatalf("download = %#v progress=%v", download, progress) } diff --git a/go_backend/extension_provider_types.go b/go_backend/extension_provider_types.go index 02d1d944..5ee5003f 100644 --- a/go_backend/extension_provider_types.go +++ b/go_backend/extension_provider_types.go @@ -38,6 +38,7 @@ type ExtTrackMetadata struct { Copyright string `json:"copyright,omitempty"` Genre string `json:"genre,omitempty"` Composer string `json:"composer,omitempty"` + Comment string `json:"comment,omitempty"` AudioQuality string `json:"audio_quality,omitempty"` AudioModes string `json:"audio_modes,omitempty"` @@ -127,6 +128,7 @@ type ExtDownloadResult struct { Label string `json:"label,omitempty"` Copyright string `json:"copyright,omitempty"` Composer string `json:"composer,omitempty"` + Comment string `json:"comment,omitempty"` LyricsLRC string `json:"lyrics_lrc,omitempty"` DecryptionKey string `json:"decryption_key,omitempty"` Decryption *DownloadDecryptionInfo `json:"decryption,omitempty"` diff --git a/lib/models/track.dart b/lib/models/track.dart index be3b1c5b..eae78c95 100644 --- a/lib/models/track.dart +++ b/lib/models/track.dart @@ -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, diff --git a/lib/models/track.g.dart b/lib/models/track.g.dart index 77555870..651d262b 100644 --- a/lib/models/track.g.dart +++ b/lib/models/track.g.dart @@ -32,6 +32,10 @@ Track _$TrackFromJson(Map 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 _$TrackToJson(Track instance) => { '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, diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index abf1dc84..3dacf45c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -721,10 +721,11 @@ class DownloadQueueNotifier extends Notifier { 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 ?? '', diff --git a/lib/providers/download_queue_provider_embedding.dart b/lib/providers/download_queue_provider_embedding.dart index 4fbc124c..1f6ade5a 100644 --- a/lib/providers/download_queue_provider_embedding.dart +++ b/lib/providers/download_queue_provider_embedding.dart @@ -59,6 +59,27 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier { ); } + bool _shouldSkipMetadataEnrichment( + ExtensionState extensionState, + String? source, + String? service, + ) { + final candidates = {}; + 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? 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) diff --git a/lib/providers/download_queue_provider_finalization.dart b/lib/providers/download_queue_provider_finalization.dart index df16c0be..7c665069 100644 --- a/lib/providers/download_queue_provider_finalization.dart +++ b/lib/providers/download_queue_provider_finalization.dart @@ -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', ); diff --git a/lib/providers/download_queue_provider_native_worker.dart b/lib/providers/download_queue_provider_native_worker.dart index c871de51..956457ef 100644 --- a/lib/providers/download_queue_provider_native_worker.dart +++ b/lib/providers/download_queue_provider_native_worker.dart @@ -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, diff --git a/lib/providers/download_queue_provider_single_item.dart b/lib/providers/download_queue_provider_single_item.dart index 70a13203..a0516f66 100644 --- a/lib/providers/download_queue_provider_single_item.dart +++ b/lib/providers/download_queue_provider_single_item.dart @@ -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 _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, ); diff --git a/lib/services/download_request_payload.dart b/lib/services/download_request_payload.dart index a58188bc..94f8216a 100644 --- a/lib/services/download_request_payload.dart +++ b/lib/services/download_request_payload.dart @@ -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, diff --git a/test/models_and_utils_test.dart b/test/models_and_utils_test.dart index ecec1097..d032ef5f 100644 --- a/test/models_and_utils_test.dart +++ b/test/models_and_utils_test.dart @@ -443,6 +443,28 @@ void main() { expect(track.isExplicit, isTrue); }); + test('preserves extension tagging metadata', () { + final track = Track.fromBackendMap({ + 'id': 'extension-track-1', + 'name': 'Song', + 'artists': 'Artist', + 'album_name': 'Album', + 'duration_ms': 180000, + 'genre': 'Pop', + 'label': 'Label', + 'copyright': 'Copyright', + 'composer': 'Composer', + 'comment': 'https://example.test/album/1', + }); + + expect(track.genre, 'Pop'); + expect(track.label, 'Label'); + expect(track.copyright, 'Copyright'); + expect(track.composer, 'Composer'); + expect(track.comment, 'https://example.test/album/1'); + expect(track.toJson()['comment'], 'https://example.test/album/1'); + }); + test('does not treat a playlist container name as a track album', () { final playlistTrack = Track.fromBackendMap({ 'id': 'playlist-track-1', @@ -855,6 +877,7 @@ void main() { label: 'Label', copyright: 'Copyright', composer: 'Composer', + comment: 'https://example.test/album/1', tidalId: 'tidal-1', qobuzId: 'qobuz-1', deezerId: 'deezer-1', @@ -911,6 +934,7 @@ void main() { 'label': 'Label', 'copyright': 'Copyright', 'composer': 'Composer', + 'comment': 'https://example.test/album/1', 'tidal_id': 'tidal-1', 'qobuz_id': 'qobuz-1', 'deezer_id': 'deezer-1',