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 @@ + +