mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat: make Jellyfin an optional source instead of the default
Revert the jellyfin-only default introduced by PR #123 so upgrading users keep their online sources; Jellyfin becomes opt-in: - default enabledProviders is now the online set (netease/qq/bilibili/ youtube/kugou); defaultPlatform() uses a fixed priority order (netease -> qq -> kugou -> jellyfin -> bilibili -> youtube) instead of jellyfin-first, so chat/REST/WebUI default to netease again - Settings: Jellyfin card is always visible with a new enable toggle (its enabled bit is enabledProviders membership); guards against clobbering other providers before the list loads - Setup wizard: saving the Jellyfin step auto-enables the source when a server URL was entered - Search/player store fallbacks flip from jellyfin to netease; !help no longer hardcodes Jellyfin lines - tests: update default-platform assertions, add coverage for the new default set, legacy configs without enabledProviders, priority order, and explicit jellyfin-only configs - README: reframe Jellyfin as optional (badges, command table, quality tiers, dedicated section, changelog), document the enabledProviders default and the v1.10.0 jellyfin-only window fix, credit @ItsEricRao Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3841fa80d3
commit
750ad9b1cc
12 files changed
+270
-72
No files matched your search
@@ -113,14 +113,14 @@ export const usePlayerStore = defineStore('player', {
|
||||
// Which sources the server has enabled (GET /api/music/providers) and the
|
||||
// configured default. Empty until fetched — sections stay hidden briefly.
|
||||
enabledProviders: [] as string[],
|
||||
defaultSource: 'jellyfin' as string,
|
||||
defaultSource: 'netease' as string,
|
||||
|
||||
// Home page cache, split by source
|
||||
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: { jellyfin: [] as PlaylistItem[], netease: [] as PlaylistItem[], qq: [] as PlaylistItem[], kugou: [] as PlaylistItem[], spotify: [] as PlaylistItem[] },
|
||||
bilibiliPopular: [] as Song[],
|
||||
// Jellyfin home sections (primary source)
|
||||
// Jellyfin home sections (shown only when the optional source is enabled)
|
||||
jellyfinLatestAlbums: [] as AlbumItem[],
|
||||
jellyfinMostPlayed: [] as Song[],
|
||||
jellyfinFavorites: [] as Song[],
|
||||
@@ -173,7 +173,7 @@ export const usePlayerStore = defineStore('player', {
|
||||
const maxDuration = this.activeBot.currentSong.duration || Infinity;
|
||||
return interpolateElapsed(timing, this.isPaused, maxDuration);
|
||||
},
|
||||
/** Sources that are currently logged in. Jellyfin (the primary source) leads. */
|
||||
/** Sources that are currently logged in. Jellyfin (when enabled) leads. */
|
||||
availableSources(): Source[] {
|
||||
const s: Source[] = [];
|
||||
if (this.authStatus.jellyfin) s.push('jellyfin');
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<template v-else-if="allSongs.length || allAlbums.length || allPlaylists.length">
|
||||
<!-- Only enabled sources are offered (enabledProviders gate); Jellyfin
|
||||
— the primary source — comes first. -->
|
||||
(opt-in) comes first when enabled. -->
|
||||
<div class="source-bar">
|
||||
<button
|
||||
v-if="sourceEnabled('jellyfin')"
|
||||
@@ -218,7 +218,7 @@ function loadSource(): SearchSource {
|
||||
const stored = localStorage.getItem(SOURCE_STORAGE_KEY);
|
||||
if (SEARCH_SOURCES.includes(stored as SearchSource)) return stored as SearchSource;
|
||||
} catch { /* localStorage blocked */ }
|
||||
return 'jellyfin';
|
||||
return 'netease';
|
||||
}
|
||||
|
||||
type TabType = 'songs' | 'albums' | 'playlists';
|
||||
@@ -272,7 +272,7 @@ function fixupSelectedSource() {
|
||||
const def = store.defaultSource as SearchSource;
|
||||
selectedSource.value = SEARCH_SOURCES.includes(def) && sourceEnabled(def)
|
||||
? def
|
||||
: SEARCH_SOURCES.find((s) => s !== 'local' && sourceEnabled(s)) ?? 'jellyfin';
|
||||
: SEARCH_SOURCES.find((s) => s !== 'local' && sourceEnabled(s)) ?? 'netease';
|
||||
}
|
||||
|
||||
// ---- 分页 / 加载更多 ----
|
||||
|
||||
@@ -165,10 +165,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Jellyfin — the primary music source. Admin-configured connection
|
||||
(server URL + credentials), no QR flow. -->
|
||||
<section v-if="can('platform.auth') && providerOn('jellyfin')" class="settings-section">
|
||||
<h2 class="section-title">Jellyfin 音乐库</h2>
|
||||
<!-- Jellyfin — optional self-hosted music source. Admin-configured
|
||||
connection (server URL + credentials), no QR flow. The card stays
|
||||
visible even while the source is disabled: the enable toggle below is
|
||||
the only UI that flips its enabledProviders membership, so hiding the
|
||||
card would leave no way to turn Jellyfin on. -->
|
||||
<section v-if="can('platform.auth')" class="settings-section">
|
||||
<h2 class="section-title">Jellyfin 音乐库(可选)</h2>
|
||||
|
||||
<div class="account-card">
|
||||
<div class="account-header">
|
||||
@@ -181,6 +184,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enable (opt-in source: presence in enabledProviders) -->
|
||||
<label class="profile-toggle behavior-toggle spotify-toggle">
|
||||
<div class="profile-toggle-text">
|
||||
<div class="profile-toggle-label">启用 Jellyfin 音源</div>
|
||||
<div class="profile-toggle-hint">开启后 Jellyfin 出现在搜索、首页与聊天命令(!play -j)的音源列表中。默认关闭,点击下方「保存」后生效。</div>
|
||||
</div>
|
||||
<input v-model="jellyfinEnabledForm" type="checkbox" class="profile-toggle-switch" />
|
||||
</label>
|
||||
|
||||
<div class="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input v-model="jellyfinForm.serverUrl" class="input" autocomplete="off" placeholder="https://jellyfin.example.com" />
|
||||
@@ -1164,6 +1176,12 @@ const jellyfinSaving = ref(false);
|
||||
const jellyfinTesting = ref(false);
|
||||
const jellyfinMessage = ref('');
|
||||
const jellyfinMessageTone = ref<'ok' | 'warn'>('ok');
|
||||
// Jellyfin is opt-in: its "enabled" bit is membership in enabledProviders
|
||||
// (unlike Spotify's dedicated spotify.enabled flag). Only trust the toggle
|
||||
// once the real list has loaded, so an early save can't clobber the other
|
||||
// providers with the empty placeholder list.
|
||||
const jellyfinEnabledForm = ref(false);
|
||||
const enabledProvidersLoaded = ref(false);
|
||||
|
||||
// Populate from the masked GET /api/bot/settings response — secrets never
|
||||
// round-trip, only hasPassword/hasApiKey.
|
||||
@@ -1183,8 +1201,22 @@ async function saveJellyfin() {
|
||||
jellyfinSaving.value = true;
|
||||
jellyfinMessage.value = '';
|
||||
try {
|
||||
const res = await axios.post('/api/bot/settings', { jellyfin: { ...jellyfinForm } });
|
||||
// enabledProviders is a full-replace field, so only send it when the
|
||||
// current list is known — otherwise we'd wipe the other sources.
|
||||
const payload: Record<string, unknown> = { jellyfin: { ...jellyfinForm } };
|
||||
if (enabledProvidersLoaded.value) {
|
||||
const others = enabledProviders.value.filter((p) => p !== 'jellyfin');
|
||||
payload.enabledProviders = jellyfinEnabledForm.value ? [...others, 'jellyfin'] : others;
|
||||
}
|
||||
const res = await axios.post('/api/bot/settings', payload);
|
||||
applyJellyfinConfig(res.data?.jellyfin);
|
||||
if (Array.isArray(res.data?.enabledProviders)) {
|
||||
enabledProviders.value = res.data.enabledProviders;
|
||||
jellyfinEnabledForm.value = res.data.enabledProviders.includes('jellyfin');
|
||||
enabledProvidersLoaded.value = true;
|
||||
// Search bar / home sections / FM cards react without a reload.
|
||||
store.fetchProviders();
|
||||
}
|
||||
jellyfinMessageTone.value = 'ok';
|
||||
jellyfinMessage.value = '已保存';
|
||||
await checkAuthStatus();
|
||||
@@ -1471,6 +1503,8 @@ async function loadIdleTimeout() {
|
||||
applyJellyfinConfig(res.data.jellyfin);
|
||||
if (Array.isArray(res.data.enabledProviders)) {
|
||||
enabledProviders.value = res.data.enabledProviders;
|
||||
jellyfinEnabledForm.value = res.data.enabledProviders.includes('jellyfin');
|
||||
enabledProvidersLoaded.value = true;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
+16
-4
@@ -61,7 +61,7 @@
|
||||
|
||||
<div v-if="currentStep === 2" class="step-content">
|
||||
<h2>连接 Jellyfin (可选)</h2>
|
||||
<p class="subtitle">连接自建 Jellyfin 服务器作为主音源;也可稍后在「设置」中配置</p>
|
||||
<p class="subtitle">连接自建 Jellyfin 服务器作为额外音源;保存后自动启用,也可稍后在「设置」中配置</p>
|
||||
<div class="form-group">
|
||||
<label>服务器地址</label>
|
||||
<input v-model="jellyfin.serverUrl" placeholder="https://jellyfin.example.com" class="input" />
|
||||
@@ -130,8 +130,8 @@ const nickname = ref('MusicBot');
|
||||
const defaultChannel = ref('');
|
||||
const channelId = ref('');
|
||||
|
||||
// Jellyfin — the primary music source. Optional: skipping leaves it
|
||||
// configurable later in Settings.
|
||||
// Jellyfin — optional self-hosted music source. Skipping leaves it
|
||||
// configurable later in Settings; saving here also enables it.
|
||||
const jellyfin = reactive({
|
||||
serverUrl: '',
|
||||
authMode: 'userpass' as 'userpass' | 'apikey',
|
||||
@@ -182,7 +182,19 @@ async function testJellyfin() {
|
||||
async function saveJellyfinAndNext() {
|
||||
jellyfinSaving.value = true;
|
||||
try {
|
||||
await axios.post('/api/bot/settings', { jellyfin: { ...jellyfin } });
|
||||
// Jellyfin is opt-in (not in the default enabledProviders), so completing
|
||||
// this step also enables the source — a configured-but-dark Jellyfin would
|
||||
// be baffling. enabledProviders is full-replace: fetch the current list
|
||||
// and append. Only do this when a server URL was actually entered.
|
||||
const payload: Record<string, unknown> = { jellyfin: { ...jellyfin } };
|
||||
if (jellyfin.serverUrl.trim()) {
|
||||
const cur = await axios.get('/api/bot/settings');
|
||||
const ep: unknown = cur.data?.enabledProviders;
|
||||
if (Array.isArray(ep) && !ep.includes('jellyfin')) {
|
||||
payload.enabledProviders = [...ep, 'jellyfin'];
|
||||
}
|
||||
}
|
||||
await axios.post('/api/bot/settings', payload);
|
||||
currentStep.value = 3;
|
||||
} catch {
|
||||
jellyfinTestOk.value = false;
|
||||
|
||||
Reference in new issue
Block a user