mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
- AudioPlayer.seek() restarts FFmpeg with -ss parameter for actual seeking - New POST /api/player/:botId/seek endpoint - BotStatus includes seekOffset and playStartTime from server - Frontend elapsed = seekOffset + (now - playStartedAt) - Progress bar click now seeks to clicked position - All timing actions use _resetTiming() helper with seekOffset - Lyrics sync uses store.elapsed which includes seekOffset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
266 lines
7.6 KiB
TypeScript
266 lines
7.6 KiB
TypeScript
import { defineStore } from 'pinia';
|
|
import axios from 'axios';
|
|
|
|
export interface Song {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number;
|
|
coverUrl: string;
|
|
platform: 'netease' | 'qq';
|
|
}
|
|
|
|
export interface BotStatus {
|
|
id: string;
|
|
name: string;
|
|
connected: boolean;
|
|
playing: boolean;
|
|
paused: boolean;
|
|
currentSong: Song | null;
|
|
queueSize: number;
|
|
volume: number;
|
|
playMode: string;
|
|
seekOffset?: number;
|
|
playStartTime?: number;
|
|
}
|
|
|
|
export interface PlaylistItem {
|
|
id: string;
|
|
name: string;
|
|
coverUrl: string;
|
|
songCount: number;
|
|
platform: string;
|
|
}
|
|
|
|
const HOME_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
|
export const usePlayerStore = defineStore('player', {
|
|
state: () => ({
|
|
bots: [] as BotStatus[],
|
|
activeBotId: null as string | null,
|
|
queue: [] as Song[],
|
|
theme: 'dark' as 'dark' | 'light',
|
|
playStartedAt: 0, // client timestamp when playback started
|
|
seekOffset: 0, // server seek offset in seconds
|
|
pausedElapsed: 0, // elapsed seconds when paused
|
|
// Home page cached data
|
|
recommendPlaylists: [] as PlaylistItem[],
|
|
dailySongs: [] as Song[],
|
|
userPlaylists: [] as PlaylistItem[],
|
|
lastFetchTime: 0,
|
|
}),
|
|
|
|
getters: {
|
|
activeBot(): BotStatus | null {
|
|
return this.bots.find((b) => b.id === this.activeBotId) ?? this.bots[0] ?? null;
|
|
},
|
|
currentSong(): Song | null {
|
|
return this.activeBot?.currentSong ?? null;
|
|
},
|
|
isPlaying(): boolean {
|
|
return this.activeBot?.playing ?? false;
|
|
},
|
|
isPaused(): boolean {
|
|
return this.activeBot?.paused ?? false;
|
|
},
|
|
elapsed(): number {
|
|
if (!this.activeBot?.currentSong) return 0;
|
|
if (this.isPaused) return this.pausedElapsed;
|
|
if (!this.isPlaying || this.playStartedAt === 0) return 0;
|
|
return this.seekOffset + (Date.now() - this.playStartedAt) / 1000;
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
setActiveBotId(id: string) {
|
|
this.activeBotId = id;
|
|
},
|
|
|
|
updateBotStatus(botId: string, status: BotStatus) {
|
|
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;
|
|
|
|
const index = this.bots.findIndex((b) => b.id === botId);
|
|
if (index >= 0) {
|
|
this.bots[index] = status;
|
|
} else {
|
|
this.bots.push(status);
|
|
}
|
|
|
|
if (botId !== (this.activeBotId ?? this.bots[0]?.id)) return;
|
|
|
|
// Use server-provided seekOffset
|
|
const serverSeek = status.seekOffset ?? 0;
|
|
|
|
// Song changed or seek offset changed — reset timer
|
|
if ((newSongId && newSongId !== prevSongId) || serverSeek !== this.seekOffset) {
|
|
this.seekOffset = serverSeek;
|
|
this.playStartedAt = Date.now();
|
|
this.pausedElapsed = 0;
|
|
return;
|
|
}
|
|
|
|
// Resumed
|
|
if (status.playing && !status.paused && prevPaused) {
|
|
this.playStartedAt = Date.now() - this.pausedElapsed * 1000;
|
|
return;
|
|
}
|
|
|
|
// Paused
|
|
if (status.paused && prevPlaying && !prevPaused) {
|
|
this.pausedElapsed = this.playStartedAt > 0
|
|
? this.seekOffset + (Date.now() - this.playStartedAt) / 1000
|
|
: this.seekOffset;
|
|
}
|
|
},
|
|
|
|
removeBotStatus(botId: string) {
|
|
this.bots = this.bots.filter((b) => b.id !== botId);
|
|
},
|
|
|
|
setQueue(queue: Song[]) {
|
|
this.queue = queue;
|
|
},
|
|
|
|
toggleTheme() {
|
|
this.theme = this.theme === 'dark' ? 'light' : 'dark';
|
|
localStorage.setItem('theme', this.theme);
|
|
},
|
|
|
|
loadTheme() {
|
|
const saved = localStorage.getItem('theme') as 'dark' | 'light' | null;
|
|
if (saved) this.theme = saved;
|
|
},
|
|
|
|
async fetchBots() {
|
|
const res = await axios.get('/api/bot');
|
|
this.bots = res.data.bots;
|
|
if (!this.activeBotId && this.bots.length > 0) {
|
|
this.activeBotId = this.bots[0].id;
|
|
}
|
|
// If a song is playing, set the start time
|
|
const bot = this.activeBot;
|
|
if (bot?.playing && bot.currentSong && this.playStartedAt === 0) {
|
|
this.playStartedAt = Date.now();
|
|
}
|
|
},
|
|
|
|
async fetchQueue() {
|
|
if (!this.activeBotId) return;
|
|
try {
|
|
const res = await axios.get(`/api/player/${this.activeBotId}/queue`);
|
|
this.queue = res.data.queue ?? [];
|
|
} catch {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
_resetTiming(seekSec = 0) {
|
|
this.seekOffset = seekSec;
|
|
this.playStartedAt = Date.now();
|
|
this.pausedElapsed = 0;
|
|
},
|
|
|
|
async play(query: string, platform = 'netease') {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/play`, { query, platform });
|
|
this._resetTiming();
|
|
},
|
|
|
|
async playById(songId: string, platform = 'netease') {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/play-by-id`, { songId, platform });
|
|
this._resetTiming();
|
|
},
|
|
|
|
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._resetTiming();
|
|
},
|
|
|
|
async pause() {
|
|
if (!this.activeBotId) return;
|
|
// Save current elapsed including seekOffset
|
|
this.pausedElapsed = this.elapsed;
|
|
await axios.post(`/api/player/${this.activeBotId}/pause`);
|
|
},
|
|
|
|
async resume() {
|
|
if (!this.activeBotId) return;
|
|
// Restore: playStartedAt = now - (pausedElapsed - seekOffset)
|
|
this.playStartedAt = Date.now() - (this.pausedElapsed - this.seekOffset) * 1000;
|
|
await axios.post(`/api/player/${this.activeBotId}/resume`);
|
|
},
|
|
|
|
async next() {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/next`);
|
|
this._resetTiming();
|
|
},
|
|
|
|
async prev() {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/prev`);
|
|
this._resetTiming();
|
|
},
|
|
|
|
async stop() {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/stop`);
|
|
this.seekOffset = 0;
|
|
this.playStartedAt = 0;
|
|
this.pausedElapsed = 0;
|
|
},
|
|
|
|
async setVolume(volume: number) {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/volume`, { volume });
|
|
},
|
|
|
|
async setMode(mode: string) {
|
|
if (!this.activeBotId) return;
|
|
await axios.post(`/api/player/${this.activeBotId}/mode`, { mode });
|
|
},
|
|
|
|
async fetchHomeData() {
|
|
// Skip if data was fetched within the last 5 minutes
|
|
if (this.lastFetchTime > 0 && Date.now() - this.lastFetchTime < HOME_CACHE_TTL) {
|
|
return;
|
|
}
|
|
|
|
const [playlistRes, dailyRes, userRes] = await Promise.allSettled([
|
|
axios.get('/api/music/recommend/playlists'),
|
|
axios.get('/api/music/recommend/songs'),
|
|
axios.get('/api/music/user/playlists'),
|
|
]);
|
|
|
|
if (playlistRes.status === 'fulfilled') {
|
|
this.recommendPlaylists = playlistRes.value.data.playlists;
|
|
}
|
|
if (dailyRes.status === 'fulfilled') {
|
|
this.dailySongs = dailyRes.value.data.songs;
|
|
}
|
|
if (userRes.status === 'fulfilled') {
|
|
this.userPlaylists = userRes.value.data.playlists;
|
|
}
|
|
|
|
this.lastFetchTime = Date.now();
|
|
},
|
|
},
|
|
});
|