mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-13 21:38:58 +02:00
fix(metadata): preserve Track fields during enrichment
Apply resolved metadata with copyWith and retain the existing field precedence. Keep UPC, preview URLs, and source-only fields when enrichment updates other tags. Extract the pure merge helpers and add regression tests for field preservation.
This commit is contained in:
@@ -32,12 +32,6 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
return albumArtist;
|
||||
}
|
||||
|
||||
static final _isrcRegex = RegExp(r'^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$');
|
||||
|
||||
bool _isValidISRC(String value) {
|
||||
return _isrcRegex.hasMatch(value.toUpperCase());
|
||||
}
|
||||
|
||||
/// Returns true if any enabled extension matching [source] or [service]
|
||||
/// declares `skipLyrics: true` in its manifest.
|
||||
bool _shouldSkipLyrics(
|
||||
@@ -129,64 +123,6 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
Track _copyTrackWithResolvedMetadata(
|
||||
Track track, {
|
||||
String? resolvedIsrc,
|
||||
int? trackNumber,
|
||||
int? totalTracks,
|
||||
int? discNumber,
|
||||
int? totalDiscs,
|
||||
String? releaseDate,
|
||||
String? deezerId,
|
||||
String? composer,
|
||||
}) {
|
||||
final normalizedIsrc = normalizeOptionalString(resolvedIsrc);
|
||||
final normalizedComposer = normalizeOptionalString(composer);
|
||||
|
||||
return Track(
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
artistName: track.artistName,
|
||||
albumName: track.albumName,
|
||||
albumArtist: track.albumArtist,
|
||||
artistId: track.artistId,
|
||||
albumId: track.albumId,
|
||||
coverUrl: normalizeCoverReference(track.coverUrl),
|
||||
duration: track.duration,
|
||||
isrc: (normalizedIsrc != null && _isValidISRC(normalizedIsrc))
|
||||
? normalizedIsrc
|
||||
: track.isrc,
|
||||
trackNumber: (track.trackNumber != null && track.trackNumber! > 0)
|
||||
? track.trackNumber
|
||||
: trackNumber,
|
||||
discNumber: (track.discNumber != null && track.discNumber! > 0)
|
||||
? track.discNumber
|
||||
: discNumber,
|
||||
totalDiscs: (track.totalDiscs != null && track.totalDiscs! > 0)
|
||||
? track.totalDiscs
|
||||
: totalDiscs,
|
||||
releaseDate: track.releaseDate ?? normalizeOptionalString(releaseDate),
|
||||
deezerId: deezerId ?? track.deezerId,
|
||||
availability: track.availability,
|
||||
source: track.source,
|
||||
albumType: track.albumType,
|
||||
totalTracks: (track.totalTracks != null && track.totalTracks! > 0)
|
||||
? track.totalTracks
|
||||
: totalTracks,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Future<_DeezerLookupPreparation> _resolveProviderTrackForDeezerLookup(
|
||||
Track track,
|
||||
String itemId,
|
||||
@@ -231,7 +167,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
'Resolved ISRC from ${effectiveProvider.isEmpty ? provider : effectiveProvider}: $resolvedIsrc',
|
||||
);
|
||||
|
||||
final updatedTrack = _copyTrackWithResolvedMetadata(
|
||||
final updatedTrack = copyTrackWithResolvedMetadata(
|
||||
track,
|
||||
resolvedIsrc: resolvedIsrc,
|
||||
releaseDate: trackData['release_date'] as String?,
|
||||
@@ -251,7 +187,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
return _DeezerLookupPreparation(
|
||||
track: deezerTrackId == null
|
||||
? updatedTrack
|
||||
: _copyTrackWithResolvedMetadata(
|
||||
: copyTrackWithResolvedMetadata(
|
||||
updatedTrack,
|
||||
deezerId: deezerTrackId,
|
||||
),
|
||||
@@ -321,7 +257,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
deezerTrackId != null;
|
||||
|
||||
final updatedTrack = needsEnrich
|
||||
? _copyTrackWithResolvedMetadata(
|
||||
? copyTrackWithResolvedMetadata(
|
||||
track,
|
||||
resolvedIsrc: deezerIsrc,
|
||||
releaseDate: trackData['release_date'] as String?,
|
||||
@@ -351,7 +287,7 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
deezerTrackId = deezerData['id'].toString();
|
||||
_log.d('Found Deezer track ID via SongLink (flat): $deezerTrackId');
|
||||
return _DeezerLookupPreparation(
|
||||
track: _copyTrackWithResolvedMetadata(track, deezerId: deezerTrackId),
|
||||
track: copyTrackWithResolvedMetadata(track, deezerId: deezerTrackId),
|
||||
deezerTrackId: deezerTrackId,
|
||||
);
|
||||
}
|
||||
@@ -451,172 +387,6 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
return _loadDeezerExtendedMetadata(deezerTrackId);
|
||||
}
|
||||
|
||||
bool _isUsableIndex(int? number, int? total) {
|
||||
if (number == null || number <= 0) return false;
|
||||
return total == null || total <= 0 || number <= total;
|
||||
}
|
||||
|
||||
int? _resolvePositiveMetadataInt(int? sourceValue, int? backendValue) {
|
||||
if (sourceValue != null && sourceValue > 0) return sourceValue;
|
||||
return backendValue;
|
||||
}
|
||||
|
||||
int? _resolveMetadataIndex({
|
||||
required int? sourceValue,
|
||||
required int? backendValue,
|
||||
required int? total,
|
||||
}) {
|
||||
if (_isUsableIndex(sourceValue, total)) return sourceValue;
|
||||
if (_isUsableIndex(backendValue, total)) return backendValue;
|
||||
return sourceValue != null && sourceValue > 0 ? sourceValue : backendValue;
|
||||
}
|
||||
|
||||
String? _resolveMetadataText(String? sourceValue, String? backendValue) {
|
||||
return normalizeOptionalString(sourceValue) ??
|
||||
normalizeOptionalString(backendValue);
|
||||
}
|
||||
|
||||
Track _buildTrackForMetadataEmbedding(
|
||||
Track baseTrack,
|
||||
Map<String, dynamic> backendResult,
|
||||
String? resolvedAlbumArtist,
|
||||
) {
|
||||
final backendTrackNum = readPositiveInt(backendResult['track_number']);
|
||||
final backendDiscNum = readPositiveInt(backendResult['disc_number']);
|
||||
final backendTotalTracks = readPositiveInt(backendResult['total_tracks']);
|
||||
final backendTotalDiscs = readPositiveInt(backendResult['total_discs']);
|
||||
final backendYear = normalizeOptionalString(
|
||||
backendResult['release_date'] as String?,
|
||||
);
|
||||
final backendAlbum = normalizeOptionalString(
|
||||
backendResult['album'] as String?,
|
||||
);
|
||||
final backendIsrc = normalizeOptionalString(
|
||||
backendResult['isrc'] as String?,
|
||||
);
|
||||
final backendCoverUrl = normalizeCoverReference(
|
||||
backendResult['cover_url']?.toString(),
|
||||
);
|
||||
final baseCoverUrl = normalizeCoverReference(baseTrack.coverUrl);
|
||||
final resolvedCoverUrl = baseCoverUrl ?? backendCoverUrl;
|
||||
final backendAlbumArtist = normalizeOptionalString(
|
||||
backendResult['album_artist'] as String?,
|
||||
);
|
||||
final backendComposer = normalizeOptionalString(
|
||||
backendResult['composer']?.toString(),
|
||||
);
|
||||
final sourceAlbumName = normalizeOptionalString(baseTrack.albumName);
|
||||
final sourceAlbumArtist = normalizeOptionalString(baseTrack.albumArtist);
|
||||
final sourceIsrc = normalizeOptionalString(baseTrack.isrc);
|
||||
final sourceReleaseDate = normalizeOptionalString(baseTrack.releaseDate);
|
||||
final sourceComposer = normalizeOptionalString(baseTrack.composer);
|
||||
final sourceAlbumType = normalizeOptionalString(baseTrack.albumType);
|
||||
final sourceGenre = normalizeOptionalString(baseTrack.genre);
|
||||
final sourceLabel = normalizeOptionalString(baseTrack.label);
|
||||
final sourceCopyright = normalizeOptionalString(baseTrack.copyright);
|
||||
final sourceComment = normalizeOptionalString(baseTrack.comment);
|
||||
final sourceUpc = normalizeOptionalString(baseTrack.upc);
|
||||
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 resolvedComment = backendComment ?? sourceComment;
|
||||
final backendAlbumType = normalizeOptionalString(
|
||||
backendResult['album_type']?.toString(),
|
||||
);
|
||||
final backendUpc = normalizeOptionalString(
|
||||
(backendResult['upc'] ?? backendResult['barcode'])?.toString(),
|
||||
);
|
||||
final backendExplicit = backendResult['explicit'] == true;
|
||||
final resolvedTotalTracks = _resolvePositiveMetadataInt(
|
||||
baseTrack.totalTracks,
|
||||
backendTotalTracks,
|
||||
);
|
||||
final resolvedTotalDiscs = _resolvePositiveMetadataInt(
|
||||
baseTrack.totalDiscs,
|
||||
backendTotalDiscs,
|
||||
);
|
||||
final resolvedTrackNumber = _resolveMetadataIndex(
|
||||
sourceValue: baseTrack.trackNumber,
|
||||
backendValue: backendTrackNum,
|
||||
total: resolvedTotalTracks,
|
||||
);
|
||||
final resolvedDiscNumber = _resolveMetadataIndex(
|
||||
sourceValue: baseTrack.discNumber,
|
||||
backendValue: backendDiscNum,
|
||||
total: resolvedTotalDiscs,
|
||||
);
|
||||
|
||||
final hasOverrides =
|
||||
resolvedTrackNumber != baseTrack.trackNumber ||
|
||||
resolvedDiscNumber != baseTrack.discNumber ||
|
||||
resolvedTotalTracks != baseTrack.totalTracks ||
|
||||
resolvedTotalDiscs != baseTrack.totalDiscs ||
|
||||
resolvedAlbumArtist != sourceAlbumArtist ||
|
||||
(sourceReleaseDate == null && backendYear != null) ||
|
||||
(sourceAlbumName == null && backendAlbum != null) ||
|
||||
(sourceIsrc == null && backendIsrc != null) ||
|
||||
(baseCoverUrl == null && backendCoverUrl != null) ||
|
||||
(sourceAlbumArtist == null &&
|
||||
resolvedAlbumArtist == null &&
|
||||
backendAlbumArtist != null) ||
|
||||
(sourceComposer == null && backendComposer != null) ||
|
||||
(sourceAlbumType == null && backendAlbumType != null) ||
|
||||
(sourceGenre == null && backendGenre != null) ||
|
||||
(sourceLabel == null && backendLabel != null) ||
|
||||
(sourceCopyright == null && backendCopyright != null) ||
|
||||
resolvedComment != sourceComment ||
|
||||
(baseTrack.explicit != true && backendExplicit) ||
|
||||
(sourceUpc == null && backendUpc != null);
|
||||
|
||||
if (!hasOverrides) {
|
||||
return baseTrack;
|
||||
}
|
||||
|
||||
return Track(
|
||||
id: baseTrack.id,
|
||||
name: baseTrack.name,
|
||||
artistName: baseTrack.artistName,
|
||||
albumName: sourceAlbumName ?? backendAlbum ?? baseTrack.albumName,
|
||||
albumArtist:
|
||||
resolvedAlbumArtist ?? sourceAlbumArtist ?? backendAlbumArtist,
|
||||
artistId: baseTrack.artistId,
|
||||
albumId: baseTrack.albumId,
|
||||
coverUrl: resolvedCoverUrl,
|
||||
duration: baseTrack.duration,
|
||||
isrc: sourceIsrc ?? backendIsrc,
|
||||
trackNumber: resolvedTrackNumber,
|
||||
discNumber: resolvedDiscNumber,
|
||||
totalDiscs: resolvedTotalDiscs,
|
||||
releaseDate: sourceReleaseDate ?? backendYear,
|
||||
deezerId: baseTrack.deezerId,
|
||||
availability: baseTrack.availability,
|
||||
albumType: sourceAlbumType ?? backendAlbumType,
|
||||
totalTracks: resolvedTotalTracks,
|
||||
composer: sourceComposer ?? backendComposer,
|
||||
genre: sourceGenre ?? backendGenre,
|
||||
label: sourceLabel ?? backendLabel,
|
||||
copyright: sourceCopyright ?? backendCopyright,
|
||||
comment: resolvedComment,
|
||||
source: baseTrack.source,
|
||||
itemType: baseTrack.itemType,
|
||||
audioQuality: baseTrack.audioQuality,
|
||||
audioModes: baseTrack.audioModes,
|
||||
explicit: baseTrack.explicit == true || backendExplicit
|
||||
? true
|
||||
: baseTrack.explicit,
|
||||
upc: sourceUpc ?? backendUpc,
|
||||
);
|
||||
}
|
||||
|
||||
/// Unified metadata, cover, lyrics, and ReplayGain embedding for all formats.
|
||||
///
|
||||
/// [format] must be one of `'flac'`, `'m4a'`, `'mp3'`, or `'opus'`.
|
||||
@@ -1079,3 +849,201 @@ extension _DownloadQueueEmbedding on DownloadQueueNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _isrcRegex = RegExp(r'^[A-Z]{2}[A-Z0-9]{3}\d{2}\d{5}$');
|
||||
|
||||
bool _isValidISRC(String value) {
|
||||
return _isrcRegex.hasMatch(value.toUpperCase());
|
||||
}
|
||||
|
||||
/// Fills resolved identifiers and missing metadata without dropping other tags.
|
||||
Track copyTrackWithResolvedMetadata(
|
||||
Track track, {
|
||||
String? resolvedIsrc,
|
||||
int? trackNumber,
|
||||
int? totalTracks,
|
||||
int? discNumber,
|
||||
int? totalDiscs,
|
||||
String? releaseDate,
|
||||
String? deezerId,
|
||||
String? composer,
|
||||
}) {
|
||||
final normalizedIsrc = normalizeOptionalString(resolvedIsrc);
|
||||
final normalizedComposer = normalizeOptionalString(composer);
|
||||
|
||||
return track.copyWith(
|
||||
coverUrl: normalizeCoverReference(track.coverUrl),
|
||||
isrc: (normalizedIsrc != null && _isValidISRC(normalizedIsrc))
|
||||
? normalizedIsrc
|
||||
: track.isrc,
|
||||
trackNumber: (track.trackNumber != null && track.trackNumber! > 0)
|
||||
? track.trackNumber
|
||||
: trackNumber,
|
||||
discNumber: (track.discNumber != null && track.discNumber! > 0)
|
||||
? track.discNumber
|
||||
: discNumber,
|
||||
totalDiscs: (track.totalDiscs != null && track.totalDiscs! > 0)
|
||||
? track.totalDiscs
|
||||
: totalDiscs,
|
||||
releaseDate: track.releaseDate ?? normalizeOptionalString(releaseDate),
|
||||
deezerId: deezerId ?? track.deezerId,
|
||||
totalTracks: (track.totalTracks != null && track.totalTracks! > 0)
|
||||
? track.totalTracks
|
||||
: totalTracks,
|
||||
composer: (track.composer != null && track.composer!.isNotEmpty)
|
||||
? track.composer
|
||||
: normalizedComposer,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isUsableIndex(int? number, int? total) {
|
||||
if (number == null || number <= 0) return false;
|
||||
return total == null || total <= 0 || number <= total;
|
||||
}
|
||||
|
||||
int? _resolvePositiveMetadataInt(int? sourceValue, int? backendValue) {
|
||||
if (sourceValue != null && sourceValue > 0) return sourceValue;
|
||||
return backendValue;
|
||||
}
|
||||
|
||||
int? _resolveMetadataIndex({
|
||||
required int? sourceValue,
|
||||
required int? backendValue,
|
||||
required int? total,
|
||||
}) {
|
||||
if (_isUsableIndex(sourceValue, total)) return sourceValue;
|
||||
if (_isUsableIndex(backendValue, total)) return backendValue;
|
||||
return sourceValue != null && sourceValue > 0 ? sourceValue : backendValue;
|
||||
}
|
||||
|
||||
String? _resolveMetadataText(String? sourceValue, String? backendValue) {
|
||||
return normalizeOptionalString(sourceValue) ??
|
||||
normalizeOptionalString(backendValue);
|
||||
}
|
||||
|
||||
/// Applies backend metadata fallbacks while preserving source-only fields.
|
||||
Track buildTrackForMetadataEmbedding(
|
||||
Track baseTrack,
|
||||
Map<String, dynamic> backendResult,
|
||||
String? resolvedAlbumArtist,
|
||||
) {
|
||||
final backendTrackNum = readPositiveInt(backendResult['track_number']);
|
||||
final backendDiscNum = readPositiveInt(backendResult['disc_number']);
|
||||
final backendTotalTracks = readPositiveInt(backendResult['total_tracks']);
|
||||
final backendTotalDiscs = readPositiveInt(backendResult['total_discs']);
|
||||
final backendYear = normalizeOptionalString(
|
||||
backendResult['release_date'] as String?,
|
||||
);
|
||||
final backendAlbum = normalizeOptionalString(
|
||||
backendResult['album'] as String?,
|
||||
);
|
||||
final backendIsrc = normalizeOptionalString(backendResult['isrc'] as String?);
|
||||
final backendCoverUrl = normalizeCoverReference(
|
||||
backendResult['cover_url']?.toString(),
|
||||
);
|
||||
final baseCoverUrl = normalizeCoverReference(baseTrack.coverUrl);
|
||||
final resolvedCoverUrl = baseCoverUrl ?? backendCoverUrl;
|
||||
final backendAlbumArtist = normalizeOptionalString(
|
||||
backendResult['album_artist'] as String?,
|
||||
);
|
||||
final backendComposer = normalizeOptionalString(
|
||||
backendResult['composer']?.toString(),
|
||||
);
|
||||
final sourceAlbumName = normalizeOptionalString(baseTrack.albumName);
|
||||
final sourceAlbumArtist = normalizeOptionalString(baseTrack.albumArtist);
|
||||
final sourceIsrc = normalizeOptionalString(baseTrack.isrc);
|
||||
final sourceReleaseDate = normalizeOptionalString(baseTrack.releaseDate);
|
||||
final sourceComposer = normalizeOptionalString(baseTrack.composer);
|
||||
final sourceAlbumType = normalizeOptionalString(baseTrack.albumType);
|
||||
final sourceGenre = normalizeOptionalString(baseTrack.genre);
|
||||
final sourceLabel = normalizeOptionalString(baseTrack.label);
|
||||
final sourceCopyright = normalizeOptionalString(baseTrack.copyright);
|
||||
final sourceComment = normalizeOptionalString(baseTrack.comment);
|
||||
final sourceUpc = normalizeOptionalString(baseTrack.upc);
|
||||
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 resolvedComment = backendComment ?? sourceComment;
|
||||
final backendAlbumType = normalizeOptionalString(
|
||||
backendResult['album_type']?.toString(),
|
||||
);
|
||||
final backendUpc = normalizeOptionalString(
|
||||
(backendResult['upc'] ?? backendResult['barcode'])?.toString(),
|
||||
);
|
||||
final backendExplicit = backendResult['explicit'] == true;
|
||||
final resolvedTotalTracks = _resolvePositiveMetadataInt(
|
||||
baseTrack.totalTracks,
|
||||
backendTotalTracks,
|
||||
);
|
||||
final resolvedTotalDiscs = _resolvePositiveMetadataInt(
|
||||
baseTrack.totalDiscs,
|
||||
backendTotalDiscs,
|
||||
);
|
||||
final resolvedTrackNumber = _resolveMetadataIndex(
|
||||
sourceValue: baseTrack.trackNumber,
|
||||
backendValue: backendTrackNum,
|
||||
total: resolvedTotalTracks,
|
||||
);
|
||||
final resolvedDiscNumber = _resolveMetadataIndex(
|
||||
sourceValue: baseTrack.discNumber,
|
||||
backendValue: backendDiscNum,
|
||||
total: resolvedTotalDiscs,
|
||||
);
|
||||
|
||||
final hasOverrides =
|
||||
resolvedTrackNumber != baseTrack.trackNumber ||
|
||||
resolvedDiscNumber != baseTrack.discNumber ||
|
||||
resolvedTotalTracks != baseTrack.totalTracks ||
|
||||
resolvedTotalDiscs != baseTrack.totalDiscs ||
|
||||
resolvedAlbumArtist != sourceAlbumArtist ||
|
||||
(sourceReleaseDate == null && backendYear != null) ||
|
||||
(sourceAlbumName == null && backendAlbum != null) ||
|
||||
(sourceIsrc == null && backendIsrc != null) ||
|
||||
(baseCoverUrl == null && backendCoverUrl != null) ||
|
||||
(sourceAlbumArtist == null &&
|
||||
resolvedAlbumArtist == null &&
|
||||
backendAlbumArtist != null) ||
|
||||
(sourceComposer == null && backendComposer != null) ||
|
||||
(sourceAlbumType == null && backendAlbumType != null) ||
|
||||
(sourceGenre == null && backendGenre != null) ||
|
||||
(sourceLabel == null && backendLabel != null) ||
|
||||
(sourceCopyright == null && backendCopyright != null) ||
|
||||
resolvedComment != sourceComment ||
|
||||
(baseTrack.explicit != true && backendExplicit) ||
|
||||
(sourceUpc == null && backendUpc != null);
|
||||
|
||||
if (!hasOverrides) {
|
||||
return baseTrack;
|
||||
}
|
||||
|
||||
return baseTrack.copyWith(
|
||||
albumName: sourceAlbumName ?? backendAlbum ?? baseTrack.albumName,
|
||||
albumArtist: resolvedAlbumArtist ?? sourceAlbumArtist ?? backendAlbumArtist,
|
||||
coverUrl: resolvedCoverUrl,
|
||||
isrc: sourceIsrc ?? backendIsrc,
|
||||
trackNumber: resolvedTrackNumber,
|
||||
discNumber: resolvedDiscNumber,
|
||||
totalDiscs: resolvedTotalDiscs,
|
||||
releaseDate: sourceReleaseDate ?? backendYear,
|
||||
albumType: sourceAlbumType ?? backendAlbumType,
|
||||
totalTracks: resolvedTotalTracks,
|
||||
composer: sourceComposer ?? backendComposer,
|
||||
genre: sourceGenre ?? backendGenre,
|
||||
label: sourceLabel ?? backendLabel,
|
||||
copyright: sourceCopyright ?? backendCopyright,
|
||||
comment: resolvedComment,
|
||||
explicit: baseTrack.explicit == true || backendExplicit
|
||||
? true
|
||||
: baseTrack.explicit,
|
||||
upc: sourceUpc ?? backendUpc,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1337,7 +1337,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
||||
item.track,
|
||||
settings,
|
||||
);
|
||||
final trackToDownload = _buildTrackForMetadataEmbedding(
|
||||
final trackToDownload = buildTrackForMetadataEmbedding(
|
||||
item.track,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
|
||||
@@ -327,7 +327,7 @@ class _DownloadRun {
|
||||
final enrichedComposer = normalizeOptionalString(
|
||||
data['composer']?.toString(),
|
||||
);
|
||||
trackToDownload = Track(
|
||||
trackToDownload = trackToDownload.copyWith(
|
||||
id: (data['spotify_id'] as String?) ?? trackToDownload.id,
|
||||
name: (data['name'] as String?) ?? trackToDownload.name,
|
||||
artistName:
|
||||
@@ -350,7 +350,6 @@ class _DownloadRun {
|
||||
totalDiscs: enrichedTotalDiscs ?? trackToDownload.totalDiscs,
|
||||
releaseDate: data['release_date'] as String?,
|
||||
deezerId: rawId,
|
||||
availability: trackToDownload.availability,
|
||||
albumType:
|
||||
(data['album_type'] as String?) ?? trackToDownload.albumType,
|
||||
totalTracks: enrichedTotalTracks ?? trackToDownload.totalTracks,
|
||||
@@ -360,10 +359,6 @@ class _DownloadRun {
|
||||
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:
|
||||
parseExplicitFlag(data['explicit']) ??
|
||||
trackToDownload.explicit,
|
||||
@@ -708,7 +703,7 @@ class _DownloadRun {
|
||||
decryptionDescriptor = DownloadDecryptionDescriptor.fromDownloadResult(
|
||||
result,
|
||||
);
|
||||
trackToDownload = n._buildTrackForMetadataEmbedding(
|
||||
trackToDownload = buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
@@ -1617,7 +1612,7 @@ class _DownloadRun {
|
||||
bool rebuildTrack = true,
|
||||
}) async {
|
||||
final track = rebuildTrack
|
||||
? n._buildTrackForMetadataEmbedding(
|
||||
? buildTrackForMetadataEmbedding(
|
||||
trackToDownload,
|
||||
result,
|
||||
resolvedAlbumArtist,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
|
||||
void main() {
|
||||
const track = Track(
|
||||
id: 'extension:123',
|
||||
name: 'Song',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
duration: 100,
|
||||
albumArtist: 'Album Artist',
|
||||
artistId: 'artist-id',
|
||||
albumId: 'album-id',
|
||||
coverUrl: 'https://example.test/cover',
|
||||
previewUrl: 'https://example.test/preview',
|
||||
upc: '012345678905',
|
||||
source: 'extension',
|
||||
audioQuality: 'LOSSLESS',
|
||||
audioModes: 'STEREO',
|
||||
explicit: false,
|
||||
genre: 'Rock',
|
||||
label: 'Label',
|
||||
copyright: 'Copyright',
|
||||
comment: 'Comment',
|
||||
availability: ServiceAvailability(deezer: true),
|
||||
);
|
||||
|
||||
test('identifier enrichment preserves every untouched Track field', () {
|
||||
final resolved = copyTrackWithResolvedMetadata(
|
||||
track,
|
||||
resolvedIsrc: ' USAAA2400001 ',
|
||||
deezerId: '123',
|
||||
composer: ' Composer ',
|
||||
);
|
||||
expect(resolved.toJson(), {
|
||||
...track.toJson(),
|
||||
'isrc': 'USAAA2400001',
|
||||
'deezerId': '123',
|
||||
'composer': 'Composer',
|
||||
});
|
||||
});
|
||||
|
||||
test('existing numbering wins, missing numbering is filled', () {
|
||||
final original = track.copyWith(
|
||||
trackNumber: 2,
|
||||
discNumber: 0,
|
||||
totalTracks: 12,
|
||||
composer: 'Original composer',
|
||||
isrc: 'USAAA2400001',
|
||||
);
|
||||
final resolved = copyTrackWithResolvedMetadata(
|
||||
original,
|
||||
resolvedIsrc: 'invalid',
|
||||
trackNumber: 9,
|
||||
totalTracks: 20,
|
||||
discNumber: 1,
|
||||
composer: 'Replacement',
|
||||
);
|
||||
expect(resolved.toJson(), {...original.toJson(), 'discNumber': 1});
|
||||
});
|
||||
|
||||
test('embedding fallbacks retain preview, UPC, and source metadata', () {
|
||||
final merged = buildTrackForMetadataEmbedding(track, {
|
||||
'track_number': 3,
|
||||
'total_tracks': 10,
|
||||
'isrc': 'USAAA2400001',
|
||||
'composer': 'Composer',
|
||||
'upc': '999999999999',
|
||||
'comment': 'Backend comment',
|
||||
}, track.albumArtist);
|
||||
expect(merged.toJson(), {
|
||||
...track.toJson(),
|
||||
'trackNumber': 3,
|
||||
'totalTracks': 10,
|
||||
'isrc': 'USAAA2400001',
|
||||
'composer': 'Composer',
|
||||
'comment': 'Backend comment',
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'embedding preserves valid source numbering and fixes invalid indices',
|
||||
() {
|
||||
final original = track.copyWith(
|
||||
trackNumber: 2,
|
||||
totalTracks: 10,
|
||||
discNumber: 9,
|
||||
totalDiscs: 2,
|
||||
);
|
||||
final merged = buildTrackForMetadataEmbedding(original, {
|
||||
'track_number': 5,
|
||||
'disc_number': 1,
|
||||
'total_tracks': 20,
|
||||
'total_discs': 3,
|
||||
}, original.albumArtist);
|
||||
expect(merged.toJson(), {...original.toJson(), 'discNumber': 1});
|
||||
},
|
||||
);
|
||||
|
||||
test('embedding without overrides reuses the original track', () {
|
||||
expect(
|
||||
identical(
|
||||
buildTrackForMetadataEmbedding(track, {}, track.albumArtist),
|
||||
track,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user