feat: add translated and phonetic lyric previews
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -13,9 +14,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
searchEndpoint = "https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp"
|
||||
lyricsEndpoint = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg"
|
||||
maxUpstreamBytes = 4 << 20
|
||||
searchEndpoint = "https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp"
|
||||
musicuEndpoint = "https://u.y.qq.com/cgi-bin/musicu.fcg"
|
||||
legacyLyricsEndpoint = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg"
|
||||
maxUpstreamBytes = 4 << 20
|
||||
)
|
||||
|
||||
type song struct {
|
||||
@@ -29,6 +31,7 @@ type song struct {
|
||||
type lyricResult struct {
|
||||
Lyric string `json:"lyric"`
|
||||
Trans string `json:"trans"`
|
||||
Roma string `json:"roma"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
@@ -40,6 +43,7 @@ type qqAPI interface {
|
||||
type qqClient struct {
|
||||
httpClient *http.Client
|
||||
searchEndpoint string
|
||||
musicuEndpoint string
|
||||
lyricsEndpoint string
|
||||
}
|
||||
|
||||
@@ -57,7 +61,8 @@ func newQQClient() *qqClient {
|
||||
},
|
||||
},
|
||||
searchEndpoint: searchEndpoint,
|
||||
lyricsEndpoint: lyricsEndpoint,
|
||||
musicuEndpoint: musicuEndpoint,
|
||||
lyricsEndpoint: legacyLyricsEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +117,54 @@ func (c *qqClient) Search(keyword string, limit int) ([]song, error) {
|
||||
}
|
||||
|
||||
func (c *qqClient) Lyrics(mid string) (lyricResult, error) {
|
||||
result, err := c.musicuLyrics(mid)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
return c.legacyLyrics(mid)
|
||||
}
|
||||
|
||||
func (c *qqClient) musicuLyrics(mid string) (lyricResult, error) {
|
||||
payload := map[string]any{
|
||||
"comm": map[string]any{"ct": 24, "cv": 0, "format": "json", "platform": "yqq.json"},
|
||||
"req_1": map[string]any{
|
||||
"module": "music.musichallSong.PlayLyricInfo",
|
||||
"method": "GetPlayLyricInfo",
|
||||
"param": map[string]any{
|
||||
"songMID": mid, "songID": 0, "crypt": 0, "qrc": 0, "trans": 1, "roma": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
var response struct {
|
||||
Code int `json:"code"`
|
||||
Request struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Lyric string `json:"lyric"`
|
||||
Trans string `json:"trans"`
|
||||
Roma string `json:"roma"`
|
||||
} `json:"data"`
|
||||
} `json:"req_1"`
|
||||
}
|
||||
if err := c.postJSON(c.musicuEndpoint, payload, &response); err != nil {
|
||||
return lyricResult{}, err
|
||||
}
|
||||
if response.Code != 0 || response.Request.Code != 0 {
|
||||
return lyricResult{}, errors.New("QQ Music lyrics request failed")
|
||||
}
|
||||
lyric, err := decodeLyricBase64(response.Request.Data.Lyric)
|
||||
if err != nil || strings.TrimSpace(lyric) == "" {
|
||||
return lyricResult{}, errors.New("lyrics are not available for this track")
|
||||
}
|
||||
trans, _ := decodeLyricBase64(response.Request.Data.Trans)
|
||||
roma := ""
|
||||
if response.Request.Data.Roma != "" {
|
||||
roma, _ = decryptQRCLyric(response.Request.Data.Roma)
|
||||
}
|
||||
return lyricResult{Lyric: lyric, Trans: trans, Roma: roma, Code: 0}, nil
|
||||
}
|
||||
|
||||
func (c *qqClient) legacyLyrics(mid string) (lyricResult, error) {
|
||||
query := url.Values{
|
||||
"format": {"json"}, "songmid": {mid}, "outCharset": {"utf-8"}, "nobase64": {"1"},
|
||||
}
|
||||
@@ -125,11 +178,42 @@ func (c *qqClient) Lyrics(mid string) (lyricResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func decodeLyricBase64(value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
decoded, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return "", errors.New("QQ Music returned invalid lyric encoding")
|
||||
}
|
||||
if len(decoded) > maxUpstreamBytes {
|
||||
return "", errors.New("decoded lyrics exceeded the safety limit")
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
func (c *qqClient) postJSON(endpoint string, payload, destination any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
return c.doJSON(request, destination)
|
||||
}
|
||||
|
||||
func (c *qqClient) getJSON(endpoint string, query url.Values, destination any) error {
|
||||
request, err := http.NewRequest(http.MethodGet, endpoint+"?"+query.Encode(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.doJSON(request, destination)
|
||||
}
|
||||
|
||||
func (c *qqClient) doJSON(request *http.Request, destination any) error {
|
||||
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131 Safari/537.36")
|
||||
request.Header.Set("Referer", "https://y.qq.com/")
|
||||
request.Header.Set("Origin", "https://y.qq.com")
|
||||
|
||||
Reference in New Issue
Block a user