diff --git a/go_backend/cross_extension_share.go b/go_backend/cross_extension_share.go index 18cdb11c..5f132b62 100644 --- a/go_backend/cross_extension_share.go +++ b/go_backend/cross_extension_share.go @@ -333,7 +333,7 @@ func resolveCollectionShareURL(ext *loadedExtension, itemType string, track *Ext if url := urlFromExternalLinks(track.ExternalLinks, "album"); url != "" { return url } - if url := templateShareURL(ext, "album", firstNonEmptyString(track.AlbumID, collectionID(*track, "album"), track.AlbumURL)); url != "" { + if url := templateShareURL(ext, "album", firstNonEmptyTrimmed(track.AlbumID, collectionID(*track, "album"), track.AlbumURL)); url != "" { return url } return "" @@ -350,7 +350,7 @@ func resolveCollectionShareURL(ext *loadedExtension, itemType string, track *Ext if url := urlFromExternalLinks(track.ExternalLinks, "artist"); url != "" { return url } - if url := templateShareURL(ext, "artist", firstNonEmptyString(track.ArtistID, collectionID(*track, "artist"))); url != "" { + if url := templateShareURL(ext, "artist", firstNonEmptyTrimmed(track.ArtistID, collectionID(*track, "artist"))); url != "" { return url } return "" @@ -430,13 +430,3 @@ func stripProviderPrefix(id string) string { } return id } - -func firstNonEmptyString(values ...string) string { - for _, value := range values { - trimmed := strings.TrimSpace(value) - if trimmed != "" { - return trimmed - } - } - return "" -} diff --git a/go_backend/exports_extensions.go b/go_backend/exports_extensions.go index 781b0f10..d5e67ccf 100644 --- a/go_backend/exports_extensions.go +++ b/go_backend/exports_extensions.go @@ -783,29 +783,7 @@ func CustomSearchWithExtensionJSONWithRequestID(extensionID, query string, optio result := make([]map[string]any, len(tracks)) for i, track := range tracks { - result[i] = map[string]any{ - "id": track.ID, - "name": track.Name, - "artists": track.Artists, - "album_name": track.AlbumName, - "album_artist": track.AlbumArtist, - "duration_ms": track.DurationMS, - "images": track.ResolvedCoverURL(), - "preview_url": track.PreviewURL, - "release_date": track.ReleaseDate, - "track_number": track.TrackNumber, - "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, - "composer": track.Composer, - "audio_quality": track.AudioQuality, - "audio_modes": track.AudioModes, - "explicit": track.Explicit, - } + result[i] = normalizeExtensionTrackMetadataMap(track, "", 0) } return marshalJSONString(result) @@ -835,51 +813,13 @@ func HandleURLWithExtensionJSON(url string) (string, error) { } if result.Track != nil { - response["track"] = map[string]any{ - "id": result.Track.ID, - "name": result.Track.Name, - "artists": result.Track.Artists, - "album_name": result.Track.AlbumName, - "album_artist": result.Track.AlbumArtist, - "duration_ms": result.Track.DurationMS, - "images": result.Track.ResolvedCoverURL(), - "preview_url": result.Track.PreviewURL, - "release_date": result.Track.ReleaseDate, - "track_number": result.Track.TrackNumber, - "total_tracks": result.Track.TotalTracks, - "disc_number": result.Track.DiscNumber, - "total_discs": result.Track.TotalDiscs, - "isrc": result.Track.ISRC, - "provider_id": result.Track.ProviderID, - "composer": result.Track.Composer, - "explicit": result.Track.Explicit, - } + response["track"] = normalizeExtensionTrackMetadataMap(*result.Track, "", 0) } if len(result.Tracks) > 0 { tracks := make([]map[string]any, len(result.Tracks)) for i, track := range result.Tracks { - tracks[i] = map[string]any{ - "id": track.ID, - "name": track.Name, - "artists": track.Artists, - "album_name": track.AlbumName, - "album_artist": track.AlbumArtist, - "duration_ms": track.DurationMS, - "images": track.ResolvedCoverURL(), - "preview_url": track.PreviewURL, - "release_date": track.ReleaseDate, - "track_number": track.TrackNumber, - "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, - "composer": track.Composer, - "explicit": track.Explicit, - } + tracks[i] = normalizeExtensionTrackMetadataMap(track, "", 0) } response["tracks"] = tracks } @@ -958,26 +898,7 @@ func HandleURLWithExtensionJSON(url string) (string, error) { if len(result.Artist.TopTracks) > 0 { topTracks := make([]map[string]any, len(result.Artist.TopTracks)) for i, track := range result.Artist.TopTracks { - topTracks[i] = map[string]any{ - "id": track.ID, - "name": track.Name, - "artists": track.Artists, - "album_name": track.AlbumName, - "album_artist": track.AlbumArtist, - "duration_ms": track.DurationMS, - "images": track.ResolvedCoverURL(), - "preview_url": track.PreviewURL, - "release_date": track.ReleaseDate, - "track_number": track.TrackNumber, - "total_tracks": track.TotalTracks, - "disc_number": track.DiscNumber, - "total_discs": track.TotalDiscs, - "isrc": track.ISRC, - "provider_id": track.ProviderID, - "spotify_id": track.SpotifyID, - "composer": track.Composer, - "explicit": track.Explicit, - } + topTracks[i] = normalizeExtensionTrackMetadataMap(track, "", 0) } artistResponse["top_tracks"] = topTracks } diff --git a/go_backend/extension_fallback.go b/go_backend/extension_fallback.go index d65d63fe..d5234077 100644 --- a/go_backend/extension_fallback.go +++ b/go_backend/extension_fallback.go @@ -879,7 +879,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro return &DownloadResponse{ Success: false, Error: "Download failed: " + lastErr.Error(), - ErrorType: firstNonEmptyString(lastErrType, "extension_error"), + ErrorType: firstNonEmptyTrimmed(lastErrType, "extension_error"), RetryAfterSeconds: lastRetryAfterSeconds, Service: req.Source, }, nil @@ -1013,7 +1013,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro } if lastErr != nil { - errorType := firstNonEmptyString(lastErrType, classifyDownloadErrorType(lastErr.Error())) + errorType := firstNonEmptyTrimmed(lastErrType, classifyDownloadErrorType(lastErr.Error())) if errorType == "unknown" { errorType = "not_found" } diff --git a/go_backend/extension_provider_wrapper.go b/go_backend/extension_provider_wrapper.go index 73fbeabe..eff9f561 100644 --- a/go_backend/extension_provider_wrapper.go +++ b/go_backend/extension_provider_wrapper.go @@ -151,11 +151,15 @@ func invokeExtensionMethod(vm *goja.Runtime, method string, args ...any) (goja.V return goja.Null(), nil } + return callable(extensionObject, gojaArgumentValues(vm, args)...) +} + +func gojaArgumentValues(vm *goja.Runtime, args []any) []goja.Value { values := make([]goja.Value, len(args)) for i, arg := range args { values[i] = gojaArgumentValue(vm, arg) } - return callable(extensionObject, values...) + return values } func gojaArgumentValue(vm *goja.Runtime, value any) goja.Value { @@ -214,11 +218,7 @@ func invokeExtensionOrGlobal(vm *goja.Runtime, method string, args ...any) (goja if !ok { return goja.Null(), nil } - values := make([]goja.Value, len(args)) - for i, arg := range args { - values[i] = gojaArgumentValue(vm, arg) - } - return callable(vm.GlobalObject(), values...) + return callable(vm.GlobalObject(), gojaArgumentValues(vm, args)...) } func extensionTrackInput(track *ExtTrackMetadata) map[string]any { diff --git a/go_backend/extension_repo.go b/go_backend/extension_repo.go index 5b5633d6..6c1e4b31 100644 --- a/go_backend/extension_repo.go +++ b/go_backend/extension_repo.go @@ -77,7 +77,7 @@ func (e *repoExtension) getMinAppVersion() string { } func (e *repoExtension) getRawSHA256() string { - return firstNonEmpty(e.SHA256, e.ChecksumSHA256, e.ChecksumAlt) + return firstNonEmptyTrimmed(e.SHA256, e.ChecksumSHA256, e.ChecksumAlt) } func (e *repoExtension) getSHA256() string { @@ -443,15 +443,6 @@ func (s *extensionRepo) downloadExtension(extensionID string, destPath string) e const maxExtensionPackageBytes int64 = 64 * 1024 * 1024 -func firstNonEmpty(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return value - } - } - return "" -} - func normalizeSHA256(value string) string { normalized := strings.ToLower(strings.TrimSpace(value)) normalized = strings.TrimPrefix(normalized, "sha256:") diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index 71b78c62..128ec04f 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -508,10 +508,6 @@ func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsRes return c.parseLRCLibResponse(&lrcResp), nil } -func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec float64) (*LyricsResponse, error) { - return c.fetchLyricsFromLRCLibSearch(query, "", "", durationSec) -} - func (c *LyricsClient) fetchLyricsFromLRCLibSearch(query, trackName, artistName string, durationSec float64) (*LyricsResponse, error) { params := url.Values{} params.Set("q", query) diff --git a/go_backend/lyrics_apple.go b/go_backend/lyrics_apple.go index 46c2e2e3..64227976 100644 --- a/go_backend/lyrics_apple.go +++ b/go_backend/lyrics_apple.go @@ -99,36 +99,15 @@ func appleMusicSearchResultMatches(result appleMusicSearchResult, trackName, art } func selectBestAppleMusicSearchResult(results []appleMusicSearchResult, trackName, artistName string, durationSec float64) *appleMusicSearchResult { - if len(results) == 0 { - return nil - } - - bestIndex := -1 - bestScore := -1 - for i := range results { + best := selectBestLyricsCandidate(len(results), trackName, artistName, durationSec, func(i int) (string, string, float64, bool) { result := &results[i] - if !appleMusicSearchResultMatches(*result, trackName, artistName, durationSec) { - continue - } - - score := scoreLyricsSearchCandidate( - result.SongName, - result.ArtistName, - float64(result.Duration)/1000.0, - trackName, - artistName, - durationSec, - ) - if score > bestScore { - bestScore = score - bestIndex = i - } - } - - if bestIndex < 0 { + ok := appleMusicSearchResultMatches(*result, trackName, artistName, durationSec) + return result.SongName, result.ArtistName, float64(result.Duration) / 1000.0, ok + }) + if best < 0 { return nil } - return &results[bestIndex] + return &results[best] } func (c *AppleMusicClient) getAppleMusicToken() (string, error) { diff --git a/go_backend/lyrics_netease.go b/go_backend/lyrics_netease.go index c7db3e29..feaefeac 100644 --- a/go_backend/lyrics_netease.go +++ b/go_backend/lyrics_netease.go @@ -114,9 +114,7 @@ func (c *NeteaseClient) SearchSong(trackName, artistName string) (int64, error) } func selectBestNeteaseSearchResult(results []neteaseSearchSong, trackName, artistName string) *neteaseSearchSong { - bestIndex := -1 - bestScore := -1 - for i := range results { + best := selectBestLyricsCandidate(len(results), trackName, artistName, 0, func(i int) (string, string, float64, bool) { result := &results[i] artists := make([]string, 0, len(result.Artists)) for _, artist := range result.Artists { @@ -125,20 +123,14 @@ func selectBestNeteaseSearchResult(results []neteaseSearchSong, trackName, artis } } candidateArtist := strings.Join(artists, ", ") - if !lyricsSearchTitlesMatch(result.Name, trackName, false) || - !lyricsSearchArtistsMatch(candidateArtist, artistName) { - continue - } - score := scoreLyricsSearchCandidate(result.Name, candidateArtist, 0, trackName, artistName, 0) - if score > bestScore { - bestIndex = i - bestScore = score - } - } - if bestIndex < 0 { + ok := lyricsSearchTitlesMatch(result.Name, trackName, false) && + lyricsSearchArtistsMatch(candidateArtist, artistName) + return result.Name, candidateArtist, 0, ok + }) + if best < 0 { return nil } - return &results[bestIndex] + return &results[best] } func (c *NeteaseClient) FetchLyricsByID(songID int64, includeTranslation, includeRomanization bool) (string, error) { diff --git a/go_backend/lyrics_paxsenix.go b/go_backend/lyrics_paxsenix.go index 05d39fb7..97463ba5 100644 --- a/go_backend/lyrics_paxsenix.go +++ b/go_backend/lyrics_paxsenix.go @@ -265,31 +265,39 @@ func (c *SpotifyLyricsClient) SearchSong(trackName, artistName string, durationS return strings.TrimSpace(best.TrackID), nil } -func selectBestSpotifyLyricsSearchResult(results []spotifyLyricsSearchResult, trackName, artistName string, durationSec float64) *spotifyLyricsSearchResult { - if len(results) == 0 { - return nil - } - +// selectBestLyricsCandidate returns the index of the highest-scoring candidate +// whose provider-specific gate passes; candidate returns +// (name, artist, durationSec, gatePassed). Returns -1 when nothing matches. +func selectBestLyricsCandidate(n int, trackName, artistName string, durationSec float64, candidate func(i int) (string, string, float64, bool)) int { bestIndex := -1 bestScore := -1 - for i := range results { - result := &results[i] - candidateDuration := parseClockDuration(result.Duration) - if !lyricsSearchTitlesMatch(result.Name, trackName, false) || - !lyricsSearchArtistsMatch(result.ArtistName, artistName) || - !lyricsSearchDurationMatches(candidateDuration, durationSec) { + for i := 0; i < n; i++ { + name, artist, duration, ok := candidate(i) + if !ok { continue } - score := scoreLyricsSearchCandidate(result.Name, result.ArtistName, candidateDuration, trackName, artistName, durationSec) + score := scoreLyricsSearchCandidate(name, artist, duration, trackName, artistName, durationSec) if score > bestScore { bestIndex = i bestScore = score } } - if bestIndex < 0 { + return bestIndex +} + +func selectBestSpotifyLyricsSearchResult(results []spotifyLyricsSearchResult, trackName, artistName string, durationSec float64) *spotifyLyricsSearchResult { + best := selectBestLyricsCandidate(len(results), trackName, artistName, durationSec, func(i int) (string, string, float64, bool) { + result := &results[i] + duration := parseClockDuration(result.Duration) + ok := lyricsSearchTitlesMatch(result.Name, trackName, false) && + lyricsSearchArtistsMatch(result.ArtistName, artistName) && + lyricsSearchDurationMatches(duration, durationSec) + return result.Name, result.ArtistName, duration, ok + }) + if best < 0 { return nil } - return &results[bestIndex] + return &results[best] } func (c *SpotifyLyricsClient) FetchLyricsByID(trackID string) (*LyricsResponse, error) { @@ -383,32 +391,20 @@ func (c *YouTubeLyricsClient) SearchSong(trackName, artistName string, durationS } func selectBestYouTubeLyricsSearchResult(results []youtubeLyricsSearchResult, trackName, artistName string, durationSec float64) *youtubeLyricsSearchResult { - if len(results) == 0 { - return nil - } - - bestIndex := -1 - bestScore := -1 - for i := range results { + best := selectBestLyricsCandidate(len(results), trackName, artistName, durationSec, func(i int) (string, string, float64, bool) { result := &results[i] - candidateDuration := parseClockDuration(result.Duration) + duration := parseClockDuration(result.Duration) artistMatches := lyricsSearchArtistsMatch(result.Author, artistName) || lyricsSearchArtistAppearsInTitle(result.Title, artistName) - if !lyricsSearchTitlesMatch(result.Title, trackName, true) || - !artistMatches || - !lyricsSearchDurationMatches(candidateDuration, durationSec) { - continue - } - score := scoreLyricsSearchCandidate(result.Title, result.Author, candidateDuration, trackName, artistName, durationSec) - if score > bestScore { - bestIndex = i - bestScore = score - } - } - if bestIndex < 0 { + ok := lyricsSearchTitlesMatch(result.Title, trackName, true) && + artistMatches && + lyricsSearchDurationMatches(duration, durationSec) + return result.Title, result.Author, duration, ok + }) + if best < 0 { return nil } - return &results[bestIndex] + return &results[best] } func (c *YouTubeLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { @@ -451,29 +447,17 @@ func (c *KugouLyricsClient) SearchSong(trackName, artistName string, durationSec } func selectBestKugouLyricsSearchResult(results []kugouLyricsSearchResult, trackName, artistName string, durationSec float64) *kugouLyricsSearchResult { - if len(results) == 0 { - return nil - } - - bestIndex := -1 - bestScore := -1 - for i := range results { + best := selectBestLyricsCandidate(len(results), trackName, artistName, durationSec, func(i int) (string, string, float64, bool) { result := &results[i] - if !lyricsSearchTitlesMatch(result.Title, trackName, false) || - !lyricsSearchArtistsMatch(result.Artist, artistName) || - !lyricsSearchDurationMatches(result.Duration, durationSec) { - continue - } - score := scoreLyricsSearchCandidate(result.Title, result.Artist, result.Duration, trackName, artistName, durationSec) - if score > bestScore { - bestIndex = i - bestScore = score - } - } - if bestIndex < 0 { + ok := lyricsSearchTitlesMatch(result.Title, trackName, false) && + lyricsSearchArtistsMatch(result.Artist, artistName) && + lyricsSearchDurationMatches(result.Duration, durationSec) + return result.Title, result.Artist, result.Duration, ok + }) + if best < 0 { return nil } - return &results[bestIndex] + return &results[best] } func (c *KugouLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index c57f2877..3598d502 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -123,9 +123,9 @@ func TestLyricsCacheParsingAndLRCLibClient(t *testing.T) { if err != nil || got.SyncType != "LINE_SYNCED" || len(got.Lines) != 1 { t.Fatalf("FetchLyricsWithMetadata = %#v/%v", got, err) } - search, err := client.FetchLyricsFromLRCLibSearch("Artist Song", 180) + search, err := client.fetchLyricsFromLRCLibSearch("Artist Song", "", "", 180) if err != nil || len(search.Lines) == 0 { - t.Fatalf("FetchLyricsFromLRCLibSearch = %#v/%v", search, err) + t.Fatalf("fetchLyricsFromLRCLibSearch = %#v/%v", search, err) } if best := client.findBestLRCLibSearchMatch( []LRCLibResponse{ diff --git a/go_backend/metadata.go b/go_backend/metadata.go index 95859cee..bf832061 100644 --- a/go_backend/metadata.go +++ b/go_backend/metadata.go @@ -1661,30 +1661,27 @@ type AudioQuality struct { Codec string `json:"codec,omitempty"` } +func flacAudioQualityFromStreamInfo(streamInfo []byte) AudioQuality { + bitDepth, sampleRate, totalSamples := parseFLACStreamInfoQuality(streamInfo) + duration := 0 + if sampleRate > 0 && totalSamples > 0 { + duration = int(totalSamples / int64(sampleRate)) + } + return AudioQuality{ + BitDepth: bitDepth, + SampleRate: sampleRate, + TotalSamples: totalSamples, + Duration: duration, + Codec: "flac", + } +} + func audioQualityFromParsedFlac(f *flac.File) (AudioQuality, error) { for _, meta := range f.Meta { if meta.Type != flac.StreamInfo || len(meta.Data) < 18 { continue } - streamInfo := meta.Data - sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4) - bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1 - totalSamples := int64(streamInfo[13]&0x0F)<<32 | - int64(streamInfo[14])<<24 | - int64(streamInfo[15])<<16 | - int64(streamInfo[16])<<8 | - int64(streamInfo[17]) - duration := 0 - if sampleRate > 0 && totalSamples > 0 { - duration = int(totalSamples / int64(sampleRate)) - } - return AudioQuality{ - BitDepth: bitsPerSample, - SampleRate: sampleRate, - TotalSamples: totalSamples, - Duration: duration, - Codec: "flac", - }, nil + return flacAudioQualityFromStreamInfo(meta.Data), nil } return AudioQuality{}, fmt.Errorf("FLAC STREAMINFO block not found") } @@ -1717,28 +1714,7 @@ func GetAudioQuality(filePath string) (AudioQuality, error) { return AudioQuality{}, fmt.Errorf("failed to read STREAMINFO: %w", err) } - sampleRate := (int(streamInfo[10]) << 12) | (int(streamInfo[11]) << 4) | (int(streamInfo[12]) >> 4) - - bitsPerSample := ((int(streamInfo[12]) & 0x01) << 4) | (int(streamInfo[13]) >> 4) + 1 - - totalSamples := int64(streamInfo[13]&0x0F)<<32 | - int64(streamInfo[14])<<24 | - int64(streamInfo[15])<<16 | - int64(streamInfo[16])<<8 | - int64(streamInfo[17]) - - duration := 0 - if sampleRate > 0 && totalSamples > 0 { - duration = int(totalSamples / int64(sampleRate)) - } - - return AudioQuality{ - BitDepth: bitsPerSample, - SampleRate: sampleRate, - TotalSamples: totalSamples, - Duration: duration, - Codec: "flac", - }, nil + return flacAudioQualityFromStreamInfo(streamInfo), nil } file.Seek(0, 0)