mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(qq): add radar FM stream
This commit is contained in:
1 parent
a861b41809
commit
e0d17cf404
6 files changed
+217
-84
No files matched your search
+25
-1
@@ -1,7 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { mapQqAlbums } from "./qq.js";
|
||||
import { mapQqAlbums, mapQqSongs } from "./qq.js";
|
||||
|
||||
describe("QQ adapter", () => {
|
||||
it("mapQqSongs maps QQMusicApi-style song entries", () => {
|
||||
const out = mapQqSongs([
|
||||
{
|
||||
mid: "001abc",
|
||||
name: "Radar Song",
|
||||
singer: [{ name: "Singer A" }, { name: "Singer B" }],
|
||||
album: { name: "Album A", mid: "alb001" },
|
||||
interval: 243,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(out).toEqual([
|
||||
{
|
||||
id: "001abc",
|
||||
name: "Radar Song",
|
||||
artist: "Singer A / Singer B",
|
||||
album: "Album A",
|
||||
duration: 243,
|
||||
coverUrl: "https://y.gtimg.cn/music/photo_new/T002R300x300M000alb001.jpg",
|
||||
platform: "qq",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("mapQqAlbums maps albumMID-style raw entries", () => {
|
||||
const raw = [
|
||||
{
|
||||
|
||||
+106
-46
@@ -39,6 +39,24 @@ const qqFavApi = axios.create({
|
||||
headers: { referer: "https://y.qq.com/" },
|
||||
});
|
||||
|
||||
export function mapQqSongs(raw: any[] | null | undefined): Song[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((s) => {
|
||||
const albumMid = s.album?.mid ?? s.album?.pmid ?? s.albummid ?? s.albumMid ?? "";
|
||||
return {
|
||||
id: String(s.mid ?? s.songmid ?? s.songMID ?? s.id ?? s.songid ?? s.songId ?? ""),
|
||||
name: s.title ?? s.name ?? s.songname ?? "",
|
||||
artist: (s.singer ?? s.singers ?? []).map((a: any) => a.name ?? a.title ?? "").filter(Boolean).join(" / "),
|
||||
album: s.album?.name ?? s.album?.title ?? s.albumname ?? "",
|
||||
duration: s.interval ?? Math.round((s.duration ?? 0) / 1000),
|
||||
coverUrl: albumMid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${albumMid}.jpg`
|
||||
: "",
|
||||
platform: "qq" as const,
|
||||
};
|
||||
}).filter((s) => s.id);
|
||||
}
|
||||
|
||||
export function mapQqAlbums(raw: any[] | null | undefined): Album[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((a) => {
|
||||
@@ -72,6 +90,7 @@ export class QQMusicProvider implements MusicProvider {
|
||||
private api: AxiosInstance;
|
||||
private cookie = "";
|
||||
private quality = "exhigh";
|
||||
private radarPage = 1;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.api = axios.create({
|
||||
@@ -92,6 +111,30 @@ export class QQMusicProvider implements MusicProvider {
|
||||
return this.cookie ? { cookie: this.cookie } : {};
|
||||
}
|
||||
|
||||
private get directCookieHeaders(): Record<string, string> {
|
||||
return this.cookie ? { Cookie: this.cookie } : {};
|
||||
}
|
||||
|
||||
private buildMusicuPayload(module: string, method: string, param: Record<string, unknown>): Record<string, unknown> {
|
||||
const uinMatch = /(?:^|; )(?:uin|qqmusic_uin)=o?0?(\d+)/.exec(this.cookie);
|
||||
const pSkeyMatch = /(?:^|; )p_skey=([^;]+)/.exec(this.cookie);
|
||||
return {
|
||||
comm: {
|
||||
ct: 24,
|
||||
cv: 4747474,
|
||||
platform: "yqq.json",
|
||||
uin: uinMatch ? uinMatch[1] : "0",
|
||||
g_tk: pSkeyMatch ? computeGtk(pSkeyMatch[1]) : 5381,
|
||||
format: "json",
|
||||
inCharset: "utf-8",
|
||||
outCharset: "utf-8",
|
||||
notice: 0,
|
||||
need_new_code: 1,
|
||||
},
|
||||
req_0: { module, method, param },
|
||||
};
|
||||
}
|
||||
|
||||
async search(query: string, limit = 20): Promise<SearchResult> {
|
||||
// Primary: u.y.qq.com/cgi-bin/musicu.fcg — supports songs + albums +
|
||||
// playlists. Fixed per https://github.com/ZHANGTIANYAO1/teamspeak-music-bot/issues/61
|
||||
@@ -141,17 +184,7 @@ export class QQMusicProvider implements MusicProvider {
|
||||
res.data?.req_0?.data?.body?.song?.list ?? [];
|
||||
if (songList.length === 0) return null;
|
||||
|
||||
const songs: Song[] = songList.map((s: any) => ({
|
||||
id: String(s.mid ?? s.id),
|
||||
name: s.title ?? s.name ?? "",
|
||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||
album: s.album?.name ?? s.album?.title ?? "",
|
||||
duration: s.interval ?? 0,
|
||||
coverUrl: s.album?.mid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
|
||||
: "",
|
||||
platform: "qq",
|
||||
}));
|
||||
const songs = mapQqSongs(songList);
|
||||
|
||||
const albumList: any[] = res.data?.req_album?.data?.body?.album?.list ?? [];
|
||||
const albums = mapQqAlbums(albumList);
|
||||
@@ -203,17 +236,7 @@ export class QQMusicProvider implements MusicProvider {
|
||||
? (songRes.value.data?.data?.song?.list ?? [])
|
||||
: [];
|
||||
|
||||
const songs: Song[] = songList.map((s: any) => ({
|
||||
id: String(s.songmid ?? s.songid ?? ""),
|
||||
name: s.songname ?? s.name ?? "",
|
||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||
album: s.albumname ?? s.album?.name ?? "",
|
||||
duration: s.interval ?? 0,
|
||||
coverUrl: s.albummid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
|
||||
: "",
|
||||
platform: "qq",
|
||||
}));
|
||||
const songs = mapQqSongs(songList);
|
||||
|
||||
const albumList: any[] =
|
||||
albumRes.status === "fulfilled"
|
||||
@@ -339,19 +362,7 @@ export class QQMusicProvider implements MusicProvider {
|
||||
});
|
||||
const cdlist = res.data?.response?.cdlist ?? [];
|
||||
if (cdlist.length === 0) return [];
|
||||
return (cdlist[0].songlist ?? []).map((s: any) => ({
|
||||
id: String(s.mid ?? s.songmid ?? s.songid),
|
||||
name: s.songname ?? s.name ?? "",
|
||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||
album: s.albumname ?? "",
|
||||
duration: s.interval ?? 0,
|
||||
coverUrl: s.album?.mid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
|
||||
: s.albummid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
|
||||
: "",
|
||||
platform: "qq",
|
||||
}));
|
||||
return mapQqSongs(cdlist[0].songlist ?? []);
|
||||
}
|
||||
|
||||
async getPlaylistDetail(playlistId: string): Promise<PlaylistDetail | null> {
|
||||
@@ -386,17 +397,7 @@ export class QQMusicProvider implements MusicProvider {
|
||||
const res = await this.api.get("/getAlbumInfo", {
|
||||
params: { albummid: albumId, ...this.cookieParams },
|
||||
});
|
||||
return (res.data?.response?.data?.list ?? []).map((s: any) => ({
|
||||
id: String(s.songmid ?? s.songid),
|
||||
name: s.songname ?? "",
|
||||
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
|
||||
album: s.albumname ?? "",
|
||||
duration: s.interval ?? 0,
|
||||
coverUrl: s.albummid
|
||||
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
|
||||
: "",
|
||||
platform: "qq",
|
||||
}));
|
||||
return mapQqSongs(res.data?.response?.data?.list ?? []);
|
||||
}
|
||||
|
||||
async getLyrics(songId: string): Promise<LyricLine[]> {
|
||||
@@ -522,6 +523,65 @@ export class QQMusicProvider implements MusicProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async getPersonalFm(): Promise<Song[]> {
|
||||
const radarSongs = await this.getRadarRecommendSongs();
|
||||
if (radarSongs.length > 0) return radarSongs;
|
||||
return this.getGuessRecommendSongs();
|
||||
}
|
||||
|
||||
private async getRadarRecommendSongs(): Promise<Song[]> {
|
||||
try {
|
||||
const page = this.radarPage;
|
||||
const res = await qqMusicuApi.post(
|
||||
"/cgi-bin/musicu.fcg",
|
||||
this.buildMusicuPayload(
|
||||
"music.recommend.TrackRelationServer",
|
||||
"GetRadarSong",
|
||||
{
|
||||
Page: page,
|
||||
ReqType: 0,
|
||||
FavSongs: [],
|
||||
EntranceSongs: [],
|
||||
}
|
||||
),
|
||||
{ headers: { referer: "https://y.qq.com/", ...this.directCookieHeaders } }
|
||||
);
|
||||
const tracks = (res.data?.req_0?.data?.VecSongs ?? [])
|
||||
.map((item: any) => item?.Track)
|
||||
.filter(Boolean);
|
||||
const songs = mapQqSongs(tracks);
|
||||
if (songs.length > 0) {
|
||||
this.radarPage = page + 1;
|
||||
}
|
||||
return songs;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getGuessRecommendSongs(): Promise<Song[]> {
|
||||
try {
|
||||
const res = await qqMusicuApi.post(
|
||||
"/cgi-bin/musicu.fcg",
|
||||
this.buildMusicuPayload(
|
||||
"music.radioProxy.MbTrackRadioSvr",
|
||||
"get_radio_track",
|
||||
{
|
||||
id: 99,
|
||||
num: 5,
|
||||
from: 0,
|
||||
scene: 0,
|
||||
song_ids: [],
|
||||
}
|
||||
),
|
||||
{ headers: { referer: "https://y.qq.com/", ...this.directCookieHeaders } }
|
||||
);
|
||||
return mapQqSongs(res.data?.req_0?.data?.Tracks ?? []);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getUserPlaylists(): Promise<Playlist[]> {
|
||||
if (!this.cookie) return [];
|
||||
const uinMatch = /(?:^|; )uin=o?0?(\d+)/.exec(this.cookie);
|
||||
|
||||
Reference in new issue
Block a user