diff --git a/src/web/api/player.ts b/src/web/api/player.ts index c24ad4b..1257bd9 100644 --- a/src/web/api/player.ts +++ b/src/web/api/player.ts @@ -1,13 +1,16 @@ import { Router } from "express"; import type { BotManager } from "../../bot/manager.js"; import type { BotDatabase } from "../../data/database.js"; +import type { MusicProvider } from "../../music/provider.js"; import type { Logger } from "../../logger.js"; import { parseCommand } from "../../bot/commands.js"; export function createPlayerRouter( botManager: BotManager, logger: Logger, - database?: BotDatabase + database?: BotDatabase, + neteaseProvider?: MusicProvider, + qqProvider?: MusicProvider, ): Router { const router = Router(); @@ -133,6 +136,119 @@ export function createPlayerRouter( } }); + // Play a playlist by ID — fetches all songs, gets URLs, loads into queue + router.post("/:botId/play-playlist", async (req, res) => { + try { + const bot = (req as any).bot; + const { playlistId, platform } = req.body; + const provider = platform === "qq" ? qqProvider : neteaseProvider; + if (!provider) { + res.status(500).json({ error: "Provider not available" }); + return; + } + + const songs = await provider.getPlaylistSongs(playlistId); + if (songs.length === 0) { + res.json({ message: "Playlist is empty" }); + return; + } + + // Get URL for first song and play it + const queue = bot.getQueueManager(); + const player = bot.getPlayer(); + queue.clear(); + + let firstUrl: string | null = null; + for (const song of songs) { + const url = await provider.getSongUrl(song.id); + if (url) { + queue.add({ ...song, url, platform: provider.platform }); + if (!firstUrl) firstUrl = url; + } + } + + if (firstUrl) { + queue.play(); + player.play(firstUrl); + } + + res.json({ message: `Loaded ${queue.size()} songs. Now playing: ${queue.current()?.name ?? "unknown"}` }); + } catch (err) { + logger.error({ err }, "Play playlist failed"); + res.status(500).json({ error: (err as Error).message }); + } + }); + + // Play a single song by ID — no search needed + router.post("/:botId/play-by-id", async (req, res) => { + try { + const bot = (req as any).bot; + const { songId, platform } = req.body; + const provider = platform === "qq" ? qqProvider : neteaseProvider; + if (!provider) { + res.status(500).json({ error: "Provider not available" }); + return; + } + + const [song, url] = await Promise.all([ + provider.getSongDetail(songId), + provider.getSongUrl(songId), + ]); + + if (!song || !url) { + res.json({ message: "Cannot get song URL" }); + return; + } + + const queue = bot.getQueueManager(); + const player = bot.getPlayer(); + queue.clear(); + queue.add({ ...song, url, platform: provider.platform }); + queue.play(); + player.play(url); + + res.json({ message: `Now playing: ${song.name} - ${song.artist}` }); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } + }); + + // Add a song to queue by ID + router.post("/:botId/add-by-id", async (req, res) => { + try { + const bot = (req as any).bot; + const { songId, platform } = req.body; + const provider = platform === "qq" ? qqProvider : neteaseProvider; + if (!provider) { + res.status(500).json({ error: "Provider not available" }); + return; + } + + const [song, url] = await Promise.all([ + provider.getSongDetail(songId), + provider.getSongUrl(songId), + ]); + + if (!song || !url) { + res.json({ message: "Cannot get song URL" }); + return; + } + + const queue = bot.getQueueManager(); + queue.add({ ...song, url, platform: provider.platform }); + + // If nothing is playing, start playing + if (bot.getPlayer().getState() === "idle") { + queue.play(); + bot.getPlayer().play(url); + } + + res.json({ message: `Added: ${song.name} - ${song.artist} (position ${queue.size()})` }); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } + }); + router.get("/:botId/history", (req, res) => { if (!database) { res.json({ history: [] }); diff --git a/src/web/server.ts b/src/web/server.ts index 20386c4..15eee6a 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -46,7 +46,10 @@ export function createWebServer(options: WebServerOptions): WebServer { "/api/music", createMusicRouter(options.neteaseProvider, options.qqProvider, logger) ); - app.use("/api/player", createPlayerRouter(options.botManager, logger, options.database)); + app.use("/api/player", createPlayerRouter( + options.botManager, logger, options.database, + options.neteaseProvider, options.qqProvider, + )); app.use( "/api/auth", createAuthRouter(options.neteaseProvider, options.qqProvider, logger, options.cookieStore) diff --git a/web/src/stores/player.ts b/web/src/stores/player.ts index 541fb8a..fd99a4a 100644 --- a/web/src/stores/player.ts +++ b/web/src/stores/player.ts @@ -76,10 +76,14 @@ export const usePlayerStore = defineStore('player', { }, updateBotStatus(botId: string, status: BotStatus) { + // Capture previous state BEFORE updating const prev = this.bots.find((b) => b.id === botId); const prevSongId = prev?.currentSong?.id; + const prevPaused = prev?.paused ?? false; + const prevPlaying = prev?.playing ?? false; const newSongId = status.currentSong?.id; + // Update the bot status const index = this.bots.findIndex((b) => b.id === botId); if (index >= 0) { this.bots[index] = status; @@ -87,19 +91,24 @@ export const usePlayerStore = defineStore('player', { this.bots.push(status); } - // Reset play timer when song changes + // Only adjust timing for the active bot + if (botId !== (this.activeBotId ?? this.bots[0]?.id)) return; + + // Song changed — reset timer if (newSongId && newSongId !== prevSongId) { this.playStartedAt = Date.now(); this.pausedElapsed = 0; + return; } - // Track pause/resume - if (status.playing && !status.paused && prev?.paused) { - // Resumed — adjust start time + // Resumed from pause + if (status.playing && !status.paused && prevPaused) { this.playStartedAt = Date.now() - this.pausedElapsed * 1000; + return; } - if (status.paused && !prev?.paused) { - // Paused — save elapsed + + // Paused + if (status.paused && prevPlaying && !prevPaused) { this.pausedElapsed = this.playStartedAt > 0 ? (Date.now() - this.playStartedAt) / 1000 : 0; @@ -154,11 +163,30 @@ export const usePlayerStore = defineStore('player', { this.pausedElapsed = 0; }, + async playById(songId: string, platform = 'netease') { + if (!this.activeBotId) return; + await axios.post(`/api/player/${this.activeBotId}/play-by-id`, { songId, platform }); + this.playStartedAt = Date.now(); + this.pausedElapsed = 0; + }, + async addToQueue(query: string, platform = 'netease') { if (!this.activeBotId) return; await axios.post(`/api/player/${this.activeBotId}/add`, { query, platform }); }, + async addToQueueById(songId: string, platform = 'netease') { + if (!this.activeBotId) return; + await axios.post(`/api/player/${this.activeBotId}/add-by-id`, { songId, platform }); + }, + + async playPlaylist(playlistId: string, platform = 'netease') { + if (!this.activeBotId) return; + await axios.post(`/api/player/${this.activeBotId}/play-playlist`, { playlistId, platform }); + this.playStartedAt = Date.now(); + this.pausedElapsed = 0; + }, + async pause() { if (!this.activeBotId) return; this.pausedElapsed = this.playStartedAt > 0 diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 8fb6ea7..8a282c7 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -54,7 +54,7 @@ v-for="song in store.dailySongs.slice(0, 12)" :key="song.id" class="daily-card hover-scale" - @click="store.play(song.name, song.platform)" + @click="store.playById(song.id, song.platform)" >
{{ song.name }}
diff --git a/web/src/views/Playlist.vue b/web/src/views/Playlist.vue index b7d536c..aac78b2 100644 --- a/web/src/views/Playlist.vue +++ b/web/src/views/Playlist.vue @@ -31,8 +31,8 @@ :song="song" :index="i + 1" :active="store.currentSong?.id === song.id" - @play="store.play(song.name, song.platform)" - @add="store.addToQueue(song.name, song.platform)" + @play="store.playById(song.id, song.platform)" + @add="store.addToQueueById(song.id, song.platform)" /> @@ -76,13 +76,9 @@ const songs = ref([]); const loading = ref(true); async function playAll() { - if (songs.value.length > 0) { - const first = songs.value[0]; - await store.play(first.name, first.platform); - for (let i = 1; i < songs.value.length; i++) { - await store.addToQueue(songs.value[i].name, songs.value[i].platform); - } - } + const id = route.params.id as string; + const platform = (route.query.platform as string) || 'netease'; + await store.playPlaylist(id, platform); } onMounted(async () => { diff --git a/web/src/views/Search.vue b/web/src/views/Search.vue index a6b1a01..fb52963 100644 --- a/web/src/views/Search.vue +++ b/web/src/views/Search.vue @@ -27,8 +27,8 @@ :song="song" :index="i + 1" :active="store.currentSong?.id === song.id" - @play="store.play(song.name, song.platform)" - @add="store.addToQueue(song.name, song.platform)" + @play="store.playById(song.id, song.platform)" + @add="store.addToQueueById(song.id, song.platform)" />