base.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package client
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "net/http"
  9. "time"
  10. )
  11. type QQMusic struct {
  12. http *http.Client
  13. }
  14. func (c *QQMusic) rpcDoRequest(ctx context.Context, reqBody any) ([]byte, error) {
  15. reqBodyBuf, err := json.Marshal(reqBody)
  16. if err != nil {
  17. return nil, fmt.Errorf("qqMusicClient[rpcDoRequest] marshal request: %w", err)
  18. }
  19. const endpointURL = "https://u.y.qq.com/cgi-bin/musicu.fcg"
  20. req, err := http.NewRequestWithContext(ctx, http.MethodPost,
  21. endpointURL+fmt.Sprintf("?pcachetime=%d", time.Now().Unix()),
  22. bytes.NewReader(reqBodyBuf),
  23. )
  24. if err != nil {
  25. return nil, fmt.Errorf("qqMusicClient[rpcDoRequest] create request: %w", err)
  26. }
  27. req.Header.Set("Accept", "*/*")
  28. req.Header.Set("Accept-Language", "zh-CN")
  29. req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)")
  30. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  31. // req.Header.Set("Accept-Encoding", "gzip, deflate")
  32. reqp, err := c.http.Do(req)
  33. if err != nil {
  34. return nil, fmt.Errorf("qqMusicClient[rpcDoRequest] send request: %w", err)
  35. }
  36. defer reqp.Body.Close()
  37. respBodyBuf, err := io.ReadAll(reqp.Body)
  38. if err != nil {
  39. return nil, fmt.Errorf("qqMusicClient[rpcDoRequest] read response: %w", err)
  40. }
  41. return respBodyBuf, nil
  42. }
  43. type rpcRequest struct {
  44. Method string `json:"method"`
  45. Module string `json:"module"`
  46. Param any `json:"param"`
  47. }
  48. type rpcResponse struct {
  49. Code int `json:"code"`
  50. Ts int64 `json:"ts"`
  51. StartTs int64 `json:"start_ts"`
  52. TraceID string `json:"traceid"`
  53. }
  54. type rpcSubResponse struct {
  55. Code int `json:"code"`
  56. Data json.RawMessage `json:"data"`
  57. }
  58. func (c *QQMusic) rpcCall(ctx context.Context,
  59. protocol string, method string, module string,
  60. param any,
  61. ) (json.RawMessage, error) {
  62. reqBody := map[string]any{protocol: rpcRequest{
  63. Method: method,
  64. Module: module,
  65. Param: param,
  66. }}
  67. respBodyBuf, err := c.rpcDoRequest(ctx, reqBody)
  68. if err != nil {
  69. return nil, fmt.Errorf("qqMusicClient[rpcCall] do request: %w", err)
  70. }
  71. // check rpc response status
  72. respStatus := rpcResponse{}
  73. if err := json.Unmarshal(respBodyBuf, &respStatus); err != nil {
  74. return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal response: %w", err)
  75. }
  76. if respStatus.Code != 0 {
  77. return nil, fmt.Errorf("qqMusicClient[rpcCall] rpc error: %d", respStatus.Code)
  78. }
  79. // parse response data
  80. var respBody map[string]json.RawMessage
  81. if err := json.Unmarshal(respBodyBuf, &respBody); err != nil {
  82. return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal response: %w", err)
  83. }
  84. subRespBuf, ok := respBody[protocol]
  85. if !ok {
  86. return nil, fmt.Errorf("qqMusicClient[rpcCall] sub-response not found")
  87. }
  88. subResp := rpcSubResponse{}
  89. if err := json.Unmarshal(subRespBuf, &subResp); err != nil {
  90. return nil, fmt.Errorf("qqMusicClient[rpcCall] unmarshal sub-response: %w", err)
  91. }
  92. if subResp.Code != 0 {
  93. return nil, fmt.Errorf("qqMusicClient[rpcCall] sub-response error: %d", subResp.Code)
  94. }
  95. return subResp.Data, nil
  96. }
  97. func (c *QQMusic) downloadFile(ctx context.Context, url string) ([]byte, error) {
  98. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  99. if err != nil {
  100. return nil, fmt.Errorf("qmc[downloadFile] init request: %w", err)
  101. }
  102. //req.Header.Set("Accept", "image/webp,image/*,*/*;q=0.8") // jpeg is preferred to embed in audio
  103. req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.5;q=0.4")
  104. 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")
  105. resp, err := c.http.Do(req)
  106. if err != nil {
  107. return nil, fmt.Errorf("qmc[downloadFile] send request: %w", err)
  108. }
  109. defer resp.Body.Close()
  110. if resp.StatusCode != http.StatusOK {
  111. return nil, fmt.Errorf("qmc[downloadFile] unexpected http status %s", resp.Status)
  112. }
  113. return io.ReadAll(resp.Body)
  114. }
  115. func NewQQMusicClient() *QQMusic {
  116. return &QQMusic{
  117. http: &http.Client{
  118. Timeout: 10 * time.Second,
  119. },
  120. }
  121. }