mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-15 05:10:28 +02:00
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
This commit is contained in:
+66
-4
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => 'アルバム';
|
||||
|
||||
@@ -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 => '앨범';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => 'Альбомы';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => 'Альбоми';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -21,6 +21,11 @@ class _LyricsProviderPriorityPageState
|
||||
'musixmatch',
|
||||
'apple_music',
|
||||
'qqmusic',
|
||||
'spotify',
|
||||
'deezer',
|
||||
'youtube',
|
||||
'kugou',
|
||||
'genius',
|
||||
];
|
||||
|
||||
late List<String> _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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user