fix: stricter metadata matching, respect embedLyrics setting, improve Apple Music lyrics

- Re-enrich: reject candidates that don't match title/artist/album unless exact ISRC match
- Respect settings.embedLyrics instead of hardcoding true in re-enrich flows
- Skip lyrics resolution in NativeDownloadFinalizer when not needed
- Apple Music lyrics: use direct catalog API with token scraping instead of Paxsenix search
- Support ELRC/ELRCMultiPerson/Plain formats in Apple Music lyrics response
- Add confidence check in metadata auto-fill to prevent applying wrong metadata
- Add tests for stricter re-enrich matching logic
This commit is contained in:
zarzet
2026-06-02 00:58:40 +07:00
parent 0952b76e11
commit 822c094c8c
9 changed files with 387 additions and 43 deletions
+3 -2
View File
@@ -883,12 +883,13 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
List<String>? updateFields,
}) async {
final durationMs = (item.duration ?? 0) * 1000;
final artistTagMode = ref.read(settingsProvider).artistTagMode;
final settings = ref.read(settingsProvider);
final artistTagMode = settings.artistTagMode;
final request = <String, dynamic>{
'file_path': item.filePath,
'cover_url': '',
'max_quality': true,
'embed_lyrics': true,
'embed_lyrics': settings.embedLyrics,
'artist_tag_mode': artistTagMode,
'spotify_id': '',
'track_name': item.trackName,
+3 -2
View File
@@ -4408,12 +4408,13 @@ class _QueueTabState extends ConsumerState<QueueTab> {
List<String>? updateFields,
}) async {
final durationMs = (item.duration ?? 0) * 1000;
final artistTagMode = ref.read(settingsProvider).artistTagMode;
final settings = ref.read(settingsProvider);
final artistTagMode = settings.artistTagMode;
final request = <String, dynamic>{
'file_path': item.filePath,
'cover_url': '',
'max_quality': true,
'embed_lyrics': true,
'embed_lyrics': settings.embedLyrics,
'artist_tag_mode': artistTagMode,
'spotify_id': '',
'track_name': item.trackName,
@@ -599,6 +599,54 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
return score;
}
bool _metadataTextMatches(String current, String candidate) {
if (current.isEmpty || candidate.isEmpty) return false;
return current == candidate ||
candidate.contains(current) ||
current.contains(candidate);
}
bool _metadataMatchIsConfident(
Map<String, dynamic> track, {
required String currentTitle,
required String currentArtist,
required String currentAlbum,
required String currentIsrc,
}) {
final candidateIsrc = (track['isrc']?.toString() ?? '')
.trim()
.toUpperCase();
if (currentIsrc.isNotEmpty && candidateIsrc == currentIsrc) {
return true;
}
final candidateTitle = _normalizeMetadataText(
(track['name'] ?? track['title'] ?? '').toString(),
);
final candidateArtist = _normalizeMetadataText(
(track['artists'] ?? track['artist'] ?? '').toString(),
);
final candidateAlbum = _normalizeMetadataText(
(track['album_name'] ?? track['album'] ?? '').toString(),
);
final titleMatches = _metadataTextMatches(currentTitle, candidateTitle);
final artistMatches = _metadataTextMatches(currentArtist, candidateArtist);
final albumMatches = _metadataTextMatches(currentAlbum, candidateAlbum);
if (currentTitle.isNotEmpty && currentArtist.isNotEmpty) {
return titleMatches && artistMatches;
}
if (currentTitle.isNotEmpty && currentAlbum.isNotEmpty) {
return titleMatches && albumMatches;
}
if (currentTitle.isNotEmpty) {
return titleMatches;
}
return false;
}
Future<void> _fetchAndFill() async {
if (_autoFillFields.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -683,6 +731,24 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
best = result;
}
}
if (best != null &&
!_metadataMatchIsConfident(
best,
currentTitle: normalizedTitle,
currentArtist: normalizedArtist,
currentAlbum: normalizedAlbum,
currentIsrc: currentIsrc,
)) {
best = null;
}
if (best == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)),
);
return;
}
}
final selectedBest = best;
+3 -2
View File
@@ -2921,7 +2921,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
if (!_fileExists) return;
try {
final artistTagMode = ref.read(settingsProvider).artistTagMode;
final settings = ref.read(settingsProvider);
final artistTagMode = settings.artistTagMode;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.trackReEnrichSearching)),
);
@@ -2931,7 +2932,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
'file_path': cleanFilePath,
'cover_url': _coverUrl ?? '',
'max_quality': true,
'embed_lyrics': true,
'embed_lyrics': settings.embedLyrics,
'artist_tag_mode': artistTagMode,
'spotify_id': _spotifyId ?? '',
'track_name': trackName,