You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
4.0 KiB
Go
152 lines
4.0 KiB
Go
2 years ago
|
package client
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const endpointURL = "https://u.y.qq.com/cgi-bin/musicu.fcg"
|
||
|
|
||
|
type QQMusic struct {
|
||
|
http *http.Client
|
||
|
}
|
||
|
|
||
|
func (c *QQMusic) doRequest(ctx context.Context, reqBody any) ([]byte, error) {
|
||
|
reqBodyBuf, ok := reqBody.([]byte)
|
||
|
if !ok {
|
||
|
var err error
|
||
|
reqBodyBuf, err = json.Marshal(reqBody)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[doRequest] marshal request: %w", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||
|
endpointURL+fmt.Sprintf("?pcachetime=%d", time.Now().Unix()),
|
||
|
bytes.NewReader(reqBodyBuf),
|
||
|
)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[doRequest] create request: %w", err)
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Accept", "*/*")
|
||
|
req.Header.Set("Accept-Language", "zh-CN")
|
||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)")
|
||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
|
// req.Header.Set("Accept-Encoding", "gzip, deflate")
|
||
|
|
||
|
reqp, err := c.http.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[doRequest] send request: %w", err)
|
||
|
}
|
||
|
defer reqp.Body.Close()
|
||
|
|
||
|
respBodyBuf, err := io.ReadAll(reqp.Body)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[doRequest] read response: %w", err)
|
||
|
}
|
||
|
|
||
|
return respBodyBuf, nil
|
||
|
}
|
||
|
|
||
|
type rpcRequest struct {
|
||
|
Method string `json:"method"`
|
||
|
Module string `json:"module"`
|
||
|
Param any `json:"param"`
|
||
|
}
|
||
|
|
||
|
type rpcResponse struct {
|
||
|
Code int `json:"code"`
|
||
|
Ts int64 `json:"ts"`
|
||
|
StartTs int64 `json:"start_ts"`
|
||
|
TraceID string `json:"traceid"`
|
||
|
}
|
||
|
|
||
|
type rpcSubResponse struct {
|
||
|
Code int `json:"code"`
|
||
|
Data json.RawMessage `json:"data"`
|
||
|
}
|
||
|
|
||
|
func (c *QQMusic) rpcCall(ctx context.Context,
|
||
|
protocol string, method string, module string,
|
||
|
param any,
|
||
|
) (json.RawMessage, error) {
|
||
|
reqBody := map[string]any{protocol: rpcRequest{
|
||
|
Method: method,
|
||
|
Module: module,
|
||
|
Param: param,
|
||
|
}}
|
||
|
|
||
|
respBodyBuf, err := c.doRequest(ctx, reqBody)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] do request: %w", err)
|
||
|
}
|
||
|
|
||
|
// check rpc response status
|
||
|
respStatus := rpcResponse{}
|
||
|
if err := json.Unmarshal(respBodyBuf, &respStatus); err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal response: %w", err)
|
||
|
}
|
||
|
if respStatus.Code != 0 {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] rpc error: %d", respStatus.Code)
|
||
|
}
|
||
|
|
||
|
// parse response data
|
||
|
var respBody map[string]json.RawMessage
|
||
|
if err := json.Unmarshal(respBodyBuf, &respBody); err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal response: %w", err)
|
||
|
}
|
||
|
|
||
|
subRespBuf, ok := respBody[protocol]
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] sub-response not found")
|
||
|
}
|
||
|
|
||
|
subResp := rpcSubResponse{}
|
||
|
if err := json.Unmarshal(subRespBuf, &subResp); err != nil {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal sub-response: %w", err)
|
||
|
}
|
||
|
|
||
|
if subResp.Code != 0 {
|
||
|
return nil, fmt.Errorf("qqMusicClient[rpcCall] sub-response error: %d", subResp.Code)
|
||
|
}
|
||
|
|
||
|
return subResp.Data, nil
|
||
|
}
|
||
|
|
||
|
func (c *QQMusic) downloadFile(ctx context.Context, url string) ([]byte, error) {
|
||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qmc[downloadFile] init request: %w", err)
|
||
|
}
|
||
|
|
||
|
//req.Header.Set("Accept", "image/webp,image/*,*/*;q=0.8") // jpeg is preferred to embed in audio
|
||
|
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.5;q=0.4")
|
||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.47.134 Safari/537.36 QBCore/3.53.47.400 QQBrowser/9.0.2524.400")
|
||
|
|
||
|
resp, err := c.http.Do(req)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("qmc[downloadFile] send request: %w", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
if resp.StatusCode != http.StatusOK {
|
||
|
return nil, fmt.Errorf("qmc[downloadFile] unexpected http status %s", resp.Status)
|
||
|
}
|
||
|
|
||
|
return io.ReadAll(resp.Body)
|
||
|
}
|
||
|
|
||
|
func NewQQMusicClient() *QQMusic {
|
||
|
return &QQMusic{
|
||
|
http: &http.Client{
|
||
|
Timeout: 10 * time.Second,
|
||
|
},
|
||
|
}
|
||
|
}
|