From 4336e6dc7816ba4cd945af62d5e603678022943c Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 14 May 2026 18:43:26 +0700 Subject: [PATCH] feat: add 5 new lyrics providers New lyrics providers using Paxsenix API: - Spotify: Synced lyrics from Spotify - Deezer: Synced lyrics from Deezer - YouTube: Lyrics from YouTube - Kugou: Lyrics from Kugou (Chinese service) - Genius: Plain text lyrics from Genius Implementation: - Add lyrics client implementations for all providers - Smart search result scoring based on track name, artist, and duration - Support for both synced (LRC) and unsynced lyrics formats - Fallback search with simplified track names and primary artist UI updates: - Add provider entries to lyrics priority settings page - Add display names for new providers in settings --- go_backend/lyrics.go | 70 ++- go_backend/lyrics_paxsenix.go | 565 ++++++++++++++++++ go_backend/lyrics_supplement_test.go | 68 +++ lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/l10n/app_localizations_es.dart | 4 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_hi.dart | 2 +- lib/l10n/app_localizations_id.dart | 2 +- lib/l10n/app_localizations_ja.dart | 2 +- lib/l10n/app_localizations_ko.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 4 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_tr.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 6 +- lib/l10n/arb/app_de.arb | 2 +- lib/l10n/arb/app_en.arb | 2 +- lib/l10n/arb/app_es.arb | 2 +- lib/l10n/arb/app_es_ES.arb | 2 +- lib/l10n/arb/app_fr.arb | 2 +- lib/l10n/arb/app_hi.arb | 2 +- lib/l10n/arb/app_id.arb | 2 +- lib/l10n/arb/app_ja.arb | 2 +- lib/l10n/arb/app_ko.arb | 2 +- lib/l10n/arb/app_nl.arb | 2 +- lib/l10n/arb/app_pt.arb | 2 +- lib/l10n/arb/app_pt_PT.arb | 2 +- lib/l10n/arb/app_ru.arb | 2 +- lib/l10n/arb/app_tr.arb | 2 +- lib/l10n/arb/app_uk.arb | 2 +- lib/l10n/arb/app_zh.arb | 2 +- lib/l10n/arb/app_zh_CN.arb | 2 +- lib/l10n/arb/app_zh_TW.arb | 2 +- lib/screens/settings/about_page.dart | 3 +- .../lyrics_provider_priority_page.dart | 35 ++ .../settings/lyrics_settings_page.dart | 5 + 39 files changed, 778 insertions(+), 42 deletions(-) create mode 100644 go_backend/lyrics_paxsenix.go diff --git a/go_backend/lyrics.go b/go_backend/lyrics.go index d7ff4542..f53124d1 100644 --- a/go_backend/lyrics.go +++ b/go_backend/lyrics.go @@ -26,6 +26,11 @@ const ( LyricsProviderMusixmatch = "musixmatch" LyricsProviderAppleMusic = "apple_music" LyricsProviderQQMusic = "qqmusic" + LyricsProviderSpotify = "spotify" + LyricsProviderDeezer = "deezer" + LyricsProviderYouTube = "youtube" + LyricsProviderKugou = "kugou" + LyricsProviderGenius = "genius" ) var DefaultLyricsProviders = []string{ @@ -102,6 +107,11 @@ func SetLyricsProviderOrder(providers []string) { LyricsProviderMusixmatch: true, LyricsProviderAppleMusic: true, LyricsProviderQQMusic: true, + LyricsProviderSpotify: true, + LyricsProviderDeezer: true, + LyricsProviderYouTube: true, + LyricsProviderKugou: true, + LyricsProviderGenius: true, } var valid []string @@ -132,10 +142,15 @@ func GetLyricsProviderOrder() []string { func GetAvailableLyricsProviders() []map[string]interface{} { return []map[string]interface{}{ {"id": LyricsProviderLRCLIB, "name": "LRCLIB", "has_proxy_dependency": false, "description": "Open-source synced lyrics database"}, - {"id": LyricsProviderNetease, "name": "Netease", "has_proxy_dependency": true, "description": "NetEase Cloud Music lyrics via Paxsenix"}, - {"id": LyricsProviderMusixmatch, "name": "Musixmatch", "has_proxy_dependency": true, "description": "Musixmatch lyrics via Paxsenix"}, - {"id": LyricsProviderAppleMusic, "name": "Apple Music", "has_proxy_dependency": true, "description": "Apple Music synced lyrics via Paxsenix"}, - {"id": LyricsProviderQQMusic, "name": "QQ Music", "has_proxy_dependency": true, "description": "QQ Music lyrics via Paxsenix"}, + {"id": LyricsProviderNetease, "name": "Netease", "has_proxy_dependency": true, "description": "NetEase Cloud Music lyrics"}, + {"id": LyricsProviderMusixmatch, "name": "Musixmatch", "has_proxy_dependency": true, "description": "Musixmatch lyrics"}, + {"id": LyricsProviderAppleMusic, "name": "Apple Music", "has_proxy_dependency": true, "description": "Apple Music synced lyrics"}, + {"id": LyricsProviderQQMusic, "name": "QQ Music", "has_proxy_dependency": true, "description": "QQ Music lyrics"}, + {"id": LyricsProviderSpotify, "name": "Spotify", "has_proxy_dependency": true, "description": "Spotify synced lyrics"}, + {"id": LyricsProviderDeezer, "name": "Deezer", "has_proxy_dependency": true, "description": "Deezer lyrics"}, + {"id": LyricsProviderYouTube, "name": "YouTube", "has_proxy_dependency": true, "description": "YouTube lyrics"}, + {"id": LyricsProviderKugou, "name": "Kugou", "has_proxy_dependency": true, "description": "Kugou lyrics"}, + {"id": LyricsProviderGenius, "name": "Genius", "has_proxy_dependency": true, "description": "Genius lyrics"}, } } @@ -550,6 +565,53 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st lyrics, err = qqClient.FetchLyrics(trackName, artistName, durationSec, fetchOptions.MultiPersonWordByWord) } + case LyricsProviderSpotify: + spotifyClient := NewSpotifyLyricsClient() + lyrics, err = spotifyClient.FetchLyrics(spotifyID, trackName, primaryArtist, durationSec) + if err != nil && primaryArtist != artistName { + lyrics, err = spotifyClient.FetchLyrics(spotifyID, trackName, artistName, durationSec) + } + if err != nil && simplifiedTrack != trackName { + lyrics, err = spotifyClient.FetchLyrics("", simplifiedTrack, primaryArtist, durationSec) + } + + case LyricsProviderDeezer: + deezerClient := NewDeezerLyricsClient() + lyrics, err = deezerClient.FetchLyrics(spotifyID, trackName, primaryArtist, durationSec) + if err != nil && primaryArtist != artistName { + lyrics, err = deezerClient.FetchLyrics(spotifyID, trackName, artistName, durationSec) + } + + case LyricsProviderYouTube: + youtubeClient := NewYouTubeLyricsClient() + lyrics, err = youtubeClient.FetchLyrics(trackName, primaryArtist, durationSec) + if err != nil && primaryArtist != artistName { + lyrics, err = youtubeClient.FetchLyrics(trackName, artistName, durationSec) + } + if err != nil && simplifiedTrack != trackName { + lyrics, err = youtubeClient.FetchLyrics(simplifiedTrack, primaryArtist, durationSec) + } + + case LyricsProviderKugou: + kugouClient := NewKugouLyricsClient() + lyrics, err = kugouClient.FetchLyrics(trackName, primaryArtist, durationSec) + if err != nil && primaryArtist != artistName { + lyrics, err = kugouClient.FetchLyrics(trackName, artistName, durationSec) + } + if err != nil && simplifiedTrack != trackName { + lyrics, err = kugouClient.FetchLyrics(simplifiedTrack, primaryArtist, durationSec) + } + + case LyricsProviderGenius: + geniusClient := NewGeniusLyricsClient() + lyrics, err = geniusClient.FetchLyrics(trackName, primaryArtist, durationSec) + if err != nil && primaryArtist != artistName { + lyrics, err = geniusClient.FetchLyrics(trackName, artistName, durationSec) + } + if err != nil && simplifiedTrack != trackName { + lyrics, err = geniusClient.FetchLyrics(simplifiedTrack, primaryArtist, durationSec) + } + default: GoLog("[Lyrics] Unknown provider: %s, skipping\n", providerName) continue diff --git a/go_backend/lyrics_paxsenix.go b/go_backend/lyrics_paxsenix.go new file mode 100644 index 00000000..f9612386 --- /dev/null +++ b/go_backend/lyrics_paxsenix.go @@ -0,0 +1,565 @@ +package gobackend + +import ( + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "time" +) + +type SpotifyLyricsClient struct { + httpClient *http.Client +} + +type DeezerLyricsClient struct { + httpClient *http.Client +} + +type YouTubeLyricsClient struct { + httpClient *http.Client +} + +type KugouLyricsClient struct { + httpClient *http.Client +} + +type GeniusLyricsClient struct { + httpClient *http.Client +} + +type spotifyLyricsSearchResult struct { + TrackID string `json:"trackId"` + Name string `json:"name"` + ArtistName string `json:"artistName"` + Duration string `json:"duration"` +} + +type youtubeLyricsSearchResult struct { + VideoID string `json:"videoId"` + Title string `json:"title"` + Author string `json:"author"` + Duration string `json:"duration"` +} + +type kugouLyricsSearchResult struct { + Hash string `json:"hash"` + Title string `json:"title"` + Artist string `json:"artist"` + Duration float64 `json:"duration"` +} + +type geniusSearchResponse struct { + Response struct { + Sections []struct { + Hits []struct { + Type string `json:"type"` + Result struct { + Title string `json:"title"` + ArtistNames string `json:"artist_names"` + PrimaryArtistNames string `json:"primary_artist_names"` + URL string `json:"url"` + } `json:"result"` + } `json:"hits"` + } `json:"sections"` + } `json:"response"` +} + +type paxsenixLyricsObject struct { + Type string `json:"type"` + Content []paxLyrics `json:"content"` + Lyrics []paxLyrics `json:"lyrics"` + LyricsText string `json:"lyrics_text"` + PlainLyrics string `json:"plain_lyrics"` +} + +func NewSpotifyLyricsClient() *SpotifyLyricsClient { + return &SpotifyLyricsClient{httpClient: NewMetadataHTTPClient(15 * time.Second)} +} + +func NewDeezerLyricsClient() *DeezerLyricsClient { + return &DeezerLyricsClient{httpClient: NewMetadataHTTPClient(15 * time.Second)} +} + +func NewYouTubeLyricsClient() *YouTubeLyricsClient { + return &YouTubeLyricsClient{httpClient: NewMetadataHTTPClient(15 * time.Second)} +} + +func NewKugouLyricsClient() *KugouLyricsClient { + return &KugouLyricsClient{httpClient: NewMetadataHTTPClient(15 * time.Second)} +} + +func NewGeniusLyricsClient() *GeniusLyricsClient { + return &GeniusLyricsClient{httpClient: NewMetadataHTTPClient(15 * time.Second)} +} + +func fetchPaxsenixBody(httpClient *http.Client, endpoint string, params url.Values) (string, error) { + fullURL := endpoint + if len(params) > 0 { + fullURL += "?" + params.Encode() + } + + req, err := http.NewRequest("GET", fullURL, nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", appUserAgent()) + + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read response: %w", err) + } + + trimmed := strings.TrimSpace(string(body)) + if resp.StatusCode != http.StatusOK { + if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload { + return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, errMsg) + } + return "", fmt.Errorf("HTTP %d", resp.StatusCode) + } + if errMsg, isErrorPayload := detectLyricsErrorPayload(trimmed); isErrorPayload { + return "", fmt.Errorf("%s", errMsg) + } + if trimmed == "" { + return "", fmt.Errorf("empty response") + } + return trimmed, nil +} + +func parsePaxsenixLyricsPayload(raw, provider string, multiPersonWordByWord bool) (*LyricsResponse, error) { + var lrcPayload string + if err := json.Unmarshal([]byte(raw), &lrcPayload); err == nil { + lrcPayload = strings.TrimSpace(lrcPayload) + if lrcPayload == "" { + return nil, fmt.Errorf("%s returned empty lyrics", provider) + } + return lyricsResponseFromText(lrcPayload, provider), nil + } + + var rawObject map[string]json.RawMessage + if err := json.Unmarshal([]byte(raw), &rawObject); err == nil { + for _, key := range []string{"lyrics", "lyric", "lyrics_text", "plain_lyrics"} { + var value string + if rawValue, ok := rawObject[key]; ok && json.Unmarshal(rawValue, &value) == nil { + value = strings.TrimSpace(value) + if value != "" { + return lyricsResponseFromText(value, provider), nil + } + } + } + } + + var payload paxsenixLyricsObject + if err := json.Unmarshal([]byte(raw), &payload); err == nil { + switch { + case strings.TrimSpace(payload.LyricsText) != "": + return lyricsResponseFromText(payload.LyricsText, provider), nil + case len(payload.Lyrics) > 0: + return lyricsResponseFromText(formatPaxContent("Syllable", payload.Lyrics, multiPersonWordByWord, true), provider), nil + case len(payload.Content) > 0: + lyricsType := payload.Type + if lyricsType == "" { + lyricsType = "Syllable" + } + return lyricsResponseFromText(formatPaxContent(lyricsType, payload.Content, multiPersonWordByWord, true), provider), nil + case strings.TrimSpace(payload.PlainLyrics) != "": + return lyricsResponseFromText(payload.PlainLyrics, provider), nil + } + } + + trimmed := strings.TrimSpace(raw) + if trimmed != "" && !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") { + return lyricsResponseFromText(trimmed, provider), nil + } + return nil, fmt.Errorf("failed to decode %s lyrics response", provider) +} + +func lyricsResponseFromText(text, provider string) *LyricsResponse { + lines := parseSyncedLyrics(text) + if len(lines) > 0 { + return &LyricsResponse{ + Lines: lines, + SyncType: "LINE_SYNCED", + PlainLyrics: plainLyricsFromTimedLines(lines), + Provider: provider, + Source: provider, + } + } + + plainLines := plainTextLyricsLines(text) + if len(plainLines) > 0 { + return &LyricsResponse{ + Lines: plainLines, + SyncType: "UNSYNCED", + PlainLyrics: text, + Provider: provider, + Source: provider, + } + } + + return &LyricsResponse{Provider: provider, Source: provider} +} + +func normalizeSpotifyLyricsID(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" || strings.HasPrefix(strings.ToLower(raw), "deezer:") { + return "" + } + if strings.HasPrefix(strings.ToLower(raw), "spotify:") { + parts := strings.Split(raw, ":") + raw = parts[len(parts)-1] + } + if strings.Contains(raw, "spotify.com/track/") { + raw = extractSpotifyIDFromURL(raw) + } + raw = strings.TrimSpace(strings.Split(raw, "?")[0]) + if regexpSpotifyTrackID.MatchString(raw) { + return raw + } + return "" +} + +var regexpSpotifyTrackID = regexp.MustCompile(`^[A-Za-z0-9]{22}$`) + +func (c *SpotifyLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { + query := strings.TrimSpace(trackName + " " + artistName) + if query == "" { + return "", fmt.Errorf("empty search query") + } + + params := url.Values{} + params.Set("q", query) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/spotify/search", params) + if err != nil { + return "", fmt.Errorf("spotify search failed: %w", err) + } + + var results []spotifyLyricsSearchResult + if err := json.Unmarshal([]byte(raw), &results); err != nil { + return "", fmt.Errorf("failed to decode spotify search: %w", err) + } + best := selectBestSpotifyLyricsSearchResult(results, trackName, artistName, durationSec) + if best == nil || strings.TrimSpace(best.TrackID) == "" { + return "", fmt.Errorf("no songs found on spotify") + } + return strings.TrimSpace(best.TrackID), nil +} + +func selectBestSpotifyLyricsSearchResult(results []spotifyLyricsSearchResult, trackName, artistName string, durationSec float64) *spotifyLyricsSearchResult { + if len(results) == 0 { + return nil + } + + bestIndex := 0 + bestScore := -1 + for i := range results { + result := &results[i] + score := scoreLyricsSearchCandidate(result.Name, result.ArtistName, parseClockDuration(result.Duration), trackName, artistName, durationSec) + if score > bestScore { + bestIndex = i + bestScore = score + } + } + return &results[bestIndex] +} + +func (c *SpotifyLyricsClient) FetchLyricsByID(trackID string) (*LyricsResponse, error) { + params := url.Values{} + params.Set("id", trackID) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/spotify/lyrics", params) + if err != nil { + return nil, fmt.Errorf("spotify lyrics fetch failed: %w", err) + } + return parsePaxsenixLyricsPayload(raw, "Spotify", false) +} + +func (c *SpotifyLyricsClient) FetchLyrics(spotifyID, trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + trackID := normalizeSpotifyLyricsID(spotifyID) + if trackID == "" { + var err error + trackID, err = c.SearchSong(trackName, artistName, durationSec) + if err != nil { + return nil, err + } + } + return c.FetchLyricsByID(trackID) +} + +func normalizeDeezerLyricsID(raw string) string { + raw = strings.TrimSpace(raw) + if strings.HasPrefix(strings.ToLower(raw), "deezer:") { + raw = strings.TrimSpace(raw[len("deezer:"):]) + } + if strings.Contains(raw, "deezer.com/") { + raw = extractDeezerIDFromURL(raw) + } + raw = strings.TrimSpace(strings.Split(raw, "?")[0]) + if _, err := strconv.ParseInt(raw, 10, 64); err == nil { + return raw + } + return "" +} + +func (c *DeezerLyricsClient) FetchLyricsByID(trackID string, multiPersonWordByWord bool) (*LyricsResponse, error) { + params := url.Values{} + params.Set("id", trackID) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/deezer/lyrics", params) + if err != nil { + return nil, fmt.Errorf("deezer lyrics fetch failed: %w", err) + } + return parsePaxsenixLyricsPayload(raw, "Deezer", multiPersonWordByWord) +} + +func (c *DeezerLyricsClient) FetchLyrics(spotifyID, trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + deezerID := normalizeDeezerLyricsID(spotifyID) + if deezerID == "" { + spotifyTrackID := normalizeSpotifyLyricsID(spotifyID) + if spotifyTrackID == "" { + return nil, fmt.Errorf("deezer provider needs a deezer id or spotify id") + } + resolvedID, err := NewSongLinkClient().GetDeezerIDFromSpotify(spotifyTrackID) + if err != nil { + return nil, fmt.Errorf("failed to resolve deezer id: %w", err) + } + deezerID = normalizeDeezerLyricsID(resolvedID) + } + if deezerID == "" { + return nil, fmt.Errorf("deezer id unavailable") + } + return c.FetchLyricsByID(deezerID, true) +} + +func (c *YouTubeLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { + query := strings.TrimSpace(trackName + " " + artistName) + if query == "" { + return "", fmt.Errorf("empty search query") + } + + params := url.Values{} + params.Set("q", query) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/youtube/search", params) + if err != nil { + return "", fmt.Errorf("youtube search failed: %w", err) + } + + var results []youtubeLyricsSearchResult + if err := json.Unmarshal([]byte(raw), &results); err != nil { + return "", fmt.Errorf("failed to decode youtube search: %w", err) + } + best := selectBestYouTubeLyricsSearchResult(results, trackName, artistName, durationSec) + if best == nil || strings.TrimSpace(best.VideoID) == "" { + return "", fmt.Errorf("no songs found on youtube") + } + return strings.TrimSpace(best.VideoID), nil +} + +func selectBestYouTubeLyricsSearchResult(results []youtubeLyricsSearchResult, trackName, artistName string, durationSec float64) *youtubeLyricsSearchResult { + if len(results) == 0 { + return nil + } + + bestIndex := 0 + bestScore := -1 + for i := range results { + result := &results[i] + score := scoreLyricsSearchCandidate(result.Title, result.Author, parseClockDuration(result.Duration), trackName, artistName, durationSec) + if score > bestScore { + bestIndex = i + bestScore = score + } + } + return &results[bestIndex] +} + +func (c *YouTubeLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + videoID, err := c.SearchSong(trackName, artistName, durationSec) + if err != nil { + return nil, err + } + + params := url.Values{} + params.Set("id", videoID) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/youtube/lyrics", params) + if err != nil { + return nil, fmt.Errorf("youtube lyrics fetch failed: %w", err) + } + return parsePaxsenixLyricsPayload(raw, "YouTube", false) +} + +func (c *KugouLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { + query := strings.TrimSpace(trackName + " " + artistName) + if query == "" { + return "", fmt.Errorf("empty search query") + } + + params := url.Values{} + params.Set("q", query) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/kugou/search", params) + if err != nil { + return "", fmt.Errorf("kugou search failed: %w", err) + } + + var results []kugouLyricsSearchResult + if err := json.Unmarshal([]byte(raw), &results); err != nil { + return "", fmt.Errorf("failed to decode kugou search: %w", err) + } + best := selectBestKugouLyricsSearchResult(results, trackName, artistName, durationSec) + if best == nil || strings.TrimSpace(best.Hash) == "" { + return "", fmt.Errorf("no songs found on kugou") + } + return strings.TrimSpace(best.Hash), nil +} + +func selectBestKugouLyricsSearchResult(results []kugouLyricsSearchResult, trackName, artistName string, durationSec float64) *kugouLyricsSearchResult { + if len(results) == 0 { + return nil + } + + bestIndex := 0 + bestScore := -1 + for i := range results { + result := &results[i] + score := scoreLyricsSearchCandidate(result.Title, result.Artist, result.Duration, trackName, artistName, durationSec) + if score > bestScore { + bestIndex = i + bestScore = score + } + } + return &results[bestIndex] +} + +func (c *KugouLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + hash, err := c.SearchSong(trackName, artistName, durationSec) + if err != nil { + return nil, err + } + + params := url.Values{} + params.Set("id", hash) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/kugou/lyrics", params) + if err != nil { + return nil, fmt.Errorf("kugou lyrics fetch failed: %w", err) + } + return parsePaxsenixLyricsPayload(raw, "Kugou", false) +} + +func (c *GeniusLyricsClient) SearchSong(trackName, artistName string, durationSec float64) (string, error) { + query := strings.TrimSpace(trackName + " " + artistName) + if query == "" { + return "", fmt.Errorf("empty search query") + } + + params := url.Values{} + params.Set("q", query) + params.Set("per_page", "10") + raw, err := fetchPaxsenixBody(c.httpClient, "https://genius.com/api/search/multi", params) + if err != nil { + return "", fmt.Errorf("genius search failed: %w", err) + } + + var results geniusSearchResponse + if err := json.Unmarshal([]byte(raw), &results); err != nil { + return "", fmt.Errorf("failed to decode genius search: %w", err) + } + + bestURL := "" + bestScore := -1 + for _, section := range results.Response.Sections { + for _, hit := range section.Hits { + if hit.Type != "song" || strings.TrimSpace(hit.Result.URL) == "" { + continue + } + + artist := hit.Result.PrimaryArtistNames + if strings.TrimSpace(artist) == "" { + artist = hit.Result.ArtistNames + } + score := scoreLyricsSearchCandidate(hit.Result.Title, artist, 0, trackName, artistName, durationSec) + if score > bestScore { + bestScore = score + bestURL = strings.TrimSpace(hit.Result.URL) + } + } + } + + if bestURL == "" { + return "", fmt.Errorf("no songs found on genius") + } + return bestURL, nil +} + +func (c *GeniusLyricsClient) FetchLyrics(trackName, artistName string, durationSec float64) (*LyricsResponse, error) { + geniusURL, err := c.SearchSong(trackName, artistName, durationSec) + if err != nil { + return nil, err + } + + params := url.Values{} + params.Set("url", geniusURL) + raw, err := fetchPaxsenixBody(c.httpClient, "https://lyrics.paxsenix.org/genius/lyrics", params) + if err != nil { + return nil, fmt.Errorf("genius lyrics fetch failed: %w", err) + } + return parsePaxsenixLyricsPayload(raw, "Genius", false) +} + +func scoreLyricsSearchCandidate(candidateTrack, candidateArtist string, candidateDuration float64, trackName, artistName string, durationSec float64) int { + normalizedTrack := strings.ToLower(strings.TrimSpace(simplifyTrackName(trackName))) + normalizedArtist := strings.ToLower(strings.TrimSpace(normalizeArtistName(artistName))) + candidateTrack = strings.ToLower(strings.TrimSpace(simplifyTrackName(candidateTrack))) + candidateArtist = strings.ToLower(strings.TrimSpace(normalizeArtistName(candidateArtist))) + + score := 0 + switch { + case candidateTrack == normalizedTrack: + score += 50 + case strings.Contains(candidateTrack, normalizedTrack) || strings.Contains(normalizedTrack, candidateTrack): + score += 25 + } + + switch { + case candidateArtist == normalizedArtist: + score += 60 + case strings.Contains(candidateArtist, normalizedArtist) || strings.Contains(normalizedArtist, candidateArtist): + score += 30 + } + + if durationSec > 0 && candidateDuration > 0 { + diff := math.Abs(candidateDuration - durationSec) + if diff <= durationToleranceSec { + score += 20 + } + } + + return score +} + +func parseClockDuration(value string) float64 { + value = strings.TrimSpace(value) + if value == "" { + return 0 + } + + parts := strings.Split(value, ":") + total := 0 + for _, part := range parts { + n, err := strconv.Atoi(strings.TrimSpace(part)) + if err != nil { + return 0 + } + total = total*60 + n + } + return float64(total) +} diff --git a/go_backend/lyrics_supplement_test.go b/go_backend/lyrics_supplement_test.go index 5beb1dfd..9199d111 100644 --- a/go_backend/lyrics_supplement_test.go +++ b/go_backend/lyrics_supplement_test.go @@ -247,4 +247,72 @@ func TestExternalLyricsProvidersWithFakeHTTP(t *testing.T) { if _, err := formatQQLyricsMetadataToLRC(`{"lyrics":[]}`, false); err == nil { t.Fatal("expected empty QQ metadata error") } + + spotify := &SpotifyLyricsClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/spotify/search"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"trackId":"spotify-1","name":"Song","artistName":"Artist","duration":"03:00"}]`)), Request: req}, nil + case strings.Contains(req.URL.Path, "/spotify/lyrics"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`"[00:01.00]Spotify"`)), Request: req}, nil + default: + return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil + } + })}} + spotifyLyrics, err := spotify.FetchLyrics("", "Song", "Artist", 180) + if err != nil || spotifyLyrics.Provider != "Spotify" || spotifyLyrics.SyncType != "LINE_SYNCED" { + t.Fatalf("spotify lyrics = %#v/%v", spotifyLyrics, err) + } + + deezer := &DeezerLyricsClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"lyrics":[{"timestamp":1000,"text":[{"text":"Deezer","part":false}]}]}`)), Request: req}, nil + })}} + deezerLyrics, err := deezer.FetchLyricsByID("123", false) + if err != nil || deezerLyrics.Provider != "Deezer" || deezerLyrics.SyncType != "LINE_SYNCED" { + t.Fatalf("deezer lyrics = %#v/%v", deezerLyrics, err) + } + + youtube := &YouTubeLyricsClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/youtube/search"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"videoId":"yt-1","title":"Song","author":"Artist","duration":"3:00"}]`)), Request: req}, nil + case strings.Contains(req.URL.Path, "/youtube/lyrics"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`"[00:01.00]YouTube"`)), Request: req}, nil + default: + return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil + } + })}} + youtubeLyrics, err := youtube.FetchLyrics("Song", "Artist", 180) + if err != nil || youtubeLyrics.Provider != "YouTube" || youtubeLyrics.SyncType != "LINE_SYNCED" { + t.Fatalf("youtube lyrics = %#v/%v", youtubeLyrics, err) + } + + kugou := &KugouLyricsClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/kugou/search"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`[{"hash":"kg-1","title":"Song","artist":"Artist","duration":180}]`)), Request: req}, nil + case strings.Contains(req.URL.Path, "/kugou/lyrics"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"lyrics_text":"[00:01.00]Kugou"}`)), Request: req}, nil + default: + return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil + } + })}} + kugouLyrics, err := kugou.FetchLyrics("Song", "Artist", 180) + if err != nil || kugouLyrics.Provider != "Kugou" || kugouLyrics.SyncType != "LINE_SYNCED" { + t.Fatalf("kugou lyrics = %#v/%v", kugouLyrics, err) + } + + genius := &GeniusLyricsClient{httpClient: &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + switch { + case strings.Contains(req.URL.Path, "/api/search/multi"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"response":{"sections":[{"hits":[{"type":"song","result":{"title":"Song","primary_artist_names":"Artist","url":"https://genius.com/artist-song-lyrics"}}]}]}}`)), Request: req}, nil + case strings.Contains(req.URL.Path, "/genius/lyrics"): + return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"error":false,"lyrics":"Genius line"}`)), Request: req}, nil + default: + return &http.Response{StatusCode: 404, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{}`)), Request: req}, nil + } + })}} + geniusLyrics, err := genius.FetchLyrics("Song", "Artist", 180) + if err != nil || geniusLyrics.Provider != "Genius" || geniusLyrics.SyncType != "UNSYNCED" { + t.Fatalf("genius lyrics = %#v/%v", geniusLyrics, err) + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 73567689..416ace16 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -837,7 +837,7 @@ abstract class AppLocalizations { /// App description in header card /// /// In en, this message translates to: - /// **'Download Spotify tracks in lossless quality from Tidal and Qobuz.'** + /// **'Search music metadata, manage extensions, and organize your library.'** String get aboutAppDescription; /// Section header for artist albums diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 3efc50b1..7f402932 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -405,7 +405,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get aboutAppDescription => - 'Lade Spotify-Titel in verlustfreier Qualität von Tidal und Qobuz herunter.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Alben'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e4a64198..a71b6136 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -397,7 +397,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 0058f0a2..151cf4f7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -397,7 +397,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; @@ -4585,7 +4585,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs { @override String get aboutAppDescription => - 'Descargar pistas de Spotify en alta calidad (sin pérdida) de Tidal y Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Álbumes'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 395133ce..20b3a552 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -399,7 +399,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index aa97580d..7cac098d 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -397,7 +397,7 @@ class AppLocalizationsHi extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index e1fffedc..b0309211 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -400,7 +400,7 @@ class AppLocalizationsId extends AppLocalizations { @override String get aboutAppDescription => - 'Unduh lagu-lagu Spotify dalam kualitas lossless dari Tidal dan Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Album'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index db483e80..06b117a2 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -393,7 +393,7 @@ class AppLocalizationsJa extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'アルバム'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 4e545514..6778332c 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -385,7 +385,7 @@ class AppLocalizationsKo extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => '앨범'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index fa1a4f56..0291893a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -397,7 +397,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index d72b1e65..abdf0045 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -397,7 +397,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; @@ -4584,7 +4584,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Álbuns'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index de3162cb..4f707f8e 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -403,7 +403,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String get aboutAppDescription => - 'Скачивайте треки Spotify в lossless качестве с Tidal и Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Альбомы'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index d13dec01..bc7127b6 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -405,7 +405,7 @@ class AppLocalizationsTr extends AppLocalizations { @override String get aboutAppDescription => - 'Spotify parçalarını Tidal ve Qobuz aracılığıyla kayıpsız kalitede indirin.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albümler'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index dab8c235..73ee89e3 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -407,7 +407,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String get aboutAppDescription => - 'Кінцеві точки потокового передавання Tidal Hi-Res FLAC. Ключовий елемент пазлу музики без втрат.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Альбоми'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 3e126804..c5f84070 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -397,7 +397,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; @@ -4565,7 +4565,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; @@ -8046,7 +8046,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh { @override String get aboutAppDescription => - 'Download Spotify tracks in lossless quality from Tidal and Qobuz.'; + 'Search music metadata, manage extensions, and organize your library.'; @override String get artistAlbums => 'Albums'; diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 530a1ee4..e5484160 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Lade Spotify-Titel in verlustfreier Qualität von Tidal und Qobuz herunter.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b7d52bd8..45aea19c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 91fe89a7..896e4da4 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -398,7 +398,7 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_es_ES.arb b/lib/l10n/arb/app_es_ES.arb index 5733e0c5..31d5f3da 100644 --- a/lib/l10n/arb/app_es_ES.arb +++ b/lib/l10n/arb/app_es_ES.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Descargar pistas de Spotify en alta calidad (sin pérdida) de Tidal y Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb index 5568f8b0..2293f8ee 100644 --- a/lib/l10n/arb/app_fr.arb +++ b/lib/l10n/arb/app_fr.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_hi.arb b/lib/l10n/arb/app_hi.arb index dd2bf633..de8cab74 100644 --- a/lib/l10n/arb/app_hi.arb +++ b/lib/l10n/arb/app_hi.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_id.arb b/lib/l10n/arb/app_id.arb index ee3c6324..f580b8e1 100644 --- a/lib/l10n/arb/app_id.arb +++ b/lib/l10n/arb/app_id.arb @@ -458,7 +458,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Unduh lagu-lagu Spotify dalam kualitas lossless dari Tidal dan Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_ja.arb b/lib/l10n/arb/app_ja.arb index 38c65733..6fac9706 100644 --- a/lib/l10n/arb/app_ja.arb +++ b/lib/l10n/arb/app_ja.arb @@ -438,7 +438,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_ko.arb b/lib/l10n/arb/app_ko.arb index 59c3760b..a34f22f7 100644 --- a/lib/l10n/arb/app_ko.arb +++ b/lib/l10n/arb/app_ko.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb index 87cc7c7d..9d6bbd6f 100644 --- a/lib/l10n/arb/app_nl.arb +++ b/lib/l10n/arb/app_nl.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb index fb6038e8..aa9a72e0 100644 --- a/lib/l10n/arb/app_pt.arb +++ b/lib/l10n/arb/app_pt.arb @@ -398,7 +398,7 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_pt_PT.arb b/lib/l10n/arb/app_pt_PT.arb index 267eaede..66389668 100644 --- a/lib/l10n/arb/app_pt_PT.arb +++ b/lib/l10n/arb/app_pt_PT.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_ru.arb b/lib/l10n/arb/app_ru.arb index 2e114e03..4ee018bc 100644 --- a/lib/l10n/arb/app_ru.arb +++ b/lib/l10n/arb/app_ru.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Скачивайте треки Spotify в lossless качестве с Tidal и Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_tr.arb b/lib/l10n/arb/app_tr.arb index c4cbe8cb..aa2abd7c 100644 --- a/lib/l10n/arb/app_tr.arb +++ b/lib/l10n/arb/app_tr.arb @@ -438,7 +438,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Spotify parçalarını Tidal ve Qobuz aracılığıyla kayıpsız kalitede indirin.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_uk.arb b/lib/l10n/arb/app_uk.arb index 39bed2c3..8e3266be 100644 --- a/lib/l10n/arb/app_uk.arb +++ b/lib/l10n/arb/app_uk.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Кінцеві точки потокового передавання Tidal Hi-Res FLAC. Ключовий елемент пазлу музики без втрат.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb index f8ab208d..10500b57 100644 --- a/lib/l10n/arb/app_zh.arb +++ b/lib/l10n/arb/app_zh.arb @@ -398,7 +398,7 @@ "@aboutSachinsenalDesc": { "description": "Credit description for sachinsenal0x64" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_zh_CN.arb b/lib/l10n/arb/app_zh_CN.arb index 214e4d62..6c7d15b8 100644 --- a/lib/l10n/arb/app_zh_CN.arb +++ b/lib/l10n/arb/app_zh_CN.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/l10n/arb/app_zh_TW.arb b/lib/l10n/arb/app_zh_TW.arb index e88dccdd..b2fc74b9 100644 --- a/lib/l10n/arb/app_zh_TW.arb +++ b/lib/l10n/arb/app_zh_TW.arb @@ -498,7 +498,7 @@ "@aboutSjdonadoDesc": { "description": "Credit description for sjdonado" }, - "aboutAppDescription": "Download Spotify tracks in lossless quality from Tidal and Qobuz.", + "aboutAppDescription": "Search music metadata, manage extensions, and organize your library.", "@aboutAppDescription": { "description": "App description in header card" }, diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index 74e6244e..88c04bfb 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -141,7 +141,8 @@ class AboutPage extends StatelessWidget { icon: Icons.lyrics_outlined, title: 'Paxsenix', subtitle: - 'Partner lyrics proxy for Apple Music and QQ Music sources', + 'Lyrics proxy for Musixmatch, Netease, Apple Music, ' + 'QQ Music, Spotify, Deezer, YouTube, Kugou, and Genius', onTap: () => _launchUrl('https://lyrics.paxsenix.org'), showDivider: false, ), diff --git a/lib/screens/settings/lyrics_provider_priority_page.dart b/lib/screens/settings/lyrics_provider_priority_page.dart index 37bcc34a..77b1eb24 100644 --- a/lib/screens/settings/lyrics_provider_priority_page.dart +++ b/lib/screens/settings/lyrics_provider_priority_page.dart @@ -21,6 +21,11 @@ class _LyricsProviderPriorityPageState 'musixmatch', 'apple_music', 'qqmusic', + 'spotify', + 'deezer', + 'youtube', + 'kugou', + 'genius', ]; late List _enabledProviders; @@ -211,6 +216,36 @@ class _LyricsProviderPriorityPageState description: context.l10n.lyricsProviderQqMusicDesc, icon: Icons.queue_music, ); + case 'spotify': + return _LyricsProviderInfo( + name: 'Spotify', + description: context.l10n.lyricsProviderExtensionDesc, + icon: Icons.graphic_eq, + ); + case 'deezer': + return _LyricsProviderInfo( + name: 'Deezer', + description: context.l10n.lyricsProviderExtensionDesc, + icon: Icons.album_outlined, + ); + case 'youtube': + return _LyricsProviderInfo( + name: 'YouTube', + description: context.l10n.lyricsProviderExtensionDesc, + icon: Icons.smart_display_outlined, + ); + case 'kugou': + return _LyricsProviderInfo( + name: 'Kugou', + description: context.l10n.lyricsProviderExtensionDesc, + icon: Icons.library_music_outlined, + ); + case 'genius': + return _LyricsProviderInfo( + name: 'Genius', + description: context.l10n.lyricsProviderExtensionDesc, + icon: Icons.auto_awesome_outlined, + ); default: return _LyricsProviderInfo( name: id, diff --git a/lib/screens/settings/lyrics_settings_page.dart b/lib/screens/settings/lyrics_settings_page.dart index d5de3285..8766230d 100644 --- a/lib/screens/settings/lyrics_settings_page.dart +++ b/lib/screens/settings/lyrics_settings_page.dart @@ -216,6 +216,11 @@ class LyricsSettingsPage extends ConsumerWidget { 'musixmatch': 'Musixmatch', 'apple_music': 'Apple Music', 'qqmusic': 'QQ Music', + 'spotify': 'Spotify', + 'deezer': 'Deezer', + 'youtube': 'YouTube', + 'kugou': 'Kugou', + 'genius': 'Genius', }; String _getLyricsProvidersSubtitle(