From a6881403ef3d4a1085ee039c0ec92e5ad97403e1 Mon Sep 17 00:00:00 2001 From: saopig1 Date: Mon, 30 Mar 2026 15:14:27 +0800 Subject: [PATCH] feat: add BiliBili audio source integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement BiliBiliProvider for video-as-audio playback using direct BiliBili API calls (search, video info, DASH audio URL extraction). Add QR code login support and cookie persistence. Update FFmpeg to send Referer header for BiliBili CDN URLs. Extend platform union type to "netease" | "qq" | "bilibili" across all interfaces. Add -b flag for chat commands and B站 badge in web UI. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/audio/player.ts | 14 +- src/audio/queue.ts | 2 +- src/bot/instance.ts | 23 ++- src/bot/manager.ts | 5 + src/data/database.ts | 2 +- src/index.ts | 6 + src/music/auth.ts | 8 +- src/music/bilibili.ts | 258 ++++++++++++++++++++++++++++++++ src/music/provider.ts | 8 +- src/web/api/auth.ts | 8 +- src/web/api/music.ts | 10 +- src/web/api/player.ts | 13 +- src/web/server.ts | 7 +- web/src/components/SongCard.vue | 9 +- web/src/views/Settings.vue | 88 ++++++++++- 15 files changed, 425 insertions(+), 36 deletions(-) create mode 100644 src/music/bilibili.ts diff --git a/src/audio/player.ts b/src/audio/player.ts index 8940552..f637c48 100644 --- a/src/audio/player.ts +++ b/src/audio/player.ts @@ -58,11 +58,21 @@ export class AudioPlayer extends EventEmitter { this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback"); - const args = [ + const args: string[] = []; + + // BiliBili CDN requires Referer header for audio playback + if (url.includes("bilivideo") || url.includes("bilibili")) { + args.push( + "-headers", + "Referer: https://www.bilibili.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n" + ); + } + + args.push( "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5", - ]; + ); if (seekSeconds > 0) { args.push("-ss", String(seekSeconds)); } diff --git a/src/audio/queue.ts b/src/audio/queue.ts index 5644b07..4e58ca0 100644 --- a/src/audio/queue.ts +++ b/src/audio/queue.ts @@ -10,7 +10,7 @@ export interface QueuedSong { name: string; artist: string; album: string; - platform: "netease" | "qq"; + platform: "netease" | "qq" | "bilibili"; url?: string; // resolved lazily at play time coverUrl: string; duration: number; // seconds diff --git a/src/bot/instance.ts b/src/bot/instance.ts index 097974d..e2c8392 100644 --- a/src/bot/instance.ts +++ b/src/bot/instance.ts @@ -22,6 +22,7 @@ export interface BotInstanceOptions { tsOptions: TS3ClientOptions; neteaseProvider: MusicProvider; qqProvider: MusicProvider; + bilibiliProvider: MusicProvider; database: BotDatabase; config: BotConfig; logger: Logger; @@ -49,6 +50,7 @@ export class BotInstance extends EventEmitter { private queue: PlayQueue; private neteaseProvider: MusicProvider; private qqProvider: MusicProvider; + private bilibiliProvider: MusicProvider; private database: BotDatabase; private config: BotConfig; private logger: Logger; @@ -62,6 +64,7 @@ export class BotInstance extends EventEmitter { this.name = options.name; this.neteaseProvider = options.neteaseProvider; this.qqProvider = options.qqProvider; + this.bilibiliProvider = options.bilibiliProvider; this.database = options.database; this.config = options.config; this.logger = options.logger.child({ botId: this.id }); @@ -207,12 +210,15 @@ export class BotInstance extends EventEmitter { } } - getProviderFor(platform: "netease" | "qq"): MusicProvider { + getProviderFor(platform: "netease" | "qq" | "bilibili"): MusicProvider { + if (platform === "bilibili") return this.bilibiliProvider; return platform === "qq" ? this.qqProvider : this.neteaseProvider; } - private getProvider(useQQ: boolean): MusicProvider { - return useQQ ? this.qqProvider : this.neteaseProvider; + private getProvider(flags: Set): MusicProvider { + if (flags.has("b")) return this.bilibiliProvider; + if (flags.has("q")) return this.qqProvider; + return this.neteaseProvider; } /** Resolve URL for a song and start playing it. Skips to next if URL fails. */ @@ -245,7 +251,7 @@ export class BotInstance extends EventEmitter { private async cmdPlay(cmd: ParsedCommand): Promise { if (!cmd.args) return "Usage: !play "; - const provider = this.getProvider(cmd.flags.has("q")); + const provider = this.getProvider(cmd.flags); const result = await provider.search(cmd.args, 1); if (result.songs.length === 0) return `No results found for: ${cmd.args}`; @@ -262,7 +268,7 @@ export class BotInstance extends EventEmitter { private async cmdAdd(cmd: ParsedCommand): Promise { if (!cmd.args) return "Usage: !add "; - const provider = this.getProvider(cmd.flags.has("q")); + const provider = this.getProvider(cmd.flags); const result = await provider.search(cmd.args, 1); if (result.songs.length === 0) return `No results found for: ${cmd.args}`; @@ -367,7 +373,7 @@ export class BotInstance extends EventEmitter { private async cmdPlaylist(cmd: ParsedCommand): Promise { if (!cmd.args) return "Usage: !playlist "; - const provider = this.getProvider(cmd.flags.has("q")); + const provider = this.getProvider(cmd.flags); const id = this.extractId(cmd.args); const songs = await provider.getPlaylistSongs(id); if (songs.length === 0) return "Playlist is empty or not found"; @@ -384,7 +390,7 @@ export class BotInstance extends EventEmitter { private async cmdAlbum(cmd: ParsedCommand): Promise { if (!cmd.args) return "Usage: !album "; - const provider = this.getProvider(cmd.flags.has("q")); + const provider = this.getProvider(cmd.flags); const songs = await provider.getAlbumSongs(cmd.args); if (songs.length === 0) return "Album is empty or not found"; @@ -437,7 +443,7 @@ export class BotInstance extends EventEmitter { private async cmdLyrics(): Promise { const song = this.queue.current(); if (!song) return "Nothing is playing"; - const provider = this.getProvider(song.platform === "qq"); + const provider = this.getProviderFor(song.platform); const lyrics = await provider.getLyrics(song.id); if (lyrics.length === 0) return "No lyrics available"; const lines = lyrics.slice(0, 10).map((l) => l.text); @@ -461,6 +467,7 @@ export class BotInstance extends EventEmitter { "TSMusicBot Commands:", `${p}play — Search and play`, `${p}play -q — Search from QQ Music`, + `${p}play -b — Search from BiliBili`, `${p}add — Add to queue`, `${p}pause/resume — Pause/resume`, `${p}next/prev — Next/previous`, diff --git a/src/bot/manager.ts b/src/bot/manager.ts index 2ed8470..d9e8c6a 100644 --- a/src/bot/manager.ts +++ b/src/bot/manager.ts @@ -23,6 +23,7 @@ export class BotManager { private bots = new Map(); private neteaseProvider: MusicProvider; private qqProvider: MusicProvider; + private bilibiliProvider: MusicProvider; private database: BotDatabase; private config: BotConfig; private logger: Logger; @@ -30,12 +31,14 @@ export class BotManager { constructor( neteaseProvider: MusicProvider, qqProvider: MusicProvider, + bilibiliProvider: MusicProvider, database: BotDatabase, config: BotConfig, logger: Logger ) { this.neteaseProvider = neteaseProvider; this.qqProvider = qqProvider; + this.bilibiliProvider = bilibiliProvider; this.database = database; this.config = config; this.logger = logger; @@ -57,6 +60,7 @@ export class BotManager { }, neteaseProvider: this.neteaseProvider, qqProvider: this.qqProvider, + bilibiliProvider: this.bilibiliProvider, database: this.database, config: this.config, logger: this.logger, @@ -125,6 +129,7 @@ export class BotManager { }, neteaseProvider: this.neteaseProvider, qqProvider: this.qqProvider, + bilibiliProvider: this.bilibiliProvider, database: this.database, config: this.config, logger: this.logger, diff --git a/src/data/database.ts b/src/data/database.ts index 75f02d7..8f043ba 100644 --- a/src/data/database.ts +++ b/src/data/database.ts @@ -6,7 +6,7 @@ export interface PlayHistoryEntry { songName: string; artist: string; album: string; - platform: "netease" | "qq"; + platform: "netease" | "qq" | "bilibili"; coverUrl: string; } diff --git a/src/index.ts b/src/index.ts index b86f71d..554825a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ import { createLogger } from "./logger.js"; import { createApiServerManager } from "./music/api-server.js"; import { NeteaseProvider } from "./music/netease.js"; import { QQMusicProvider } from "./music/qq.js"; +import { BiliBiliProvider } from "./music/bilibili.js"; import { createCookieStore } from "./music/auth.js"; import { BotManager } from "./bot/manager.js"; import { createWebServer } from "./web/server.js"; @@ -34,16 +35,20 @@ async function main() { const neteaseProvider = new NeteaseProvider(apiServer.getNeteaseBaseUrl()); const qqProvider = new QQMusicProvider(apiServer.getQQMusicBaseUrl()); + const bilibiliProvider = new BiliBiliProvider(); const cookieStore = createCookieStore(COOKIE_DIR); const neteaseCookie = cookieStore.load("netease"); if (neteaseCookie) neteaseProvider.setCookie(neteaseCookie); const qqCookie = cookieStore.load("qq"); if (qqCookie) qqProvider.setCookie(qqCookie); + const bilibiliCookie = cookieStore.load("bilibili"); + if (bilibiliCookie) bilibiliProvider.setCookie(bilibiliCookie); const botManager = new BotManager( neteaseProvider, qqProvider, + bilibiliProvider, db, config, logger @@ -55,6 +60,7 @@ async function main() { botManager, neteaseProvider, qqProvider, + bilibiliProvider, database: db, config, logger, diff --git a/src/music/auth.ts b/src/music/auth.ts index b71bc75..1f6272f 100644 --- a/src/music/auth.ts +++ b/src/music/auth.ts @@ -2,8 +2,8 @@ import fs from "node:fs"; import path from "node:path"; export interface CookieStore { - save(platform: "netease" | "qq", cookie: string): void; - load(platform: "netease" | "qq"): string; + save(platform: "netease" | "qq" | "bilibili", cookie: string): void; + load(platform: "netease" | "qq" | "bilibili"): string; } export function createCookieStore(cookieDir: string): CookieStore { @@ -12,7 +12,7 @@ export function createCookieStore(cookieDir: string): CookieStore { } return { - save(platform: "netease" | "qq", cookie: string): void { + save(platform: "netease" | "qq" | "bilibili", cookie: string): void { const filePath = path.join(cookieDir, `${platform}.json`); fs.writeFileSync( filePath, @@ -21,7 +21,7 @@ export function createCookieStore(cookieDir: string): CookieStore { ); }, - load(platform: "netease" | "qq"): string { + load(platform: "netease" | "qq" | "bilibili"): string { const filePath = path.join(cookieDir, `${platform}.json`); if (!fs.existsSync(filePath)) return ""; try { diff --git a/src/music/bilibili.ts b/src/music/bilibili.ts new file mode 100644 index 0000000..15bf828 --- /dev/null +++ b/src/music/bilibili.ts @@ -0,0 +1,258 @@ +import axios, { type AxiosInstance } from "axios"; +import type { + MusicProvider, + Song, + Playlist, + LyricLine, + SearchResult, + QrCodeResult, + AuthStatus, +} from "./provider.js"; + +const BILIBILI_HEADERS = { + Referer: "https://www.bilibili.com", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", +}; + +export class BiliBiliProvider implements MusicProvider { + readonly platform = "bilibili" as const; + private api: AxiosInstance; + private passportApi: AxiosInstance; + private cookie = ""; + private quality = "high"; + private cidCache = new Map(); + + constructor() { + this.api = axios.create({ + baseURL: "https://api.bilibili.com", + timeout: 15000, + headers: BILIBILI_HEADERS, + }); + this.passportApi = axios.create({ + baseURL: "https://passport.bilibili.com", + timeout: 15000, + headers: BILIBILI_HEADERS, + }); + } + + private get cookieHeaders(): Record { + return this.cookie ? { Cookie: this.cookie } : {}; + } + + setQuality(quality: string): void { + this.quality = quality; + } + + getQuality(): string { + return this.quality; + } + + /** Strip HTML tags from BiliBili search results */ + private stripHtml(str: string): string { + return str.replace(/<[^>]+>/g, ""); + } + + async search(query: string, limit = 20): Promise { + const res = await this.api.get("/x/web-interface/search/type", { + params: { + search_type: "video", + keyword: query, + page_size: limit, + }, + headers: this.cookieHeaders, + }); + + const results = res.data?.data?.result ?? []; + const songs: Song[] = results.map((v: any) => ({ + id: String(v.bvid), + name: this.stripHtml(v.title ?? ""), + artist: v.author ?? "", + album: "", + duration: v.duration + ? typeof v.duration === "string" + ? this.parseDurationString(v.duration) + : v.duration + : 0, + coverUrl: v.pic + ? v.pic.startsWith("//") + ? `https:${v.pic}` + : v.pic + : "", + platform: "bilibili" as const, + })); + + return { songs, playlists: [], albums: [] }; + } + + /** Parse "MM:SS" duration string to seconds */ + private parseDurationString(dur: string): number { + const parts = dur.split(":"); + if (parts.length === 2) { + return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10); + } + return parseInt(dur, 10) || 0; + } + + async getSongDetail(songId: string): Promise { + try { + const res = await this.api.get("/x/web-interface/view", { + params: { bvid: songId }, + headers: this.cookieHeaders, + }); + + const data = res.data?.data; + if (!data) return null; + + // Cache cid for later audio URL fetching + if (data.pages?.[0]?.cid) { + this.cidCache.set(songId, data.pages[0].cid); + } + + return { + id: String(data.bvid), + name: data.title ?? "", + artist: data.owner?.name ?? "", + album: "", + duration: data.duration ?? 0, + coverUrl: data.pic ?? "", + platform: "bilibili" as const, + }; + } catch { + return null; + } + } + + /** Get CID for a bvid, using cache when available */ + private async getCid(bvid: string): Promise { + const cached = this.cidCache.get(bvid); + if (cached) return cached; + + const detail = await this.getSongDetail(bvid); + if (!detail) return null; + return this.cidCache.get(bvid) ?? null; + } + + async getSongUrl(songId: string, _quality?: string): Promise { + const cid = await this.getCid(songId); + if (!cid) return null; + + try { + const res = await this.api.get("/x/player/playurl", { + params: { + cid, + bvid: songId, + fnval: 16, // DASH format + }, + headers: this.cookieHeaders, + }); + + const audioStreams = res.data?.data?.dash?.audio; + if (!audioStreams || audioStreams.length === 0) return null; + + // Pick highest bandwidth audio stream + const best = audioStreams.reduce((a: any, b: any) => + (b.bandwidth ?? 0) > (a.bandwidth ?? 0) ? b : a + ); + + return best.baseUrl ?? best.base_url ?? null; + } catch { + return null; + } + } + + // --- QR Code Login --- + + async getQrCode(): Promise { + const res = await this.passportApi.get( + "/x/passport-login/web/qrcode/generate" + ); + const data = res.data?.data ?? {}; + return { + qrUrl: data.url ?? "", + key: data.qrcode_key ?? "", + }; + } + + async checkQrCodeStatus( + key: string + ): Promise<"waiting" | "scanned" | "confirmed" | "expired"> { + const res = await this.passportApi.get( + "/x/passport-login/web/qrcode/poll", + { params: { qrcode_key: key }, headers: this.cookieHeaders } + ); + + const code = res.data?.data?.code; + switch (code) { + case 0: { + // Login success — extract cookie from response headers + const setCookieHeaders = res.headers["set-cookie"]; + if (setCookieHeaders) { + this.cookie = setCookieHeaders + .map((c: string) => c.split(";")[0]) + .join("; "); + } + // Also check if cookie is returned in response data + if (res.data?.data?.url) { + // BiliBili returns refresh info in the URL, cookie comes from set-cookie headers + } + return "confirmed"; + } + case 86038: + return "expired"; + case 86090: + return "scanned"; + case 86101: + default: + return "waiting"; + } + } + + // --- Auth Status --- + + async getAuthStatus(): Promise { + if (!this.cookie) return { loggedIn: false }; + try { + const res = await this.api.get("/x/web-interface/nav", { + headers: this.cookieHeaders, + }); + const data = res.data?.data; + if (data && data.isLogin) { + return { + loggedIn: true, + nickname: data.uname, + avatarUrl: data.face, + }; + } + } catch { + // ignore + } + return { loggedIn: false }; + } + + setCookie(cookie: string): void { + this.cookie = cookie; + } + + getCookie(): string { + return this.cookie; + } + + // --- Not applicable for BiliBili (video platform) --- + + async getPlaylistSongs(_playlistId: string): Promise { + return []; + } + + async getRecommendPlaylists(): Promise { + return []; + } + + async getAlbumSongs(_albumId: string): Promise { + return []; + } + + async getLyrics(_songId: string): Promise { + return []; + } +} diff --git a/src/music/provider.ts b/src/music/provider.ts index ee81d00..90b5f87 100644 --- a/src/music/provider.ts +++ b/src/music/provider.ts @@ -5,7 +5,7 @@ export interface Song { album: string; duration: number; // seconds coverUrl: string; - platform: "netease" | "qq"; + platform: "netease" | "qq" | "bilibili"; } export interface SongWithUrl extends Song { @@ -17,7 +17,7 @@ export interface Playlist { name: string; coverUrl: string; songCount: number; - platform: "netease" | "qq"; + platform: "netease" | "qq" | "bilibili"; } export interface Album { @@ -26,7 +26,7 @@ export interface Album { artist: string; coverUrl: string; songCount: number; - platform: "netease" | "qq"; + platform: "netease" | "qq" | "bilibili"; } export interface LyricLine { @@ -54,7 +54,7 @@ export interface AuthStatus { } export interface MusicProvider { - readonly platform: "netease" | "qq"; + readonly platform: "netease" | "qq" | "bilibili"; search(query: string, limit?: number): Promise; getSongUrl(songId: string, quality?: string): Promise; diff --git a/src/web/api/auth.ts b/src/web/api/auth.ts index 6fd736e..244618e 100644 --- a/src/web/api/auth.ts +++ b/src/web/api/auth.ts @@ -6,12 +6,14 @@ import type { Logger } from "../../logger.js"; export function createAuthRouter( neteaseProvider: MusicProvider, qqProvider: MusicProvider, + bilibiliProvider: MusicProvider, logger: Logger, cookieStore?: CookieStore ): Router { const router = Router(); function getProvider(platform?: string): MusicProvider { + if (platform === "bilibili") return bilibiliProvider; return platform === "qq" ? qqProvider : neteaseProvider; } @@ -55,7 +57,8 @@ export function createAuthRouter( // When confirmed, persist cookie if (status === "confirmed") { const cookie = provider.getCookie(); - const plat = (platform as string) === "qq" ? "qq" as const : "netease" as const; + const plat = (platform as string) === "bilibili" ? "bilibili" as const + : (platform as string) === "qq" ? "qq" as const : "netease" as const; if (cookie && cookieStore) { cookieStore.save(plat, cookie); logger.info({ platform: plat }, "Cookie persisted to disk"); @@ -118,7 +121,8 @@ export function createAuthRouter( } const provider = getProvider(platform); provider.setCookie(cookie); - const plat = platform === "qq" ? "qq" as const : "netease" as const; + const plat = platform === "bilibili" ? "bilibili" as const + : platform === "qq" ? "qq" as const : "netease" as const; if (cookieStore) { cookieStore.save(plat, cookie); } diff --git a/src/web/api/music.ts b/src/web/api/music.ts index e064956..5797b7c 100644 --- a/src/web/api/music.ts +++ b/src/web/api/music.ts @@ -5,11 +5,13 @@ import type { Logger } from "../../logger.js"; export function createMusicRouter( neteaseProvider: MusicProvider, qqProvider: MusicProvider, + bilibiliProvider: MusicProvider, logger: Logger ): Router { const router = Router(); function getProvider(platform?: string): MusicProvider { + if (platform === "bilibili") return bilibiliProvider; return platform === "qq" ? qqProvider : neteaseProvider; } @@ -40,9 +42,10 @@ export function createMusicRouter( return; } const parsedLimit = parseInt(limit as string) || 20; - const [neteaseResult, qqResult] = await Promise.allSettled([ + const [neteaseResult, qqResult, bilibiliResult] = await Promise.allSettled([ neteaseProvider.search(q as string, parsedLimit), qqProvider.search(q as string, parsedLimit), + bilibiliProvider.search(q as string, parsedLimit), ]); const songs = [ @@ -50,6 +53,7 @@ export function createMusicRouter( ? neteaseResult.value.songs : []), ...(qqResult.status === "fulfilled" ? qqResult.value.songs : []), + ...(bilibiliResult.status === "fulfilled" ? bilibiliResult.value.songs : []), ]; res.json({ songs }); @@ -198,6 +202,7 @@ export function createMusicRouter( res.json({ netease: neteaseProvider.getQuality(), qq: qqProvider.getQuality(), + bilibili: bilibiliProvider.getQuality(), }); }); @@ -214,6 +219,9 @@ export function createMusicRouter( if (!platform || platform === "qq") { qqProvider.setQuality(quality); } + if (!platform || platform === "bilibili") { + bilibiliProvider.setQuality(quality); + } logger.info({ quality, platform }, "Audio quality changed"); res.json({ success: true, quality }); }); diff --git a/src/web/api/player.ts b/src/web/api/player.ts index 5daaba7..305c133 100644 --- a/src/web/api/player.ts +++ b/src/web/api/player.ts @@ -11,6 +11,7 @@ export function createPlayerRouter( database?: BotDatabase, neteaseProvider?: MusicProvider, qqProvider?: MusicProvider, + bilibiliProvider?: MusicProvider, ): Router { const router = Router(); @@ -32,7 +33,7 @@ export function createPlayerRouter( res.status(400).json({ error: "query is required" }); return; } - const flags = platform === "qq" ? "-q" : ""; + const flags = platform === "bilibili" ? "-b" : platform === "qq" ? "-q" : ""; const cmd = parseCommand(`!play ${flags} ${query}`.trim(), "!"); if (!cmd) { res.status(400).json({ error: "Invalid command" }); @@ -49,7 +50,7 @@ export function createPlayerRouter( try { const bot = (req as any).bot; const { query, platform } = req.body; - const flags = platform === "qq" ? "-q" : ""; + const flags = platform === "bilibili" ? "-b" : platform === "qq" ? "-q" : ""; const cmd = parseCommand(`!add ${flags} ${query}`.trim(), "!"); if (!cmd) { res.status(400).json({ error: "Invalid command" }); @@ -173,7 +174,7 @@ export function createPlayerRouter( try { const bot = (req as any).bot; const { playlistId, platform } = req.body; - const flags = platform === "qq" ? "-q" : ""; + const flags = platform === "bilibili" ? "-b" : platform === "qq" ? "-q" : ""; const cmd = parseCommand( `!playlist ${flags} ${playlistId}`.trim(), "!" @@ -191,7 +192,7 @@ export function createPlayerRouter( try { const bot = (req as any).bot; const { playlistId, platform } = req.body; - const provider = platform === "qq" ? qqProvider : neteaseProvider; + const provider = platform === "bilibili" ? bilibiliProvider : platform === "qq" ? qqProvider : neteaseProvider; if (!provider) { res.status(500).json({ error: "Provider not available" }); return; @@ -238,7 +239,7 @@ export function createPlayerRouter( try { const bot = (req as any).bot; const { songId, platform } = req.body; - const provider = platform === "qq" ? qqProvider : neteaseProvider; + const provider = platform === "bilibili" ? bilibiliProvider : platform === "qq" ? qqProvider : neteaseProvider; if (!provider) { res.status(500).json({ error: "Provider not available" }); return; @@ -272,7 +273,7 @@ export function createPlayerRouter( try { const bot = (req as any).bot; const { songId, platform } = req.body; - const provider = platform === "qq" ? qqProvider : neteaseProvider; + const provider = platform === "bilibili" ? bilibiliProvider : platform === "qq" ? qqProvider : neteaseProvider; if (!provider) { res.status(500).json({ error: "Provider not available" }); return; diff --git a/src/web/server.ts b/src/web/server.ts index 15eee6a..af9780d 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -19,6 +19,7 @@ export interface WebServerOptions { botManager: BotManager; neteaseProvider: MusicProvider; qqProvider: MusicProvider; + bilibiliProvider: MusicProvider; database: BotDatabase; config: BotConfig; logger: Logger; @@ -44,15 +45,15 @@ export function createWebServer(options: WebServerOptions): WebServer { ); app.use( "/api/music", - createMusicRouter(options.neteaseProvider, options.qqProvider, logger) + createMusicRouter(options.neteaseProvider, options.qqProvider, options.bilibiliProvider, logger) ); app.use("/api/player", createPlayerRouter( options.botManager, logger, options.database, - options.neteaseProvider, options.qqProvider, + options.neteaseProvider, options.qqProvider, options.bilibiliProvider, )); app.use( "/api/auth", - createAuthRouter(options.neteaseProvider, options.qqProvider, logger, options.cookieStore) + createAuthRouter(options.neteaseProvider, options.qqProvider, options.bilibiliProvider, logger, options.cookieStore) ); app.get("/api/health", (_req, res) => { diff --git a/web/src/components/SongCard.vue b/web/src/components/SongCard.vue index f820259..ce6d6ae 100644 --- a/web/src/components/SongCard.vue +++ b/web/src/components/SongCard.vue @@ -7,8 +7,8 @@ {{ song.name }} {{ song.platform === 'qq' ? 'QQ' : '网易云' }} + :class="song.platform === 'bilibili' ? 'badge-bilibili' : song.platform === 'qq' ? 'badge-qq' : 'badge-netease'" + >{{ song.platform === 'bilibili' ? 'B站' : song.platform === 'qq' ? 'QQ' : '网易云' }}
{{ song.artist }}
@@ -113,6 +113,11 @@ function formatDuration(seconds: number): string { color: #12b76a; } +.badge-bilibili { + background: rgba(0, 161, 214, 0.15); + color: #00a1d6; +} + .song-artist { font-size: 12px; color: var(--text-secondary); diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index 270a683..c581a93 100644 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -193,6 +193,75 @@ + +