Files
teamspeak-music-bot/web/src/stores/player.ts
T
saopig1andClaude Opus 4.6 94a0df8391 feat: add QQ Music API integration, Pinia home cache, and unified search
- Start @sansenjian/qq-music-api server via dynamic import in api-server.ts
- Rewrite QQ Music provider to use real API endpoints (getSearchByKey,
  getMusicPlay, getSongListDetail, getAlbumInfo, getLyric, etc.)
- Cache home page data (recommend playlists, daily songs, user playlists)
  in Pinia store with 5-minute TTL to avoid refetching on every mount
- Add unified /api/music/search/all endpoint that queries both Netease
  and QQ providers in parallel and returns merged results
- Remove platform toggle from Search.vue; search both platforms at once
- Add platform badge (网易云 / QQ) to SongCard component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 02:31:15 +08:00

233 lines
6.5 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;
}
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, // timestamp when current song started
pausedElapsed: 0, // elapsed time 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 playback time in seconds */
elapsed(): number {
if (!this.activeBot?.currentSong) return 0;
if (this.isPaused) return this.pausedElapsed;
if (!this.isPlaying || this.playStartedAt === 0) return 0;
return (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 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);
}
// Reset play timer when song changes
if (newSongId && newSongId !== prevSongId) {
this.playStartedAt = Date.now();
this.pausedElapsed = 0;
}
// Track pause/resume
if (status.playing && !status.paused && prev?.paused) {
// Resumed — adjust start time
this.playStartedAt = Date.now() - this.pausedElapsed * 1000;
}
if (status.paused && !prev?.paused) {
// Paused — save elapsed
this.pausedElapsed = this.playStartedAt > 0
? (Date.now() - this.playStartedAt) / 1000
: 0;
}
},
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
}
},
async play(query: string, platform = 'netease') {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/play`, { query, 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 pause() {
if (!this.activeBotId) return;
this.pausedElapsed = this.playStartedAt > 0
? (Date.now() - this.playStartedAt) / 1000
: 0;
await axios.post(`/api/player/${this.activeBotId}/pause`);
},
async resume() {
if (!this.activeBotId) return;
this.playStartedAt = Date.now() - this.pausedElapsed * 1000;
await axios.post(`/api/player/${this.activeBotId}/resume`);
},
async next() {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/next`);
this.playStartedAt = Date.now();
this.pausedElapsed = 0;
},
async prev() {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/prev`);
this.playStartedAt = Date.now();
this.pausedElapsed = 0;
},
async stop() {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/stop`);
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();
},
},
});