feat(qq): include albums in search results

Add mapQqAlbums pure helper (exported, TDD-covered) and wire a parallel
req_album sub-request (search_type: 8) into search(), populating the
albums field of SearchResult.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Sonnet 4.6 committed 2026-05-07 20:20:05 +08:00
1 parent a2453784c1
commit 7d9081efc1
2 files changed
+74 -6

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { mapQqAlbums } from "./qq.js";
describe("QQ adapter", () => {
it("mapQqAlbums maps albumMID-style raw entries", () => {
const raw = [
{
albumMID: "abc",
albumName: "Aero",
singerName: "Singer A",
},
{
albumMID: "xyz",
albumName: "Beta",
singer: [{ name: "Singer B" }, { name: "Singer C" }],
},
];
const out = mapQqAlbums(raw);
expect(out).toHaveLength(2);
expect(out[0]).toMatchObject({
id: "abc",
name: "Aero",
artist: "Singer A",
platform: "qq",
});
expect(out[0].coverUrl).toContain("T002R300x300M000abc.jpg");
expect(out[1].artist).toBe("Singer B / Singer C");
expect(out[1].coverUrl).toContain("xyz");
});
it("mapQqAlbums returns [] for empty/null input", () => {
expect(mapQqAlbums([])).toEqual([]);
expect(mapQqAlbums(null as any)).toEqual([]);
expect(mapQqAlbums(undefined as any)).toEqual([]);
});
it("mapQqAlbums falls back to albumPic when no albumMID", () => {
const raw = [{ albumName: "C", albumPic: "https://x/p.jpg", singerName: "S" }];
const out = mapQqAlbums(raw);
expect(out[0].coverUrl).toBe("https://x/p.jpg");
expect(out[0].id).toBe("");
});
});
+31 -6
View File
@@ -8,6 +8,7 @@ import type {
SearchResult,
QrCodeResult,
AuthStatus,
Album,
} from "./provider.js";
import { parseLyrics } from "./netease.js";
@@ -27,6 +28,26 @@ const qqFavApi = axios.create({
headers: { referer: "https://y.qq.com/" },
});
export function mapQqAlbums(raw: any[] | null | undefined): Album[] {
if (!Array.isArray(raw)) return [];
return raw.map((a) => {
const id = String(a.albumMID ?? a.mid ?? a.albumID ?? "");
const artist = a.singerName
?? (Array.isArray(a.singer) ? a.singer.map((s: any) => s.name).join(" / ") : "");
const coverUrl = id
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${id}.jpg`
: (a.albumPic ?? "");
return {
id,
name: a.albumName ?? a.title ?? "",
artist,
coverUrl,
songCount: a.song_count ?? a.songCount ?? 0,
platform: "qq" as const,
};
});
}
function computeGtk(pSkey: string): number {
let hash = 5381;
for (let i = 0; i < pSkey.length; i++) {
@@ -65,11 +86,12 @@ export class QQMusicProvider implements MusicProvider {
req_0: {
module: "music.search.SearchCgiService",
method: "DoSearchForQQMusicDesktop",
param: {
searchid: "1",
query,
num_per_page: Math.min(limit, 50),
},
param: { searchid: "1", query, num_per_page: Math.min(limit, 50), search_type: 0 },
},
req_album: {
module: "music.search.SearchCgiService",
method: "DoSearchForQQMusicDesktop",
param: { searchid: "1", query, num_per_page: 5, search_type: 8 },
},
});
const res = await qqDirectApi.get("/cgi-bin/musicu.fcg", {
@@ -90,7 +112,10 @@ export class QQMusicProvider implements MusicProvider {
platform: "qq",
}));
return { songs, playlists: [], albums: [] };
const albumList: any[] = res.data?.req_album?.data?.body?.album?.list ?? [];
const albums = mapQqAlbums(albumList);
return { songs, playlists: [], albums };
}
async getSongUrl(songId: string, quality?: string): Promise<string | null> {