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)"
>