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:
saopig1andClaude Fable 5 committed 2026-07-14 00:54:34 +08:00
1 parent 3841fa80d3
commit 750ad9b1cc
12 files changed
+270 -72

No files matched your search

+39 -5
View File
@@ -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 */ }
}