mirror of
https://github.com/spotiflacapp/SpotiFLAC-Mobile.git
synced 2026-06-01 03:15:17 +07:00
The HIGH-quality lossy format picker can now produce an AAC/M4A 320 kbps output alongside MP3 and Opus. FFmpegService.convertM4aToLossy/convertAudioFormat, the Dart queue pipeline, the Kotlin finalizer, and the library database format helper all route .m4a through a unified aac codec path and tag the resulting file with the M4A metadata writer. The Lossy Format setting gains a new option, and the track metadata convert dialog lists AAC next to the other targets. Apple Music lyrics gain a 'eLRC word sync' switch (default off). When disabled the pax-to-LRC formatter strips inline word timestamps, producing line-synced LRC that is safer for players that choke on eLRC; enabling it restores the previous word-by-word behaviour. The change propagates through SetLyricsFetchOptions and invalidates the global lyrics cache on toggle. Broad l10n migration: roughly 400 previously hardcoded English strings across queue, settings, track metadata, repo, audio analysis, setup and extension screens now live in the ARB catalog, with matching plural/placeholder forms. No behaviour change beyond localisation. Existing and new unit tests (lyrics eLRC toggle and Dart settings round-trip) pass.
138 lines
3.5 KiB
Go
138 lines
3.5 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type QQMusicClient struct {
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type qqLyricsMetadataRequest struct {
|
|
Artist []string `json:"artist"`
|
|
Album string `json:"album,omitempty"`
|
|
SongID int64 `json:"songid,omitempty"`
|
|
Title string `json:"title"`
|
|
Duration int64 `json:"duration,omitempty"`
|
|
}
|
|
|
|
type qqLyricsMetadataResponse struct {
|
|
Lyrics []paxLyrics `json:"lyrics"`
|
|
}
|
|
|
|
func NewQQMusicClient() *QQMusicClient {
|
|
return &QQMusicClient{
|
|
httpClient: NewMetadataHTTPClient(15 * time.Second),
|
|
}
|
|
}
|
|
|
|
func (c *QQMusicClient) fetchLyricsByMetadata(trackName, artistName string, durationSec float64) (string, error) {
|
|
payload := qqLyricsMetadataRequest{
|
|
Artist: []string{artistName},
|
|
Title: trackName,
|
|
}
|
|
if durationSec > 0 {
|
|
payload.Duration = int64(math.Round(durationSec))
|
|
}
|
|
|
|
lyricsURL := "https://lyrics.paxsenix.org/qq/lyrics-metadata"
|
|
|
|
payloadBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal payload: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", lyricsURL, strings.NewReader(string(payloadBytes)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("User-Agent", appUserAgent())
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("qqmusic lyrics fetch failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", fmt.Errorf("qqmusic lyrics proxy returned HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read lyrics response: %w", err)
|
|
}
|
|
|
|
bodyStr := strings.TrimSpace(string(bodyBytes))
|
|
if bodyStr == "" {
|
|
return "", fmt.Errorf("empty lyrics response from qqmusic")
|
|
}
|
|
|
|
return bodyStr, nil
|
|
}
|
|
|
|
func formatQQLyricsMetadataToLRC(rawJSON string, multiPersonWordByWord bool) (string, error) {
|
|
var response qqLyricsMetadataResponse
|
|
if err := json.Unmarshal([]byte(rawJSON), &response); err != nil {
|
|
return "", fmt.Errorf("failed to parse qq metadata lyrics response")
|
|
}
|
|
if len(response.Lyrics) == 0 {
|
|
return "", fmt.Errorf("qq metadata lyrics response was empty")
|
|
}
|
|
return formatPaxContent("Syllable", response.Lyrics, multiPersonWordByWord, true), nil
|
|
}
|
|
|
|
func (c *QQMusicClient) FetchLyrics(
|
|
trackName,
|
|
artistName string,
|
|
durationSec float64,
|
|
multiPersonWordByWord bool,
|
|
) (*LyricsResponse, error) {
|
|
rawLyrics, err := c.fetchLyricsByMetadata(trackName, artistName, durationSec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if errMsg, isErrorPayload := detectLyricsErrorPayload(rawLyrics); isErrorPayload {
|
|
return nil, fmt.Errorf("qqmusic proxy returned non-lyric payload: %s", errMsg)
|
|
}
|
|
|
|
lrcText, err := formatQQLyricsMetadataToLRC(rawLyrics, multiPersonWordByWord)
|
|
if err != nil {
|
|
if fallback, fallbackErr := formatPaxLyricsToLRC(rawLyrics, multiPersonWordByWord, true); fallbackErr == nil {
|
|
lrcText = fallback
|
|
} else {
|
|
lrcText = rawLyrics
|
|
}
|
|
}
|
|
|
|
lines := parseSyncedLyrics(lrcText)
|
|
if len(lines) > 0 {
|
|
return &LyricsResponse{
|
|
Lines: lines,
|
|
SyncType: "LINE_SYNCED",
|
|
Provider: "QQ Music",
|
|
Source: "QQ Music",
|
|
}, nil
|
|
}
|
|
|
|
resultLines := plainTextLyricsLines(lrcText)
|
|
|
|
if len(resultLines) > 0 {
|
|
return &LyricsResponse{
|
|
Lines: resultLines,
|
|
SyncType: "UNSYNCED",
|
|
Provider: "QQ Music",
|
|
Source: "QQ Music",
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("no lyrics found on qqmusic")
|
|
}
|