feat: read the songs in QQ Music playlists

This commit is contained in:
DiheChen committed 2026-11-11 00:28:21 +08:00
1 parent 52c4aaa9d3
commit 19a135c983
5 files changed
+132

No files matched your search

+60
View File
@@ -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
})
}
+69
View File
@@ -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":[]}
+1
View File
@@ -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":"歌手丙"}]}]}]}