mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-04 05:52:50 +08:00
Adds artist support for the NetEase and QQ providers plus the matching UI. Backend: - SearchResult gains `artists`; new optional MusicProvider methods getArtistDetail / getArtistSongs / getArtistAlbums / getArtistAllSongs. - NetEase: /cloudsearch type=100 for artist search, /artists, /artist/songs, /artist/album and /artist/desc for the artist page. - QQ: singer search rides along in the existing musicu.fcg batch (search_type=1); singer detail via music.web_singer_info_svr. QQ exposes no working singer-song paging endpoint, so the full catalogue is built from the hot 50 plus every album of the singer (album search filtered by singerMID, songs fetched per album, de-duplicated, cached for 10 minutes). A failed album sweep is never cached and degrades to the hot list. - API: GET /api/music/artist/:id and POST /api/player/:botId/play-artist (player.control capability, guest flag playCollection); /search/all now aggregates artists too. Frontend: - Search history in localStorage (max 10, per platform, never shared between users), shown as a dropdown under the search box and as 最近搜索 chips. - Artist row in the search results; new /artist/:id page (portrait, aliases, stats, description, top songs, album shelf) with 播放 / 随机播放, which queue the singer's whole catalogue. - playArtist store action. Tests cover the provider mappers, the new routes, the play-artist collector (paging, de-duplication, 500-track cap), permission gating and the new views.
154 lines
4.3 KiB
TypeScript
154 lines
4.3 KiB
TypeScript
/** All music source platforms. Jellyfin ItemIds are GUID strings — never
|
|
* assume numeric ids when handling a generic Platform. */
|
|
export type Platform =
|
|
| "netease"
|
|
| "qq"
|
|
| "bilibili"
|
|
| "youtube"
|
|
| "local"
|
|
| "kugou"
|
|
| "spotify"
|
|
| "jellyfin";
|
|
|
|
export interface Song {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number; // seconds
|
|
coverUrl: string;
|
|
platform: Platform;
|
|
/** 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: Platform;
|
|
}
|
|
|
|
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: Platform;
|
|
}
|
|
|
|
/** An artist / singer entity. Only sources with a real artist concept expose
|
|
* these (NetEase, QQ); the others simply never return `SearchResult.artists`
|
|
* and leave the optional provider methods unimplemented. */
|
|
export interface Artist {
|
|
id: string;
|
|
name: string;
|
|
avatarUrl: string;
|
|
platform: Platform;
|
|
/** Alternate names / romanizations (NetEase alias, QQ other_name). */
|
|
aliases?: string[];
|
|
songCount?: number;
|
|
albumCount?: number;
|
|
}
|
|
|
|
export interface ArtistDetail extends Artist {
|
|
/** Short biography, when the source provides one. */
|
|
description?: string;
|
|
}
|
|
|
|
/** One page of an artist's COMPLETE catalogue (the "全部歌曲" list), as opposed
|
|
* to `getArtistSongs`, which only ever returns the hot top-N. */
|
|
export interface ArtistSongPage {
|
|
songs: Song[];
|
|
/** Total tracks the source reports for this artist (best effort). */
|
|
total: number;
|
|
hasMore: boolean;
|
|
}
|
|
|
|
export interface LyricLine {
|
|
time: number; // seconds
|
|
text: string;
|
|
translation?: string;
|
|
}
|
|
|
|
export interface SearchResult {
|
|
songs: Song[];
|
|
playlists: Playlist[];
|
|
albums: Album[];
|
|
/** Present only for sources with an artist entity (NetEase, QQ). */
|
|
artists?: Artist[];
|
|
}
|
|
|
|
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: Platform;
|
|
|
|
search(query: string, limit?: number, offset?: 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>;
|
|
getArtistDetail?(artistId: string): Promise<ArtistDetail | null>;
|
|
/** The artist's most popular tracks, best-first. */
|
|
getArtistSongs?(artistId: string, limit?: number): Promise<Song[]>;
|
|
/** One page of the artist's full catalogue, best-first. Sources that can only
|
|
* expose a fixed top-N list leave this unimplemented (the route then 501s and
|
|
* the web hides the "全部歌曲" section). */
|
|
getArtistAllSongs?(
|
|
artistId: string,
|
|
offset?: number,
|
|
limit?: number
|
|
): Promise<ArtistSongPage>;
|
|
getArtistAlbums?(artistId: string, limit?: number): Promise<Album[]>;
|
|
}
|