mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-10-08 14:05:02 -04:00
feat(ui): overhaul Configuration and Services tabs to use direct inline inputs and single save buttons, replacing individual modals
CI / build-and-test (push) Has been cancelled
CI / build-and-test (push) Has been cancelled
This commit is contained in:
4 files changed
+371
-321
No files matched your search
+110
-161
@@ -527,38 +527,25 @@
|
||||
|
||||
<div class="card">
|
||||
<h2>Spotify API Settings</h2>
|
||||
<div
|
||||
style="background: rgba(248, 81, 73, 0.15); border: 1px solid var(--error); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-primary);">
|
||||
<div style="background: rgba(248, 81, 73, 0.15); border: 1px solid var(--error); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-primary);">
|
||||
⚠️ For active playlists and link functionality to work, sp_dc session cookie must be set!
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">API Enabled</span>
|
||||
<span class="value" id="config-spotify-enabled">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_API_ENABLED', 'Spotify API Enabled', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-spotify-enabled" data-env-key="SPOTIFY_API_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Session Cookie (sp_dc) <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-spotify-cookie">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_API_SESSION_COOKIE', 'Spotify Session Cookie', 'password', 'Get from browser dev tools while logged into Spotify. Cookie typically lasts ~1 year.')">Update</button>
|
||||
</div>
|
||||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||||
<span class="label">Cookie Age</span>
|
||||
<span class="value" id="config-cookie-age">-</span>
|
||||
<input type="password" id="config-spotify-cookie" data-env-key="SPOTIFY_API_SESSION_COOKIE" class="config-input" placeholder="Enter sp_dc cookie">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Cache Duration</span>
|
||||
<span class="value" id="config-cache-duration">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_API_CACHE_DURATION_MINUTES', 'Cache Duration (minutes)', 'number')">Edit</button>
|
||||
<span class="label">Cache Duration (mins)</span>
|
||||
<input type="number" id="config-cache-duration" data-env-key="SPOTIFY_API_CACHE_DURATION_MINUTES" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">ISRC Matching</span>
|
||||
<span class="value" id="config-isrc-matching">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_API_PREFER_ISRC_MATCHING', 'Prefer ISRC Matching', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-isrc-matching" data-env-key="SPOTIFY_API_PREFER_ISRC_MATCHING" class="config-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -568,15 +555,16 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Quality</span>
|
||||
<span class="value" id="config-squid-quality">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SQUIDWTF_QUALITY', 'SquidWTF Quality', 'select', 'HI_RES_LOSSLESS: 24-bit/192kHz FLAC (highest)\\nLOSSLESS: 16-bit/44.1kHz FLAC (default)\\nHIGH: 320kbps AAC\\nLOW: 96kbps AAC', ['HI_RES_LOSSLESS', 'LOSSLESS', 'HIGH', 'LOW'])">Edit</button>
|
||||
<select id="config-squid-quality" data-env-key="SQUIDWTF_QUALITY" class="config-input">
|
||||
<option value="HI_RES_LOSSLESS">HI_RES_LOSSLESS (24-bit/192kHz FLAC)</option>
|
||||
<option value="LOSSLESS">LOSSLESS (16-bit/44.1kHz FLAC)</option>
|
||||
<option value="HIGH">HIGH (320kbps AAC)</option>
|
||||
<option value="LOW">LOW (96kbps AAC)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Request Interval</span>
|
||||
<span class="value" id="config-squid-ratelimit">200 ms</span>
|
||||
<button
|
||||
onclick="openEditSetting('SQUIDWTF_MIN_REQUEST_INTERVAL_MS', 'SquidWTF Request Interval', 'number', 'Minimum milliseconds between API requests (default: 200)')">Edit</button>
|
||||
<span class="label">Request Interval (ms)</span>
|
||||
<input type="number" id="config-squid-ratelimit" data-env-key="SQUIDWTF_MIN_REQUEST_INTERVAL_MS" class="config-input" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -586,21 +574,19 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">ARL Token</span>
|
||||
<span class="value" id="config-deezer-arl">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('DEEZER_ARL', 'Deezer ARL Token', 'password', 'Get from browser cookies while logged into Deezer')">Update</button>
|
||||
<input type="password" id="config-deezer-arl" data-env-key="DEEZER_ARL" class="config-input" placeholder="Enter Deezer ARL token">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Quality</span>
|
||||
<span class="value" id="config-deezer-quality">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('DEEZER_QUALITY', 'Deezer Quality', 'select', '', ['FLAC', 'MP3_320', 'MP3_128'])">Edit</button>
|
||||
<select id="config-deezer-quality" data-env-key="DEEZER_QUALITY" class="config-input">
|
||||
<option value="FLAC">FLAC</option>
|
||||
<option value="MP3_320">MP3_320</option>
|
||||
<option value="MP3_128">MP3_128</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Request Interval</span>
|
||||
<span class="value" id="config-deezer-ratelimit">200 ms</span>
|
||||
<button
|
||||
onclick="openEditSetting('DEEZER_MIN_REQUEST_INTERVAL_MS', 'Deezer Request Interval', 'number', 'Minimum milliseconds between API requests (default: 200)')">Edit</button>
|
||||
<span class="label">Request Interval (ms)</span>
|
||||
<input type="number" id="config-deezer-ratelimit" data-env-key="DEEZER_MIN_REQUEST_INTERVAL_MS" class="config-input" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -610,21 +596,21 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">User Auth Token</span>
|
||||
<span class="value" id="config-qobuz-token">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('QOBUZ_USER_AUTH_TOKEN', 'Qobuz User Auth Token', 'password', 'Get from browser while logged into Qobuz')">Update</button>
|
||||
<input type="password" id="config-qobuz-token" data-env-key="QOBUZ_USER_AUTH_TOKEN" class="config-input" placeholder="Enter Qobuz user auth token">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Preferred Quality</span>
|
||||
<span class="value" id="config-qobuz-quality">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('QOBUZ_QUALITY', 'Qobuz Quality', 'select', 'Default: FLAC', ['FLAC', 'FLAC_24_HIGH', 'FLAC_24_LOW', 'FLAC_16', 'MP3_320'])">Edit</button>
|
||||
<select id="config-qobuz-quality" data-env-key="QOBUZ_QUALITY" class="config-input">
|
||||
<option value="FLAC">FLAC</option>
|
||||
<option value="FLAC_24_HIGH">FLAC_24_HIGH</option>
|
||||
<option value="FLAC_24_LOW">FLAC_24_LOW</option>
|
||||
<option value="FLAC_16">FLAC_16</option>
|
||||
<option value="MP3_320">MP3_320</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Request Interval</span>
|
||||
<span class="value" id="config-qobuz-ratelimit">200 ms</span>
|
||||
<button
|
||||
onclick="openEditSetting('QOBUZ_MIN_REQUEST_INTERVAL_MS', 'Qobuz Request Interval', 'number', 'Minimum milliseconds between API requests (default: 200)')">Edit</button>
|
||||
<span class="label">Request Interval (ms)</span>
|
||||
<input type="number" id="config-qobuz-ratelimit" data-env-key="QOBUZ_MIN_REQUEST_INTERVAL_MS" class="config-input" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -634,15 +620,17 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">gamdl-aio Sidecar URL</span>
|
||||
<span class="value" id="config-applemusic-baseurl">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('APPLE_MUSIC_AIO_URL', 'gamdl-aio Sidecar URL', 'text', 'Base URL of the gamdl-aio sidecar container (default: http://gamdl-aio:8000)')">Edit</button>
|
||||
<input type="text" id="config-applemusic-baseurl" data-env-key="APPLE_MUSIC_AIO_URL" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Preferred Quality</span>
|
||||
<span class="value" id="config-applemusic-quality">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('APPLE_MUSIC_QUALITY', 'Apple Music Quality', 'select', 'Select preferred audio codec tier', ['alac-16-44', 'alac-24-96', 'alac-24-192', 'aac-320', 'aac-96'])">Edit</button>
|
||||
<select id="config-applemusic-quality" data-env-key="APPLE_MUSIC_QUALITY" class="config-input">
|
||||
<option value="alac-16-44">alac-16-44 (16-bit Lossless)</option>
|
||||
<option value="alac-24-96">alac-24-96 (24-bit Hi-Res Lossless)</option>
|
||||
<option value="alac-24-192">alac-24-192 (24-bit Hi-Res Lossless Max)</option>
|
||||
<option value="aac-320">aac-320 (High Quality AAC)</option>
|
||||
<option value="aac-96">aac-96 (Low Quality AAC)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -723,18 +711,15 @@
|
||||
<div class="config-section" style="margin-bottom: 24px;">
|
||||
<div class="config-item">
|
||||
<span class="label">Scrobbling Enabled</span>
|
||||
<span class="value" id="scrobbling-enabled-value">-</span>
|
||||
<button onclick="toggleScrobblingEnabled()">Toggle</button>
|
||||
<input type="checkbox" id="scrobbling-enabled-value" data-env-key="SCROBBLING_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Local Track Scrobbling</span>
|
||||
<span class="value" id="local-tracks-enabled-value">-</span>
|
||||
<button onclick="toggleLocalTracksEnabled()">Toggle</button>
|
||||
<input type="checkbox" id="local-tracks-enabled-value" data-env-key="SCROBBLING_LOCAL_TRACKS_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Synthetic Local Played Signal</span>
|
||||
<span class="value" id="synthetic-local-played-signal-enabled-value">-</span>
|
||||
<button onclick="toggleSyntheticLocalPlayedSignalEnabled()">Toggle</button>
|
||||
<input type="checkbox" id="synthetic-local-played-signal-enabled-value" data-env-key="SCROBBLING_SYNTHETIC_LOCAL_PLAYED_SIGNAL_ENABLED" class="config-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -756,18 +741,15 @@
|
||||
<div class="config-section" style="margin-bottom: 24px;">
|
||||
<div class="config-item">
|
||||
<span class="label">Last.fm Enabled</span>
|
||||
<span class="value" id="lastfm-enabled-value">-</span>
|
||||
<button onclick="toggleLastFmEnabled()">Toggle</button>
|
||||
<input type="checkbox" id="lastfm-enabled-value" data-env-key="SCROBBLING_LASTFM_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Username</span>
|
||||
<span class="value" id="lastfm-username-value">-</span>
|
||||
<button onclick="editLastFmUsername()">Edit</button>
|
||||
<input type="text" id="lastfm-username-value" data-env-key="SCROBBLING_LASTFM_USERNAME" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Password</span>
|
||||
<span class="value" id="lastfm-password-value">-</span>
|
||||
<button onclick="editLastFmPassword()">Edit</button>
|
||||
<input type="password" id="lastfm-password-value" data-env-key="SCROBBLING_LASTFM_PASSWORD" class="config-input" placeholder="Enter Last.fm password">
|
||||
</div>
|
||||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||||
<span class="label">Session Key</span>
|
||||
@@ -795,13 +777,11 @@
|
||||
<div class="config-section" style="margin-bottom: 24px;">
|
||||
<div class="config-item">
|
||||
<span class="label">ListenBrainz Enabled</span>
|
||||
<span class="value" id="listenbrainz-enabled-value">-</span>
|
||||
<button onclick="toggleListenBrainzEnabled()">Toggle</button>
|
||||
<input type="checkbox" id="listenbrainz-enabled-value" data-env-key="SCROBBLING_LISTENBRAINZ_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">User Token</span>
|
||||
<span class="value" id="listenbrainz-token-value">-</span>
|
||||
<button onclick="editListenBrainzToken()">Edit</button>
|
||||
<input type="password" id="listenbrainz-token-value" data-env-key="SCROBBLING_LISTENBRAINZ_USER_TOKEN" class="config-input" placeholder="Enter ListenBrainz user token">
|
||||
</div>
|
||||
<div class="config-item" style="grid-template-columns: 200px 1fr;">
|
||||
<span class="label">Status</span>
|
||||
@@ -820,24 +800,23 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Enabled</span>
|
||||
<span class="value" id="config-musicbrainz-enabled">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('MUSICBRAINZ_ENABLED', 'MusicBrainz Enabled', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-musicbrainz-enabled" data-env-key="MUSICBRAINZ_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Username</span>
|
||||
<span class="value" id="config-musicbrainz-username">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('MUSICBRAINZ_USERNAME', 'MusicBrainz Username', 'text', 'Your MusicBrainz username')">Update</button>
|
||||
<input type="text" id="config-musicbrainz-username" data-env-key="MUSICBRAINZ_USERNAME" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Password</span>
|
||||
<span class="value" id="config-musicbrainz-password">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('MUSICBRAINZ_PASSWORD', 'MusicBrainz Password', 'password', 'Your MusicBrainz password')">Update</button>
|
||||
<input type="password" id="config-musicbrainz-password" data-env-key="MUSICBRAINZ_PASSWORD" class="config-input" placeholder="Enter MusicBrainz password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar-sticky">
|
||||
<button class="primary" onclick="saveAllSettings('tab-services')">💾 Save Services Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Tab -->
|
||||
@@ -847,77 +826,75 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Backend Type <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-backend-type">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('BACKEND_TYPE', 'Backend Type', 'select', 'Choose your media server backend', ['Jellyfin', 'Subsonic'])">Edit</button>
|
||||
<select id="config-backend-type" data-env-key="BACKEND_TYPE" class="config-input">
|
||||
<option value="Jellyfin">Jellyfin</option>
|
||||
<option value="Subsonic">Subsonic</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Music Service <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-music-service">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('MUSIC_SERVICE', 'Music Service', 'select', 'Choose your music download provider', ['SquidWTF', 'Deezer', 'Qobuz', 'AppleMusic'])">Edit</button>
|
||||
<select id="config-music-service" data-env-key="MUSIC_SERVICE" class="config-input">
|
||||
<option value="SquidWTF">SquidWTF (Tidal)</option>
|
||||
<option value="Deezer">Deezer</option>
|
||||
<option value="Qobuz">Qobuz</option>
|
||||
<option value="AppleMusic">Apple Music</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Storage Mode</span>
|
||||
<span class="value" id="config-storage-mode">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('STORAGE_MODE', 'Storage Mode', 'select', 'Permanent keeps files forever, Cache auto-deletes after duration', ['Permanent', 'Cache'])">Edit</button>
|
||||
<select id="config-storage-mode" data-env-key="STORAGE_MODE" class="config-input">
|
||||
<option value="Permanent">Permanent</option>
|
||||
<option value="Cache">Cache</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item" id="cache-duration-row" style="display: none;">
|
||||
<div class="config-item" id="cache-duration-row">
|
||||
<span class="label">Cache Duration (hours)</span>
|
||||
<span class="value" id="config-cache-duration-hours">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('CACHE_DURATION_HOURS', 'Cache Duration (hours)', 'number', 'How long to keep cached files before deletion')">Edit</button>
|
||||
<input type="number" id="config-cache-duration-hours" data-env-key="CACHE_DURATION_HOURS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Download Mode</span>
|
||||
<span class="value" id="config-download-mode">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('DOWNLOAD_MODE', 'Download Mode', 'select', 'Download individual tracks or full albums', ['Track', 'Album'])">Edit</button>
|
||||
<select id="config-download-mode" data-env-key="DOWNLOAD_MODE" class="config-input">
|
||||
<option value="Track">Track</option>
|
||||
<option value="Album">Album</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Explicit Filter</span>
|
||||
<span class="value" id="config-explicit-filter">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('EXPLICIT_FILTER', 'Explicit Filter', 'select', 'Filter explicit content', ['All', 'ExplicitOnly', 'CleanOnly'])">Edit</button>
|
||||
<select id="config-explicit-filter" data-env-key="EXPLICIT_FILTER" class="config-input">
|
||||
<option value="All">All</option>
|
||||
<option value="ExplicitOnly">ExplicitOnly</option>
|
||||
<option value="CleanOnly">CleanOnly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Enable External Playlists</span>
|
||||
<span class="value" id="config-enable-external-playlists">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('ENABLE_EXTERNAL_PLAYLISTS', 'Enable External Playlists', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-enable-external-playlists" data-env-key="ENABLE_EXTERNAL_PLAYLISTS" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Playlists Directory</span>
|
||||
<span class="value" id="config-playlists-directory">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('PLAYLISTS_DIRECTORY', 'Playlists Directory', 'text', 'Directory path for external playlists')">Edit</button>
|
||||
<input type="text" id="config-playlists-directory" data-env-key="PLAYLISTS_DIRECTORY" class="config-input" placeholder="e.g. /app/playlists">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Redis Enabled</span>
|
||||
<span class="value" id="config-redis-enabled">-</span>
|
||||
<button onclick="openEditSetting('REDIS_ENABLED', 'Redis Enabled', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-redis-enabled" data-env-key="REDIS_ENABLED" class="config-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Admin Network Settings</h2>
|
||||
<div
|
||||
style="background: rgba(245, 158, 11, 0.12); border: 1px solid var(--warning); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-secondary); font-size: 0.9rem;">
|
||||
<div style="background: rgba(245, 158, 11, 0.12); border: 1px solid var(--warning); border-radius: 6px; padding: 12px; margin-bottom: 16px; color: var(--text-secondary); font-size: 0.9rem;">
|
||||
Keep admin UI on localhost by default. If you enable LAN bind, restrict access with trusted CIDR
|
||||
subnets (for example: <code>192.168.1.0/24,10.0.0.0/8</code>).
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Bind Admin UI On LAN</span>
|
||||
<span class="value" id="config-admin-bind-any-ip">-</span>
|
||||
<button onclick="openEditSetting('ADMIN_BIND_ANY_IP', 'Bind Admin UI On LAN', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-admin-bind-any-ip" data-env-key="ADMIN_BIND_ANY_IP" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Trusted Subnets (CIDR)</span>
|
||||
<span class="value" id="config-admin-trusted-subnets">-</span>
|
||||
<button onclick="openEditSetting('ADMIN_TRUSTED_SUBNETS', 'Trusted Subnets (CIDR)', 'text', 'Comma-separated CIDRs allowed on admin port 5275. Example: 192.168.1.0/24,10.0.0.0/8')">Edit</button>
|
||||
<input type="text" id="config-admin-trusted-subnets" data-env-key="ADMIN_TRUSTED_SUBNETS" class="config-input" placeholder="e.g. 192.168.1.0/24, 10.0.0.0/8">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -925,12 +902,11 @@
|
||||
<div class="card">
|
||||
<details class="advanced-section">
|
||||
<summary>Advanced: Debug Settings</summary>
|
||||
<div class="advanced-section-content">
|
||||
<div class="advanced-section-content" style="padding-top: 12px;">
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Log All Requests</span>
|
||||
<span class="value" id="config-debug-log-requests">-</span>
|
||||
<button onclick="openEditSetting('DEBUG_LOG_ALL_REQUESTS', 'Log All Requests', 'toggle', 'Enable detailed logging of every HTTP request (useful for debugging client issues)')">Edit</button>
|
||||
<input type="checkbox" id="config-debug-log-requests" data-env-key="DEBUG_LOG_ALL_REQUESTS" class="config-input">
|
||||
</div>
|
||||
<div class="guidance-banner info compact" style="margin-top: 12px;">
|
||||
ℹ️ When enabled, logs every incoming request with method, path, headers, and response status. Auth tokens are automatically masked.
|
||||
@@ -940,27 +916,20 @@
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- External services settings moved to Services tab -->
|
||||
|
||||
<div class="card">
|
||||
<h2>Jellyfin Settings</h2>
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">URL <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-jellyfin-url">-</span>
|
||||
<button onclick="openEditSetting('JELLYFIN_URL', 'Jellyfin URL', 'text')">Edit</button>
|
||||
<input type="text" id="config-jellyfin-url" data-env-key="JELLYFIN_URL" class="config-input" placeholder="http://192.168.1.100:8096">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">API Key <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-jellyfin-api-key">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('JELLYFIN_API_KEY', 'Jellyfin API Key', 'password')">Update</button>
|
||||
<input type="password" id="config-jellyfin-api-key" data-env-key="JELLYFIN_API_KEY" class="config-input" placeholder="Enter Jellyfin API key">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">User ID <span style="color: var(--error);">*</span></span>
|
||||
<span class="value" id="config-jellyfin-user-id">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('JELLYFIN_USER_ID', 'Jellyfin User ID', 'text', 'Required for playlist operations. Get from Jellyfin user profile URL: userId=...')">Edit</button>
|
||||
<input type="text" id="config-jellyfin-user-id" data-env-key="JELLYFIN_USER_ID" class="config-input" placeholder="Enter user profile ID">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -970,14 +939,11 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Download Path (Cache)</span>
|
||||
<span class="value" id="config-download-path">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('LIBRARY_DOWNLOAD_PATH', 'Download Path', 'text')">Edit</button>
|
||||
<input type="text" id="config-download-path" data-env-key="LIBRARY_DOWNLOAD_PATH" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Kept Path (Favorited)</span>
|
||||
<span class="value" id="config-kept-path">-</span>
|
||||
<button onclick="openEditSetting('LIBRARY_KEPT_PATH', 'Kept Path', 'text')">Edit</button>
|
||||
<input type="text" id="config-kept-path" data-env-key="LIBRARY_KEPT_PATH" class="config-input">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -987,15 +953,11 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Spotify Import Enabled</span>
|
||||
<span class="value" id="config-spotify-import-enabled">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_IMPORT_ENABLED', 'Spotify Import Enabled', 'toggle')">Edit</button>
|
||||
<input type="checkbox" id="config-spotify-import-enabled" data-env-key="SPOTIFY_IMPORT_ENABLED" class="config-input">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Matching Interval (hours)</span>
|
||||
<span class="value" id="config-matching-interval">-</span>
|
||||
<button
|
||||
onclick="openEditSetting('SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS', 'Matching Interval (hours)', 'number', 'How often to check for playlist updates')">Edit</button>
|
||||
<input type="number" id="config-matching-interval" data-env-key="SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS" class="config-input" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1009,60 +971,47 @@
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<span class="label">Search Results (minutes)</span>
|
||||
<span class="value" id="config-cache-search">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_SEARCH_RESULTS_MINUTES', 'Search Results Cache (minutes)', 'How long to cache search results')">Edit</button>
|
||||
<input type="number" id="config-cache-search" data-env-key="CACHE_SEARCH_RESULTS_MINUTES" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Playlist Images (hours)</span>
|
||||
<span class="value" id="config-cache-playlist-images">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_PLAYLIST_IMAGES_HOURS', 'Playlist Images Cache (hours)', 'How long to cache playlist cover images')">Edit</button>
|
||||
<input type="number" id="config-cache-playlist-images" data-env-key="CACHE_PLAYLIST_IMAGES_HOURS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Spotify Playlist Items (hours)</span>
|
||||
<span class="value" id="config-cache-spotify-items">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_SPOTIFY_PLAYLIST_ITEMS_HOURS', 'Spotify Playlist Items Cache (hours)', 'How long to cache Spotify playlist data')">Edit</button>
|
||||
<input type="number" id="config-cache-spotify-items" data-env-key="CACHE_SPOTIFY_PLAYLIST_ITEMS_HOURS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Spotify Matched Tracks (days)</span>
|
||||
<span class="value" id="config-cache-matched-tracks">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_SPOTIFY_MATCHED_TRACKS_DAYS', 'Matched Tracks Cache (days)', 'How long to cache Spotify ID to track mappings')">Edit</button>
|
||||
<input type="number" id="config-cache-matched-tracks" data-env-key="CACHE_SPOTIFY_MATCHED_TRACKS_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Lyrics (days)</span>
|
||||
<span class="value" id="config-cache-lyrics">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_LYRICS_DAYS', 'Lyrics Cache (days)', 'How long to cache fetched lyrics')">Edit</button>
|
||||
<input type="number" id="config-cache-lyrics" data-env-key="CACHE_LYRICS_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Genre Data (days)</span>
|
||||
<span class="value" id="config-cache-genres">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_GENRE_DAYS', 'Genre Cache (days)', 'How long to cache genre information')">Edit</button>
|
||||
<input type="number" id="config-cache-genres" data-env-key="CACHE_GENRE_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">External Metadata (days)</span>
|
||||
<span class="value" id="config-cache-metadata">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_METADATA_DAYS', 'Metadata Cache (days)', 'How long to cache SquidWTF/Deezer/Qobuz metadata')">Edit</button>
|
||||
<input type="number" id="config-cache-metadata" data-env-key="CACHE_METADATA_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Odesli Lookups (days)</span>
|
||||
<span class="value" id="config-cache-odesli">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_ODESLI_LOOKUP_DAYS', 'Odesli Lookup Cache (days)', 'How long to cache Odesli URL conversions')">Edit</button>
|
||||
<input type="number" id="config-cache-odesli" data-env-key="CACHE_ODESLI_LOOKUP_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Proxy Images (days)</span>
|
||||
<span class="value" id="config-cache-proxy-images">-</span>
|
||||
<button
|
||||
onclick="openEditCacheSetting('CACHE_PROXY_IMAGES_DAYS', 'Proxy Images Cache (days)', 'How long to cache proxied Jellyfin images')">Edit</button>
|
||||
<input type="number" id="config-cache-proxy-images" data-env-key="CACHE_PROXY_IMAGES_DAYS" class="config-input" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="save-bar-sticky">
|
||||
<button class="primary" onclick="saveAllSettings('tab-config')">💾 Save Configuration</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="config-backup-card">
|
||||
<h2>Configuration Backup</h2>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { showToast } from "./utils.js";
|
||||
import * as API from "./api.js";
|
||||
import { runAction } from "./operations.js";
|
||||
import { updateConfigUI } from "./ui.js";
|
||||
|
||||
let showRestartBanner = () => {};
|
||||
|
||||
@@ -63,50 +64,11 @@ async function loadScrobblingConfig() {
|
||||
API.fetchScrobblingStatus(),
|
||||
]);
|
||||
|
||||
document.getElementById("scrobbling-enabled-value").textContent = data
|
||||
.scrobbling.enabled
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
|
||||
document.getElementById("local-tracks-enabled-value").textContent = data
|
||||
.scrobbling.localTracksEnabled
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
|
||||
document.getElementById(
|
||||
"synthetic-local-played-signal-enabled-value",
|
||||
).textContent = data.scrobbling.syntheticLocalPlayedSignalEnabled
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
|
||||
document.getElementById("lastfm-enabled-value").textContent = data
|
||||
.scrobbling.lastFm.enabled
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
updateConfigUI(data);
|
||||
|
||||
const username = data.scrobbling.lastFm.username;
|
||||
document.getElementById("lastfm-username-value").textContent =
|
||||
username && username !== "(not set)" ? username : "Not Set";
|
||||
|
||||
const password = data.scrobbling.lastFm.password;
|
||||
document.getElementById("lastfm-password-value").textContent =
|
||||
password && password !== "(not set)" ? "••••••••" : "Not Set";
|
||||
|
||||
const sessionKey = data.scrobbling.lastFm.sessionKey;
|
||||
if (
|
||||
sessionKey &&
|
||||
sessionKey !== "(not set)" &&
|
||||
!sessionKey.startsWith("••••")
|
||||
) {
|
||||
document.getElementById("lastfm-session-key-value").textContent =
|
||||
sessionKey.substring(0, 32) + "...";
|
||||
} else if (sessionKey && sessionKey.startsWith("••••")) {
|
||||
document.getElementById("lastfm-session-key-value").textContent =
|
||||
sessionKey;
|
||||
} else {
|
||||
document.getElementById("lastfm-session-key-value").textContent =
|
||||
"Not Set";
|
||||
}
|
||||
|
||||
const hasApiKey =
|
||||
data.scrobbling.lastFm.apiKey &&
|
||||
@@ -153,17 +115,9 @@ async function loadScrobblingConfig() {
|
||||
}
|
||||
document.getElementById("lastfm-status-value").innerHTML = statusHtml;
|
||||
|
||||
document.getElementById("listenbrainz-enabled-value").textContent = data
|
||||
.scrobbling.listenBrainz.enabled
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
|
||||
const hasToken =
|
||||
data.scrobbling.listenBrainz.userToken &&
|
||||
data.scrobbling.listenBrainz.userToken !== "(not set)";
|
||||
document.getElementById("listenbrainz-token-value").textContent = hasToken
|
||||
? "••••••••"
|
||||
: "Not Set";
|
||||
|
||||
let lbStatus = "";
|
||||
if (data.scrobbling.listenBrainz.enabled && hasToken) {
|
||||
@@ -297,6 +251,15 @@ async function editListenBrainzToken() {
|
||||
}
|
||||
|
||||
async function authenticateLastFm() {
|
||||
try {
|
||||
if (window.saveAllSettings) {
|
||||
await window.saveAllSettings('tab-services', { silent: true });
|
||||
}
|
||||
} catch (err) {
|
||||
showToast("Failed to save credentials before authentication", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
await runScrobblingAction({
|
||||
before: async () => {
|
||||
showToast("Authenticating with Last.fm...", "info");
|
||||
@@ -321,10 +284,32 @@ async function testLastFmConnection() {
|
||||
}
|
||||
|
||||
async function validateListenBrainzToken() {
|
||||
const token = prompt(
|
||||
"Enter your ListenBrainz User Token:\n\nGet from https://listenbrainz.org/settings/",
|
||||
);
|
||||
const token = document.getElementById("listenbrainz-token-value")?.value?.trim() || "";
|
||||
if (!token) {
|
||||
showToast("Please enter a ListenBrainz token", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (token === "••••••••") {
|
||||
// Validate currently saved token
|
||||
await runScrobblingAction({
|
||||
before: async () => {
|
||||
showToast("Validating ListenBrainz token...", "info");
|
||||
},
|
||||
task: () => API.validateListenBrainzToken(),
|
||||
success: (data) =>
|
||||
`✓ Token validated! User: ${data.username}.`,
|
||||
error: (error) => "Validation failed: " + error.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.saveAllSettings) {
|
||||
await window.saveAllSettings('tab-services', { silent: true });
|
||||
}
|
||||
} catch (err) {
|
||||
showToast("Failed to save token before validation", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -332,7 +317,7 @@ async function validateListenBrainzToken() {
|
||||
before: async () => {
|
||||
showToast("Validating ListenBrainz token...", "info");
|
||||
},
|
||||
task: () => API.validateListenBrainzToken(token.trim()),
|
||||
task: () => API.validateListenBrainzToken(token),
|
||||
success: (data) =>
|
||||
`✓ Token validated! User: ${data.username}. Please restart the container.`,
|
||||
error: (error) => "Validation failed: " + error.message,
|
||||
|
||||
+172
-99
@@ -859,6 +859,20 @@ export function updateDownloadsUI(data) {
|
||||
.join("");
|
||||
}
|
||||
|
||||
function setInputValue(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setCheckboxChecked(id, checked) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.checked = Boolean(checked);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateConfigUI(data) {
|
||||
window.lastConfigData = data;
|
||||
|
||||
@@ -877,111 +891,170 @@ export function updateConfigUI(data) {
|
||||
if (typeof currentWizardStep !== 'undefined') {
|
||||
renderWizardStep(currentWizardStep);
|
||||
}
|
||||
document.getElementById("config-backend-type").textContent =
|
||||
data.backendType || "Jellyfin";
|
||||
document.getElementById("config-music-service").textContent =
|
||||
data.musicService || "SquidWTF";
|
||||
document.getElementById("config-storage-mode").textContent =
|
||||
data.library?.storageMode || "Cache";
|
||||
document.getElementById("config-cache-duration-hours").textContent =
|
||||
data.library?.cacheDurationHours || "24";
|
||||
document.getElementById("config-download-mode").textContent =
|
||||
data.library?.downloadMode || "Track";
|
||||
document.getElementById("config-explicit-filter").textContent =
|
||||
data.explicitFilter || "All";
|
||||
document.getElementById("config-enable-external-playlists").textContent =
|
||||
data.enableExternalPlaylists ? "Yes" : "No";
|
||||
document.getElementById("config-playlists-directory").textContent =
|
||||
data.playlistsDirectory || "(not set)";
|
||||
document.getElementById("config-redis-enabled").textContent =
|
||||
data.redisEnabled ? "Yes" : "No";
|
||||
document.getElementById("config-debug-log-requests").textContent = data.debug
|
||||
?.logAllRequests
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
document.getElementById("config-admin-bind-any-ip").textContent = data.admin
|
||||
?.bindAnyIp
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
document.getElementById("config-admin-trusted-subnets").textContent =
|
||||
data.admin?.trustedSubnets?.trim() || "(localhost only)";
|
||||
|
||||
document.getElementById("config-spotify-enabled").textContent = data
|
||||
.spotifyApi.enabled
|
||||
? "Yes"
|
||||
: "No";
|
||||
document.getElementById("config-spotify-cookie").textContent =
|
||||
data.spotifyApi.sessionCookie;
|
||||
document.getElementById("config-cache-duration").textContent =
|
||||
data.spotifyApi.cacheDurationMinutes + " minutes";
|
||||
document.getElementById("config-isrc-matching").textContent = data.spotifyApi
|
||||
.preferIsrcMatching
|
||||
? "Enabled"
|
||||
: "Disabled";
|
||||
|
||||
document.getElementById("config-deezer-arl").textContent =
|
||||
data.deezer.arl || "(not set)";
|
||||
document.getElementById("config-deezer-quality").textContent =
|
||||
data.deezer.quality;
|
||||
document.getElementById("config-deezer-ratelimit").textContent =
|
||||
(data.deezer.minRequestIntervalMs || 200) + " ms";
|
||||
document.getElementById("config-squid-quality").textContent =
|
||||
data.squidWtf.quality;
|
||||
document.getElementById("config-squid-ratelimit").textContent =
|
||||
(data.squidWtf.minRequestIntervalMs || 200) + " ms";
|
||||
document.getElementById("config-applemusic-baseurl").textContent =
|
||||
data.appleMusic.baseUrl || "http://gamdl-aio:8000";
|
||||
document.getElementById("config-applemusic-quality").textContent =
|
||||
data.appleMusic.quality || "alac-16-44";
|
||||
document.getElementById("config-musicbrainz-enabled").textContent = data
|
||||
.musicBrainz.enabled
|
||||
? "Yes"
|
||||
: "No";
|
||||
document.getElementById("config-qobuz-token").textContent =
|
||||
data.qobuz.userAuthToken || "(not set)";
|
||||
document.getElementById("config-qobuz-quality").textContent =
|
||||
data.qobuz.quality || "FLAC";
|
||||
document.getElementById("config-qobuz-ratelimit").textContent =
|
||||
(data.qobuz.minRequestIntervalMs || 200) + " ms";
|
||||
document.getElementById("config-jellyfin-url").textContent =
|
||||
data.jellyfin.url || "-";
|
||||
document.getElementById("config-jellyfin-api-key").textContent =
|
||||
data.jellyfin.apiKey;
|
||||
document.getElementById("config-jellyfin-user-id").textContent =
|
||||
data.jellyfin.userId || "(not set)";
|
||||
document.getElementById("config-jellyfin-library-id").textContent =
|
||||
data.jellyfin.libraryId || "-";
|
||||
document.getElementById("config-download-path").textContent =
|
||||
data.library?.downloadPath || "./downloads";
|
||||
document.getElementById("config-kept-path").textContent =
|
||||
data.library?.keptPath || "/app/kept";
|
||||
document.getElementById("config-spotify-import-enabled").textContent = data
|
||||
.spotifyImport?.enabled
|
||||
? "Yes"
|
||||
: "No";
|
||||
document.getElementById("config-matching-interval").textContent =
|
||||
(data.spotifyImport?.matchingIntervalHours || 24) + " hours";
|
||||
// Set form inputs in tab-config
|
||||
setInputValue("config-backend-type", data.backendType || "Jellyfin");
|
||||
setInputValue("config-music-service", data.musicService || "SquidWTF");
|
||||
setInputValue("config-storage-mode", data.library?.storageMode || "Cache");
|
||||
setInputValue("config-cache-duration-hours", data.library?.cacheDurationHours || 24);
|
||||
setInputValue("config-download-mode", data.library?.downloadMode || "Track");
|
||||
setInputValue("config-explicit-filter", data.explicitFilter || "All");
|
||||
setCheckboxChecked("config-enable-external-playlists", data.enableExternalPlaylists);
|
||||
setInputValue("config-playlists-directory", data.playlistsDirectory || "");
|
||||
setCheckboxChecked("config-redis-enabled", data.redisEnabled);
|
||||
|
||||
setCheckboxChecked("config-admin-bind-any-ip", data.admin?.bindAnyIp);
|
||||
setInputValue("config-admin-trusted-subnets", data.admin?.trustedSubnets || "");
|
||||
setCheckboxChecked("config-debug-log-requests", data.debug?.logAllRequests);
|
||||
|
||||
setInputValue("config-jellyfin-url", data.jellyfin?.url || "");
|
||||
setInputValue("config-jellyfin-api-key", data.jellyfin?.apiKey ? "••••••••" : "");
|
||||
setInputValue("config-jellyfin-user-id", data.jellyfin?.userId || "");
|
||||
|
||||
setInputValue("config-download-path", data.library?.downloadPath || "./downloads");
|
||||
setInputValue("config-kept-path", data.library?.keptPath || "/app/kept");
|
||||
|
||||
setCheckboxChecked("config-spotify-import-enabled", data.spotifyImport?.enabled);
|
||||
setInputValue("config-matching-interval", data.spotifyImport?.matchingIntervalHours || 24);
|
||||
|
||||
if (data.cache) {
|
||||
document.getElementById("config-cache-playlist-images").textContent =
|
||||
data.cache.playlistImagesHours || "168";
|
||||
document.getElementById("config-cache-spotify-items").textContent =
|
||||
data.cache.spotifyPlaylistItemsHours || "168";
|
||||
document.getElementById("config-cache-matched-tracks").textContent =
|
||||
data.cache.spotifyMatchedTracksDays || "30";
|
||||
document.getElementById("config-cache-lyrics").textContent =
|
||||
data.cache.lyricsDays || "14";
|
||||
document.getElementById("config-cache-genres").textContent =
|
||||
data.cache.genreDays || "30";
|
||||
document.getElementById("config-cache-metadata").textContent =
|
||||
data.cache.metadataDays || "7";
|
||||
document.getElementById("config-cache-odesli").textContent =
|
||||
data.cache.odesliLookupDays || "60";
|
||||
document.getElementById("config-cache-proxy-images").textContent =
|
||||
data.cache.proxyImagesDays || "14";
|
||||
setInputValue("config-cache-search", data.cache.searchResultsMinutes || "360");
|
||||
setInputValue("config-cache-playlist-images", data.cache.playlistImagesHours || "168");
|
||||
setInputValue("config-cache-spotify-items", data.cache.spotifyPlaylistItemsHours || "168");
|
||||
setInputValue("config-cache-matched-tracks", data.cache.spotifyMatchedTracksDays || "30");
|
||||
setInputValue("config-cache-lyrics", data.cache.lyricsDays || "14");
|
||||
setInputValue("config-cache-genres", data.cache.genreDays || "30");
|
||||
setInputValue("config-cache-metadata", data.cache.metadataDays || "30");
|
||||
setInputValue("config-cache-odesli", data.cache.odesliLookupDays || "30");
|
||||
setInputValue("config-cache-proxy-images", data.cache.proxyImagesDays || "30");
|
||||
}
|
||||
|
||||
// Set form inputs in tab-services
|
||||
setCheckboxChecked("config-spotify-enabled", data.spotifyApi?.enabled);
|
||||
setInputValue("config-spotify-cookie", data.spotifyApi?.sessionCookie ? "••••••••" : "");
|
||||
setInputValue("config-cache-duration", data.spotifyApi?.cacheDurationMinutes || 360);
|
||||
setCheckboxChecked("config-isrc-matching", data.spotifyApi?.preferIsrcMatching);
|
||||
|
||||
setInputValue("config-squid-quality", data.squidWtf?.quality || "LOSSLESS");
|
||||
setInputValue("config-squid-ratelimit", data.squidWtf?.minRequestIntervalMs || 200);
|
||||
|
||||
setInputValue("config-deezer-arl", data.deezer?.arl ? "••••••••" : "");
|
||||
setInputValue("config-deezer-quality", data.deezer?.quality || "FLAC");
|
||||
setInputValue("config-deezer-ratelimit", data.deezer?.minRequestIntervalMs || 200);
|
||||
|
||||
setInputValue("config-qobuz-token", data.qobuz?.userAuthToken ? "••••••••" : "");
|
||||
setInputValue("config-qobuz-quality", data.qobuz?.quality || "FLAC");
|
||||
setInputValue("config-qobuz-ratelimit", data.qobuz?.minRequestIntervalMs || 200);
|
||||
|
||||
setInputValue("config-applemusic-baseurl", data.appleMusic?.baseUrl || "http://gamdl-aio:8000");
|
||||
setInputValue("config-applemusic-quality", data.appleMusic?.quality || "alac-16-44");
|
||||
|
||||
// Scrobbling values
|
||||
if (data.scrobbling) {
|
||||
setCheckboxChecked("scrobbling-enabled-value", data.scrobbling.enabled);
|
||||
setCheckboxChecked("local-tracks-enabled-value", data.scrobbling.localTracksEnabled);
|
||||
setCheckboxChecked("synthetic-local-played-signal-enabled-value", data.scrobbling.syntheticLocalPlayedSignalEnabled);
|
||||
|
||||
setCheckboxChecked("lastfm-enabled-value", data.scrobbling.lastFm?.enabled);
|
||||
setInputValue("lastfm-username-value", data.scrobbling.lastFm?.username && data.scrobbling.lastFm.username !== "(not set)" ? data.scrobbling.lastFm.username : "");
|
||||
setInputValue("lastfm-password-value", data.scrobbling.lastFm?.password && data.scrobbling.lastFm.password !== "(not set)" ? "••••••••" : "");
|
||||
|
||||
const sessionKey = data.scrobbling.lastFm?.sessionKey;
|
||||
let sessionKeyDisplay = "Not Set";
|
||||
if (sessionKey && sessionKey !== "(not set)" && !sessionKey.startsWith("••••")) {
|
||||
sessionKeyDisplay = sessionKey.substring(0, 32) + "...";
|
||||
} else if (sessionKey && sessionKey.startsWith("••••")) {
|
||||
sessionKeyDisplay = sessionKey;
|
||||
}
|
||||
document.getElementById("lastfm-session-key-value").textContent = sessionKeyDisplay;
|
||||
|
||||
setCheckboxChecked("listenbrainz-enabled-value", data.scrobbling.listenBrainz?.enabled);
|
||||
setInputValue("listenbrainz-token-value", data.scrobbling.listenBrainz?.userToken && data.scrobbling.listenBrainz.userToken !== "(not set)" ? "••••••••" : "");
|
||||
}
|
||||
|
||||
if (data.musicBrainz) {
|
||||
setCheckboxChecked("config-musicbrainz-enabled", data.musicBrainz.enabled);
|
||||
setInputValue("config-musicbrainz-username", data.musicBrainz.username && data.musicBrainz.username !== "(not set)" ? data.musicBrainz.username : "");
|
||||
setInputValue("config-musicbrainz-password", data.musicBrainz.password && data.musicBrainz.password !== "(not set)" ? "••••••••" : "");
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAllSettings(tabId, options = {}) {
|
||||
const tab = document.getElementById(tabId);
|
||||
if (!tab) return;
|
||||
|
||||
const inputs = tab.querySelectorAll(".config-input");
|
||||
const updates = {};
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = input.getAttribute("data-env-key");
|
||||
if (!key) return;
|
||||
|
||||
let value = "";
|
||||
if (input.type === "checkbox") {
|
||||
value = input.checked ? "true" : "false";
|
||||
} else {
|
||||
value = input.value.trim();
|
||||
}
|
||||
|
||||
// Skip placeholder password masks
|
||||
if (value === "••••••••") {
|
||||
return;
|
||||
}
|
||||
|
||||
updates[key] = value;
|
||||
});
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
if (!options.silent) {
|
||||
showToast("No changes to save", "info");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/admin/config", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ updates })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text();
|
||||
throw new Error(errText || "Failed to update configuration");
|
||||
}
|
||||
|
||||
if (!options.silent) {
|
||||
showToast("Configuration saved successfully", "success");
|
||||
}
|
||||
|
||||
// Check if any updated key requires restart
|
||||
let needsRestart = false;
|
||||
for (const key in updates) {
|
||||
if (key !== "SPOTIFY_API_SESSION_COOKIE") {
|
||||
needsRestart = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRestart) {
|
||||
window.showRestartBanner?.();
|
||||
}
|
||||
|
||||
// Refresh config
|
||||
if (window.fetchConfig) {
|
||||
await window.fetchConfig();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Save error:", error);
|
||||
if (!options.silent) {
|
||||
showToast(error.message || "Failed to save configuration", "error");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
window.saveAllSettings = saveAllSettings;
|
||||
|
||||
export function updateJellyfinPlaylistsUI(data) {
|
||||
const tbody = document.getElementById("jellyfin-playlist-table-body");
|
||||
const playlists = data.playlists || [];
|
||||
|
||||
@@ -959,22 +959,65 @@ textarea {
|
||||
|
||||
.config-item {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr auto;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 14px 18px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 12px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.config-item:focus-within {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.config-item .label {
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.config-item .value {
|
||||
font-family: monospace;
|
||||
color: var(--text-secondary);
|
||||
/* Styled inputs for modern inline configuration editing */
|
||||
.config-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.config-input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.config-input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.save-bar-sticky {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: rgba(15, 21, 36, 0.95);
|
||||
backdrop-filter: blur(12px);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 32px -24px -24px -24px;
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.toast {
|
||||
|
||||
Reference in new issue
Block a user