feat: read the songs in QQ Music playlists
This commit is contained in:
1 parent
52c4aaa9d3
commit
19a135c983
5 files changed
+132
No files matched your search
@@ -0,0 +1,60 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
)
|
||||
|
||||
const playlistDetailAPI = "https://i.y.qq.com/qzone-music/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg"
|
||||
|
||||
type playlistDetailResponse struct {
|
||||
Code int `json:"code"`
|
||||
Cdlist []struct {
|
||||
Songlist []struct {
|
||||
SongMID string `json:"songmid"`
|
||||
SongName string `json:"songname"`
|
||||
AlbumName string `json:"albumname"`
|
||||
AlbumMID string `json:"albummid"`
|
||||
Interval int64 `json:"interval"`
|
||||
Singer []singer `json:"singer"`
|
||||
} `json:"songlist"`
|
||||
} `json:"cdlist"`
|
||||
}
|
||||
|
||||
func (r playlistDetailResponse) check() error {
|
||||
if r.Code != 0 {
|
||||
return fmt.Errorf("QQ 音乐返回错误码 %d", r.Code)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) PlaylistTracks(ctx context.Context, playlistID string) ([]port.Song, error) {
|
||||
return cached(c, "playlist_tracks "+playlistID, func() ([]port.Song, error) {
|
||||
var resp playlistDetailResponse
|
||||
query := url.Values{
|
||||
"type": {"1"}, "json": {"1"}, "utf8": {"1"}, "onlysong": {"0"}, "disstid": {playlistID},
|
||||
"format": {"json"}, "g_tk": {"5381"}, "loginUin": {"0"}, "hostUin": {"0"},
|
||||
"inCharset": {"utf8"}, "outCharset": {"utf-8"}, "notice": {"0"}, "platform": {"yqq"}, "needNewCode": {"0"},
|
||||
}
|
||||
if err := c.get(ctx, "qq", "playlist_tracks", playlistDetailAPI, query, desktopHeader, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Cdlist) == 0 {
|
||||
return nil, errors.New("QQ 音乐的歌单不存在或不可见")
|
||||
}
|
||||
// 这个接口一次返回全部曲目,实测 1137 首的歌单也完整,不需要翻页。
|
||||
songs := make([]port.Song, 0, len(resp.Cdlist[0].Songlist))
|
||||
for _, s := range resp.Cdlist[0].Songlist {
|
||||
// 没有 songmid 的条目是下架等不可用的歌,丢弃但保持其余歌曲的原顺序。
|
||||
if s.SongMID == "" {
|
||||
continue
|
||||
}
|
||||
songs = append(songs, song(s.SongMID, s.SongName, s.Singer, s.AlbumName, s.AlbumMID, s.Interval))
|
||||
}
|
||||
return songs, nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dihemoe/alisten/internal/app/port"
|
||||
"github.com/dihemoe/alisten/internal/domain/track"
|
||||
)
|
||||
|
||||
func TestPlaylistTracks(t *testing.T) {
|
||||
c, obs := serve(t, map[string]http.HandlerFunc{
|
||||
"/qzone-music/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg": func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.URL.Query().Get("disstid"); got != "7382629476" {
|
||||
t.Errorf("歌单 ID = %q,期望与搜索返回的 dissid 一致", got)
|
||||
}
|
||||
fixture(t, "playlist-tracks.json")(w, r)
|
||||
},
|
||||
})
|
||||
want := []port.Song{
|
||||
{
|
||||
Track: track.Track{Source: track.QQ, ID: "mid1"},
|
||||
Meta: track.Meta{
|
||||
Title: "第一首", Artist: "歌手甲, 歌手乙", Album: "专辑一",
|
||||
CoverURL: "https://y.gtimg.cn/music/photo_new/T002R300x300M000album1.jpg",
|
||||
PageURL: "https://y.qq.com/n/ryqq/songDetail/mid1",
|
||||
Duration: 200 * time.Second,
|
||||
},
|
||||
},
|
||||
{
|
||||
Track: track.Track{Source: track.QQ, ID: "mid2"},
|
||||
Meta: track.Meta{
|
||||
Title: "第二首", Artist: "歌手丙", Album: "专辑二",
|
||||
CoverURL: "https://y.gtimg.cn/music/photo_new/T002R300x300M000album2.jpg",
|
||||
PageURL: "https://y.qq.com/n/ryqq/songDetail/mid2",
|
||||
Duration: 100 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
// 缓存命中时第二次调用不再发请求。
|
||||
for range 2 {
|
||||
got, err := c.PlaylistTracks(t.Context(), "7382629476")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("歌单内的歌曲 = %#v\n期望 %#v", got, want)
|
||||
}
|
||||
}
|
||||
obs.check(t, "qq playlist_tracks")
|
||||
}
|
||||
|
||||
func TestPlaylistTracksUnavailable(t *testing.T) {
|
||||
for name, file := range map[string]string{
|
||||
"接口返回错误码": "playlist-tracks-missing.json",
|
||||
"cdlist 为空": "playlist-tracks-empty.json",
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
c, _ := serve(t, map[string]http.HandlerFunc{
|
||||
"/qzone-music/fcg-bin/fcg_ucc_getcdinfo_byids_cp.fcg": fixture(t, file),
|
||||
})
|
||||
if _, err := c.PlaylistTracks(t.Context(), "999999999999"); err == nil {
|
||||
t.Error("不存在或不可见的歌单没有返回错误")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"code":0,"subcode":0,"cdnum":0,"cdlist":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"code":10,"subcode":0,"cdnum":0,"cdlist":[]}
|
||||
@@ -0,0 +1 @@
|
||||
{"code":0,"subcode":0,"cdnum":1,"cdlist":[{"dissname":"测试歌单","songlist":[{"songmid":"mid1","songname":"第一首","albumname":"专辑一","albummid":"album1","interval":200,"singer":[{"name":"歌手甲"},{"name":"歌手乙"}]},{"songmid":"","songname":"已下架的歌","albumname":"","albummid":"","interval":0,"singer":[]},{"songmid":"mid2","songname":"第二首","albumname":"专辑二","albummid":"album2","interval":100,"singer":[{"name":"歌手丙"}]}]}]}
|
||||
Reference in new issue
Block a user