diff --git a/web/src/components/SongCard.vue b/web/src/components/SongCard.vue
index 9dbd3de..e79889d 100644
--- a/web/src/components/SongCard.vue
+++ b/web/src/components/SongCard.vue
@@ -7,8 +7,8 @@
{{ song.name }}
{{ song.platform === 'bilibili' ? 'B站' : song.platform === 'qq' ? 'QQ' : song.platform === 'youtube' ? 'YouTube' : song.platform === 'local' ? '本地' : song.platform === 'kugou' ? '酷狗' : '网易云' }}
+ :class="song.platform === 'bilibili' ? 'badge-bilibili' : song.platform === 'qq' ? 'badge-qq' : song.platform === 'youtube' ? 'badge-youtube' : song.platform === 'local' ? 'badge-local' : song.platform === 'kugou' ? 'badge-kugou' : song.platform === 'spotify' ? 'badge-spotify' : 'badge-netease'"
+ >{{ song.platform === 'bilibili' ? 'B站' : song.platform === 'qq' ? 'QQ' : song.platform === 'youtube' ? 'YouTube' : song.platform === 'local' ? '本地' : song.platform === 'kugou' ? '酷狗' : song.platform === 'spotify' ? 'Spotify' : '网易云' }}
{{ song.artist }}
@@ -145,6 +145,11 @@ function formatDuration(seconds: number): string {
color: var(--brand-kugou);
}
+.badge-spotify {
+ background: var(--brand-spotify-12);
+ color: var(--brand-spotify);
+}
+
.song-artist {
font-size: 12px;
color: var(--text-secondary);
diff --git a/web/src/components/SourceTabs.vue b/web/src/components/SourceTabs.vue
index ef4b02d..0b33372 100644
--- a/web/src/components/SourceTabs.vue
+++ b/web/src/components/SourceTabs.vue
@@ -25,6 +25,7 @@ const LABELS: Record = {
netease: '网易云',
qq: 'QQ',
kugou: '酷狗',
+ spotify: 'Spotify',
};
defineProps<{
diff --git a/web/src/stores/player.ts b/web/src/stores/player.ts
index 6abc868..496eb7d 100644
--- a/web/src/stores/player.ts
+++ b/web/src/stores/player.ts
@@ -10,10 +10,10 @@ export interface Song {
album: string;
duration: number;
coverUrl: string;
- platform: 'netease' | 'qq' | 'bilibili' | 'youtube' | 'local' | 'kugou';
+ platform: 'netease' | 'qq' | 'bilibili' | 'youtube' | 'local' | 'kugou' | 'spotify';
}
-export type Source = 'netease' | 'qq' | 'kugou';
+export type Source = 'netease' | 'qq' | 'kugou' | 'spotify';
export interface BotStatus {
id: string;
@@ -100,11 +100,11 @@ export const usePlayerStore = defineStore('player', {
theme: 'dark' as 'dark' | 'light',
// Home page cache, split by source
- recommendPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[], kugou: [] as PlaylistItem[] },
- dailySongs: { netease: [] as Song[], qq: [] as Song[], kugou: [] as Song[] },
- userPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[], kugou: [] as PlaylistItem[] },
+ recommendPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[], kugou: [] as PlaylistItem[], spotify: [] as PlaylistItem[] },
+ dailySongs: { netease: [] as Song[], qq: [] as Song[], kugou: [] as Song[], spotify: [] as Song[] },
+ userPlaylists: { netease: [] as PlaylistItem[], qq: [] as PlaylistItem[], kugou: [] as PlaylistItem[], spotify: [] as PlaylistItem[] },
bilibiliPopular: [] as Song[],
- authStatus: { netease: false, qq: false, kugou: false },
+ authStatus: { netease: false, qq: false, kugou: false, spotify: false },
lastFetchTime: 0,
// Favorited playlists (fetched from server, isolated per WebUI user)
@@ -158,6 +158,7 @@ export const usePlayerStore = defineStore('player', {
if (this.authStatus.netease) s.push('netease');
if (this.authStatus.qq) s.push('qq');
if (this.authStatus.kugou) s.push('kugou');
+ if (this.authStatus.spotify) s.push('spotify');
return s;
},
},
@@ -572,23 +573,27 @@ export const usePlayerStore = defineStore('player', {
// Always check auth status first — if it changed since the cached
// fetch (e.g., user logged in/out as a different account), the
// cached playlists belong to a different user and we MUST refetch.
- const [neAuthRes, qqAuthRes, kugouAuthRes] = await Promise.allSettled([
+ const [neAuthRes, qqAuthRes, kugouAuthRes, spAuthRes] = await Promise.allSettled([
axios.get('/api/auth/status', { params: { platform: 'netease' } }),
axios.get('/api/auth/status', { params: { platform: 'qq' } }),
axios.get('/api/auth/status', { params: { platform: 'kugou' } }),
+ axios.get('/api/auth/status', { params: { platform: 'spotify' } }),
]);
const newAuth = {
netease: neAuthRes.status === 'fulfilled' && !!neAuthRes.value.data?.loggedIn,
qq: qqAuthRes.status === 'fulfilled' && !!qqAuthRes.value.data?.loggedIn,
kugou: kugouAuthRes.status === 'fulfilled' && !!kugouAuthRes.value.data?.loggedIn,
+ spotify: spAuthRes.status === 'fulfilled' && !!spAuthRes.value.data?.loggedIn,
};
const authChanged =
newAuth.netease !== this.authStatus.netease ||
newAuth.qq !== this.authStatus.qq ||
- newAuth.kugou !== this.authStatus.kugou;
+ newAuth.kugou !== this.authStatus.kugou ||
+ newAuth.spotify !== this.authStatus.spotify;
this.authStatus.netease = newAuth.netease;
this.authStatus.qq = newAuth.qq;
this.authStatus.kugou = newAuth.kugou;
+ this.authStatus.spotify = newAuth.spotify;
// Favorites are user-local and cheap; always refresh them, even on a
// home-data cache hit, so hearts stay correct across tabs/sessions.
diff --git a/web/src/stores/sourceTabs.ts b/web/src/stores/sourceTabs.ts
index f3c7472..adf19d7 100644
--- a/web/src/stores/sourceTabs.ts
+++ b/web/src/stores/sourceTabs.ts
@@ -28,7 +28,7 @@ function readAll(): Partial> {
export function loadTabSource(key: TabKey, fallback: Source = 'netease'): Source {
const all = readAll();
const v = all[key];
- return v === 'netease' || v === 'qq' || v === 'kugou' ? v : fallback;
+ return v === 'netease' || v === 'qq' || v === 'kugou' || v === 'spotify' ? v : fallback;
}
export function saveTabSource(key: TabKey, value: Source): void {
diff --git a/web/src/styles/variables.scss b/web/src/styles/variables.scss
index 1e2d32a..031868a 100644
--- a/web/src/styles/variables.scss
+++ b/web/src/styles/variables.scss
@@ -82,6 +82,8 @@
--brand-youtube-12: rgba(255, 0, 0, 0.12);
--brand-kugou: #2ca2f9;
--brand-kugou-12: rgba(44, 162, 249, 0.12);
+ --brand-spotify: #1DB954;
+ --brand-spotify-12: rgba(29, 185, 84, 0.12);
}
// Dark theme (default)