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,