mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
Adds a self-contained Kugou provider (bilibili-style: direct API calls, no embedded API server, no new npm dependency) plus full backend + WebUI wiring. Provider (src/music/kugou.ts): search, song-url (with device registration), lyrics (KRC decode), song detail, playlist, album, personal FM, QR login + cookie persistence, and quality. Request signing / crypto / KRC decoding are ported from the MIT-licensed MakcRe/KuGouMusicApi using Node's built-in crypto and zlib (no third-party crypto packages). Wiring: the "kugou" platform is threaded through the provider contract, queue, play-history, bot instance/manager dispatch (getProviderFor + the -k command flag), index/server composition, the music/player/auth routers (unified /search/all, /quality, the platform coercions, QR login), the cookie store, and the WebUI (search source tab + badge, SongCard badge, brand token, and a Kugou QR/cookie login card in Settings). Verified live during development: search, lyrics, and album playback resolve correctly. NOT verifiable in CI (Kugou anti-bot blocks the build host's IP): play-URL resolution, QR login, and VIP audio — these are built faithfully to the reference and need end-to-end testing on a non-flagged IP / a Kugou account. See the header comment in kugou.ts. Includes src/music/kugou.test.ts (mappers + KRC→LRC). An adversarial review pass fixed: pagination truncating on filtered counts, an ms/seconds duration heuristic, dfid soft-fail caching, the /v5/url random-dfid fallback, the FM body identity, and an unguarded nickname decode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
export interface Song {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number; // seconds
|
|
coverUrl: string;
|
|
platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou";
|
|
/** VIP / copyright-restricted: non-VIP users can only play a trial fragment
|
|
* (NetEase fee=1 VIP / fee=4 album-only, or QQ pay.payplay/paytrackprice=1). */
|
|
vip?: boolean;
|
|
}
|
|
|
|
export interface SongWithUrl extends Song {
|
|
url: string;
|
|
}
|
|
|
|
/** getSongUrl 解析结果。trialDuration 缺省 = 完整可播放(VIP 账号 / 免费曲)。 */
|
|
export interface SongUrlResult {
|
|
url: string;
|
|
/** 试听片段时长(秒)。VIP/免费曲为 undefined → 调用方回退完整 duration。 */
|
|
trialDuration?: number;
|
|
}
|
|
|
|
export interface Playlist {
|
|
id: string;
|
|
name: string;
|
|
coverUrl: string;
|
|
songCount: number;
|
|
platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou";
|
|
}
|
|
|
|
export interface PlaylistDetail {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
coverUrl: string;
|
|
songCount: number;
|
|
}
|
|
|
|
export interface Album {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
coverUrl: string;
|
|
songCount: number;
|
|
platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou";
|
|
}
|
|
|
|
export interface LyricLine {
|
|
time: number; // seconds
|
|
text: string;
|
|
translation?: string;
|
|
}
|
|
|
|
export interface SearchResult {
|
|
songs: Song[];
|
|
playlists: Playlist[];
|
|
albums: Album[];
|
|
}
|
|
|
|
export interface QrCodeResult {
|
|
qrUrl: string;
|
|
qrImg?: string; // base64 data URL of QR image
|
|
key: string;
|
|
}
|
|
|
|
export interface AuthStatus {
|
|
loggedIn: boolean;
|
|
nickname?: string;
|
|
avatarUrl?: string;
|
|
}
|
|
|
|
export interface MusicProvider {
|
|
readonly platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou";
|
|
|
|
search(query: string, limit?: number): Promise<SearchResult>;
|
|
getSongUrl(songId: string, quality?: string): Promise<SongUrlResult | null>;
|
|
setQuality(quality: string): void;
|
|
getQuality(): string;
|
|
getSongDetail(songId: string): Promise<Song | null>;
|
|
getPlaylistSongs(playlistId: string): Promise<Song[]>;
|
|
getRecommendPlaylists(): Promise<Playlist[]>;
|
|
getAlbumSongs(albumId: string): Promise<Song[]>;
|
|
getLyrics(songId: string): Promise<LyricLine[]>;
|
|
getQrCode(): Promise<QrCodeResult>;
|
|
checkQrCodeStatus(
|
|
key: string
|
|
): Promise<"waiting" | "scanned" | "confirmed" | "expired">;
|
|
loginWithSms?(phone: string, code: string): Promise<boolean>;
|
|
sendSmsCode?(phone: string): Promise<boolean>;
|
|
setCookie(cookie: string): void;
|
|
getCookie(): string;
|
|
getAuthStatus(): Promise<AuthStatus>;
|
|
getPersonalFm?(): Promise<Song[]>;
|
|
getDailyRecommendSongs?(): Promise<Song[]>;
|
|
getUserPlaylists?(): Promise<Playlist[]>;
|
|
getPlaylistDetail?(playlistId: string): Promise<PlaylistDetail | null>;
|
|
}
|