From 822c094c8cb9dd0c11f0e7f46fa5b566257222cd Mon Sep 17 00:00:00 2001 From: zarzet Date: Wed, 20 May 2026 23:16:51 +0700 Subject: [PATCH] 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 --- .../zarz/spotiflac/NativeDownloadFinalizer.kt | 9 +- go_backend/exports.go | 31 ++- go_backend/exports_test.go | 56 +++++ go_backend/lyrics_apple.go | 236 ++++++++++++++++-- go_backend/lyrics_supplement_test.go | 17 +- lib/screens/local_album_screen.dart | 5 +- lib/screens/queue_tab.dart | 5 +- lib/screens/track_metadata_edit_sheet.dart | 66 +++++ lib/screens/track_metadata_screen.dart | 5 +- 9 files changed, 387 insertions(+), 43 deletions(-) diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt index 75408f28..630ed6eb 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt @@ -1081,10 +1081,11 @@ object NativeDownloadFinalizer { 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 lyrics = resolveLyricsLrc(input) - val shouldEmbedLyrics = input.request.optBoolean("embed_lyrics", false) && - (input.request.optString("lyrics_mode", "embed") == "embed" || - input.request.optString("lyrics_mode", "embed") == "both") && + val lyricsMode = input.request.optString("lyrics_mode", "embed") + val shouldResolveLyrics = input.request.optBoolean("embed_lyrics", false) && + (lyricsMode == "embed" || lyricsMode == "both") + val lyrics = if (shouldResolveLyrics) resolveLyricsLrc(input) else "" + val shouldEmbedLyrics = shouldResolveLyrics && lyrics.isNotBlank() && lyrics != "[instrumental:true]" if (format == "flac") { diff --git a/go_backend/exports.go b/go_backend/exports.go index 1c353cc8..32717325 100644 --- a/go_backend/exports.go +++ b/go_backend/exports.go @@ -600,6 +600,10 @@ func selectBestReEnrichTrack(req reEnrichRequest, tracks []ExtTrackMetadata) *Ex for i := range tracks { track := &tracks[i] score := 0 + exactISRCMatch := currentISRC != "" && strings.EqualFold(currentISRC, strings.TrimSpace(track.ISRC)) + titleMatches := req.TrackName != "" && track.Name != "" && titlesMatch(req.TrackName, track.Name) + artistMatches := req.ArtistName != "" && track.Artists != "" && artistsMatch(req.ArtistName, track.Artists) + albumMatches := currentAlbum != "" && track.AlbumName != "" && titlesMatch(currentAlbum, track.AlbumName) resolved := resolvedTrackInfo{ Title: track.Name, @@ -607,22 +611,39 @@ func selectBestReEnrichTrack(req reEnrichRequest, tracks []ExtTrackMetadata) *Ex ISRC: track.ISRC, Duration: track.DurationMS / 1000, } - if trackMatchesRequest(downloadReq, resolved, "ReEnrich") { + verified := trackMatchesRequest(downloadReq, resolved, "ReEnrich") + + if !exactISRCMatch { + if req.TrackName != "" && !titleMatches { + continue + } + if req.ArtistName != "" && !artistMatches { + continue + } + if req.TrackName == "" && req.ArtistName == "" && currentAlbum != "" && !albumMatches { + continue + } + if req.TrackName == "" && req.ArtistName == "" && currentAlbum == "" && !verified { + continue + } + } + + if verified { score += 2000 } - if currentISRC != "" && strings.EqualFold(currentISRC, strings.TrimSpace(track.ISRC)) { + if exactISRCMatch { score += 10000 } - if req.TrackName != "" && track.Name != "" && titlesMatch(req.TrackName, track.Name) { + if titleMatches { score += 400 } - if req.ArtistName != "" && track.Artists != "" && artistsMatch(req.ArtistName, track.Artists) { + if artistMatches { score += 320 } if currentAlbum != "" && track.AlbumName != "" { switch { - case titlesMatch(currentAlbum, track.AlbumName): + case albumMatches: score += 120 case strings.Contains(strings.ToLower(track.AlbumName), strings.ToLower(currentAlbum)), strings.Contains(strings.ToLower(currentAlbum), strings.ToLower(track.AlbumName)): diff --git a/go_backend/exports_test.go b/go_backend/exports_test.go index 21fc3bc6..7ca487a5 100644 --- a/go_backend/exports_test.go +++ b/go_backend/exports_test.go @@ -407,6 +407,62 @@ func TestSelectBestReEnrichTrackPrefersCandidateWithReleaseDate(t *testing.T) { } } +func TestSelectBestReEnrichTrackRejectsMismatchedSearchResults(t *testing.T) { + req := reEnrichRequest{ + TrackName: "Song Title", + ArtistName: "Artist Name", + AlbumName: "Album Name", + DurationMs: 180000, + } + + tracks := []ExtTrackMetadata{ + { + ID: "wrong-rich-metadata", + Name: "Different Song", + Artists: "Different Artist", + AlbumName: "Album Name", + DurationMS: 180000, + ReleaseDate: "2024-03-09", + TrackNumber: 4, + DiscNumber: 1, + ISRC: "WRONG1234567", + ProviderID: "deezer", + }, + } + + if best := selectBestReEnrichTrack(req, tracks); best != nil { + t.Fatalf("selected track = %q, want no match", best.ID) + } +} + +func TestSelectBestReEnrichTrackAllowsExactISRCDespiteMetadataMismatch(t *testing.T) { + req := reEnrichRequest{ + TrackName: "Song Title", + ArtistName: "Artist Name", + ISRC: "USRC17607839", + DurationMs: 999999000, + } + + tracks := []ExtTrackMetadata{ + { + ID: "same-isrc", + Name: "Different Song", + Artists: "Different Artist", + DurationMS: 180000, + ISRC: "USRC17607839", + ProviderID: "deezer", + }, + } + + best := selectBestReEnrichTrack(req, tracks) + if best == nil { + t.Fatal("expected exact ISRC candidate to be selected") + } + if best.ID != "same-isrc" { + t.Fatalf("selected track = %q, want exact ISRC candidate", best.ID) + } +} + func TestBuildReEnrichFFmpegMetadataOmitsEmptyFields(t *testing.T) { req := reEnrichRequest{ TrackName: "Song", diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go index 3c36e88e..9e9decf5 100644 --- a/go_backend/lyrics_apple.go +++ b/go_backend/lyrics_apple.go @@ -7,7 +7,9 @@ import ( "math" "net/http" "net/url" + "regexp" "strings" + "sync" "time" ) @@ -15,6 +17,8 @@ type AppleMusicClient struct { httpClient *http.Client } +const appleMusicCatalogBaseURL = "https://amp-api.music.apple.com/v1/catalog/us" + type appleMusicSearchResult struct { ID string `json:"id"` SongName string `json:"songName"` @@ -23,9 +27,33 @@ type appleMusicSearchResult struct { Duration int `json:"duration"` } +type appleMusicCatalogSearchResponse struct { + Results struct { + Songs *struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } `json:"songs"` + } `json:"results"` + Resources *struct { + Songs map[string]struct { + Attributes struct { + Name string `json:"name"` + ArtistName string `json:"artistName"` + AlbumName string `json:"albumName"` + DurationInMillis int `json:"durationInMillis"` + } `json:"attributes"` + } `json:"songs"` + } `json:"resources"` +} + type paxResponse struct { - Type string `json:"type"` // "Syllable" or "Line" - Content []paxLyrics `json:"content"` // List of lyric lines + Type string `json:"type"` // "Syllable" or "Line" + Content []paxLyrics `json:"content"` + ELRC string `json:"elrc"` + ELRCMultiPerson string `json:"elrcMultiPerson"` + Plain string `json:"plain"` + TTMLContent string `json:"ttmlContent"` } type paxLyrics struct { @@ -44,6 +72,11 @@ type paxLyricDetail struct { EndTime *int `json:"endtime"` } +var ( + appleMusicTokenMu sync.Mutex + appleMusicCachedToken string +) + func NewAppleMusicClient() *AppleMusicClient { return &AppleMusicClient{ httpClient: NewMetadataHTTPClient(20 * time.Second), @@ -100,36 +133,164 @@ func selectBestAppleMusicSearchResult(results []appleMusicSearchResult, trackNam return &results[bestIndex] } +func (c *AppleMusicClient) getAppleMusicToken() (string, error) { + appleMusicTokenMu.Lock() + defer appleMusicTokenMu.Unlock() + + if appleMusicCachedToken != "" { + return appleMusicCachedToken, nil + } + + req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) + if err != nil { + return "", fmt.Errorf("failed to create apple music page request: %w", err) + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + resp, err := c.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch apple music page: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("apple music page returned HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read apple music page: %w", err) + } + + indexPath := regexp.MustCompile(`/assets/index~[^"' <]+\.js`).FindString(string(body)) + if indexPath == "" { + return "", fmt.Errorf("apple music index script not found") + } + + jsReq, err := http.NewRequest("GET", "https://beta.music.apple.com"+indexPath, nil) + if err != nil { + return "", fmt.Errorf("failed to create apple music script request: %w", err) + } + jsReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + + jsResp, err := c.httpClient.Do(jsReq) + if err != nil { + return "", fmt.Errorf("failed to fetch apple music script: %w", err) + } + defer jsResp.Body.Close() + + if jsResp.StatusCode != http.StatusOK { + return "", fmt.Errorf("apple music script returned HTTP %d", jsResp.StatusCode) + } + + jsBody, err := io.ReadAll(jsResp.Body) + if err != nil { + return "", fmt.Errorf("failed to read apple music script: %w", err) + } + + token := regexp.MustCompile(`eyJh[^"' <]+`).FindString(string(jsBody)) + if token == "" { + return "", fmt.Errorf("apple music token not found") + } + + appleMusicCachedToken = token + return token, nil +} + +func clearAppleMusicToken() { + appleMusicTokenMu.Lock() + defer appleMusicTokenMu.Unlock() + appleMusicCachedToken = "" +} + +func (c *AppleMusicClient) searchSongWithToken(token, query string) ([]appleMusicSearchResult, error) { + params := url.Values{} + params.Set("term", query) + params.Set("types", "songs") + params.Set("limit", "25") + params.Set("l", "en-US") + params.Set("platform", "web") + params.Set("format[resources]", "map") + params.Set("include[songs]", "artists") + params.Set("extend", "artistUrl") + + searchURL := appleMusicCatalogBaseURL + "/search?" + params.Encode() + req, err := http.NewRequest("GET", searchURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create apple music catalog request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Origin", "https://music.apple.com") + req.Header.Set("Referer", "https://music.apple.com/") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0") + req.Header.Set("Accept", "application/json") + req.Header.Set("Accept-Language", "en-US,en;q=0.5") + req.Header.Set("x-apple-renewal", "true") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("apple music catalog search failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusUnauthorized { + return nil, fmt.Errorf("apple music catalog search unauthorized") + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("apple music catalog search returned HTTP %d", resp.StatusCode) + } + + var searchResp appleMusicCatalogSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return nil, fmt.Errorf("failed to decode apple music catalog response: %w", err) + } + + if searchResp.Results.Songs == nil || searchResp.Resources == nil { + return nil, nil + } + + results := make([]appleMusicSearchResult, 0, len(searchResp.Results.Songs.Data)) + for _, item := range searchResp.Results.Songs.Data { + detail, ok := searchResp.Resources.Songs[item.ID] + if !ok { + continue + } + attr := detail.Attributes + results = append(results, appleMusicSearchResult{ + ID: item.ID, + SongName: attr.Name, + ArtistName: attr.ArtistName, + AlbumName: attr.AlbumName, + Duration: attr.DurationInMillis, + }) + } + + return results, nil +} + func (c *AppleMusicClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { query := trackName + " " + artistName if strings.TrimSpace(query) == "" { return "", fmt.Errorf("empty search query") } - encodedQuery := url.QueryEscape(query) - searchURL := fmt.Sprintf("https://lyrics.paxsenix.org/apple-music/search?q=%s", encodedQuery) - - req, err := http.NewRequest("GET", searchURL, nil) + token, err := c.getAppleMusicToken() if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) + return "", err } - req.Header.Set("User-Agent", appUserAgent()) - req.Header.Set("Accept", "application/json") - - resp, err := c.httpClient.Do(req) + searchResp, err := c.searchSongWithToken(token, strings.TrimSpace(query)) + if err != nil && strings.Contains(strings.ToLower(err.Error()), "unauthorized") { + clearAppleMusicToken() + token, tokenErr := c.getAppleMusicToken() + if tokenErr != nil { + return "", tokenErr + } + searchResp, err = c.searchSongWithToken(token, strings.TrimSpace(query)) + } if err != nil { - return "", fmt.Errorf("apple music search failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return "", fmt.Errorf("apple music search returned HTTP %d", resp.StatusCode) - } - - var searchResp []appleMusicSearchResult - if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { - return "", fmt.Errorf("failed to decode apple music response: %w", err) + return "", err } best := selectBestAppleMusicSearchResult(searchResp, trackName, artistName, durationSec) @@ -174,8 +335,33 @@ func (c *AppleMusicClient) FetchLyricsByID(songID string) (string, error) { } func formatPaxLyricsToLRC(rawJSON string, multiPersonWordByWord bool, preserveWordTiming bool) (string, error) { + var stringPayload string + if err := json.Unmarshal([]byte(rawJSON), &stringPayload); err == nil { + stringPayload = strings.TrimSpace(stringPayload) + if stringPayload != "" { + return stringPayload, nil + } + } + var paxResp paxResponse - if err := json.Unmarshal([]byte(rawJSON), &paxResp); err == nil && paxResp.Content != nil { + if err := json.Unmarshal([]byte(rawJSON), &paxResp); err == nil && + (paxResp.Content != nil || + strings.TrimSpace(paxResp.ELRCMultiPerson) != "" || + strings.TrimSpace(paxResp.ELRC) != "" || + strings.TrimSpace(paxResp.Plain) != "" || + strings.TrimSpace(paxResp.TTMLContent) != "") { + if preserveWordTiming && multiPersonWordByWord && strings.TrimSpace(paxResp.ELRCMultiPerson) != "" { + return strings.TrimSpace(paxResp.ELRCMultiPerson), nil + } + if preserveWordTiming && strings.TrimSpace(paxResp.ELRC) != "" { + return strings.TrimSpace(paxResp.ELRC), nil + } + if strings.TrimSpace(paxResp.Plain) != "" && len(paxResp.Content) == 0 { + return strings.TrimSpace(paxResp.Plain), nil + } + if len(paxResp.Content) == 0 { + return "", fmt.Errorf("unsupported apple music lyrics payload") + } return formatPaxContent(paxResp.Type, paxResp.Content, multiPersonWordByWord, preserveWordTiming), nil } @@ -270,6 +456,10 @@ func (c *AppleMusicClient) FetchLyrics( lrcText, err := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord, preserveWordTiming) if err != nil { + trimmedRaw := strings.TrimSpace(rawLyrics) + if strings.HasPrefix(trimmedRaw, "{") || strings.HasPrefix(trimmedRaw, "[") { + return nil, err + } lrcText = rawLyrics } diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index 9199d111..ddc56f2f 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -131,14 +131,18 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) { } func TestExternalLyricsProvidersWithFakeHTTP(t *testing.T) { + clearAppleMusicToken() + defer clearAppleMusicToken() + paxJSON := `{"type":"Syllable","content":[{"timestamp":1000,"oppositeTurn":true,"background":true,"text":[{"text":"Hel","part":true,"timestamp":1000},{"text":"lo","part":false,"timestamp":1200,"endtime":1500}],"backgroundText":[{"text":"bg","part":false,"timestamp":900}]}]}` apple := &AppleMusicClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { switch { - case strings.Contains(req.URL.Path, "/apple-music/search"): - if req.URL.Query().Get("q") == "bad" { - return &http.Response{StatusCode: 500, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`error`)), Request: req}, nil - } - return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"id":"apple-2","songName":"Other","artistName":"Other","duration":1000},{"id":"apple-1","songName":"Song","artistName":"Artist","albumName":"Album","duration":180000}]`)), Request: req}, nil + case req.URL.Host == "beta.music.apple.com" && (req.URL.Path == "" || req.URL.Path == "/"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(``)), Request: req}, nil + case req.URL.Host == "beta.music.apple.com" && req.URL.Path == "/assets/index~test.js": + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`const token="eyJhbGci.test";`)), Request: req}, nil + case req.URL.Host == "amp-api.music.apple.com" && strings.Contains(req.URL.Path, "/v1/catalog/us/search"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"results":{"songs":{"data":[{"id":"apple-2"},{"id":"apple-1"}]}},"resources":{"songs":{"apple-2":{"attributes":{"name":"Other","artistName":"Other","durationInMillis":1000}},"apple-1":{"attributes":{"name":"Song","artistName":"Artist","albumName":"Album","durationInMillis":180000}}}}}`)), Request: req}, nil case strings.Contains(req.URL.Path, "/apple-music/lyrics"): return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(paxJSON)), Request: req}, nil default: @@ -177,6 +181,9 @@ func TestExternalLyricsProvidersWithFakeHTTP(t *testing.T) { if !strings.Contains(elrc, "<00:") { t.Fatalf("elrc pax should include inline word timing: %q", elrc) } + if preferred, err := formatPaxLyricsToLRC(`{"elrcMultiPerson":"[00:01.00]v1:<00:01.00>Hello","content":[{"timestamp":1000,"text":[{"text":"Fallback","part":false}]}]}`, true, true); err != nil || !strings.Contains(preferred, "Hello") { + t.Fatalf("preferred apple elrc = %q/%v", preferred, err) + } if _, err := apple.SearchSong("", "", 0); err == nil { t.Fatal("expected empty apple search error") } diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 919506f4..8fdb37a6 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -883,12 +883,13 @@ class _LocalAlbumScreenState extends ConsumerState { List? 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 = { '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, diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 347d1eb2..e0f304a3 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -4408,12 +4408,13 @@ class _QueueTabState extends ConsumerState { List? 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 = { '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, diff --git a/lib/screens/track_metadata_edit_sheet.dart b/lib/screens/track_metadata_edit_sheet.dart index dca0d43b..d22e77ec 100644 --- a/lib/screens/track_metadata_edit_sheet.dart +++ b/lib/screens/track_metadata_edit_sheet.dart @@ -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 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 _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; diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index ec4a014b..705343a2 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2921,7 +2921,8 @@ class _TrackMetadataScreenState extends ConsumerState { 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 { '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,