Expand admin UI with full config editing and sp_dc cookie age tracking

- Fix auth status detection to use token validity instead of /me endpoint
- Add SessionCookieSetDate to SpotifyApiSettings for tracking cookie age
- Auto-set cookie date when updating sp_dc via admin UI
- Add edit buttons for all config settings (Spotify, Deezer, Qobuz, SquidWTF, Jellyfin)
- Show cookie age with color-coded expiration warnings (green/yellow/red)
- Display cookie age on both Dashboard and Config tabs
- Add generic edit setting modal supporting text/password/number/toggle/select inputs
- Remove SquidWTF base URL (not configurable)
- Add restart container button with manual restart instructions
This commit is contained in:
2026-02-03 14:52:40 -05:00
parent 71c4241a8a
commit c9895f6d1a
5 changed files with 306 additions and 41 deletions

View File

@@ -24,6 +24,9 @@ public class AdminController : ControllerBase
private readonly SpotifyApiSettings _spotifyApiSettings; private readonly SpotifyApiSettings _spotifyApiSettings;
private readonly SpotifyImportSettings _spotifyImportSettings; private readonly SpotifyImportSettings _spotifyImportSettings;
private readonly JellyfinSettings _jellyfinSettings; private readonly JellyfinSettings _jellyfinSettings;
private readonly DeezerSettings _deezerSettings;
private readonly QobuzSettings _qobuzSettings;
private readonly SquidWTFSettings _squidWtfSettings;
private readonly SpotifyApiClient _spotifyClient; private readonly SpotifyApiClient _spotifyClient;
private readonly SpotifyPlaylistFetcher _playlistFetcher; private readonly SpotifyPlaylistFetcher _playlistFetcher;
private readonly RedisCacheService _cache; private readonly RedisCacheService _cache;
@@ -36,6 +39,9 @@ public class AdminController : ControllerBase
IOptions<SpotifyApiSettings> spotifyApiSettings, IOptions<SpotifyApiSettings> spotifyApiSettings,
IOptions<SpotifyImportSettings> spotifyImportSettings, IOptions<SpotifyImportSettings> spotifyImportSettings,
IOptions<JellyfinSettings> jellyfinSettings, IOptions<JellyfinSettings> jellyfinSettings,
IOptions<DeezerSettings> deezerSettings,
IOptions<QobuzSettings> qobuzSettings,
IOptions<SquidWTFSettings> squidWtfSettings,
SpotifyApiClient spotifyClient, SpotifyApiClient spotifyClient,
SpotifyPlaylistFetcher playlistFetcher, SpotifyPlaylistFetcher playlistFetcher,
RedisCacheService cache) RedisCacheService cache)
@@ -45,6 +51,9 @@ public class AdminController : ControllerBase
_spotifyApiSettings = spotifyApiSettings.Value; _spotifyApiSettings = spotifyApiSettings.Value;
_spotifyImportSettings = spotifyImportSettings.Value; _spotifyImportSettings = spotifyImportSettings.Value;
_jellyfinSettings = jellyfinSettings.Value; _jellyfinSettings = jellyfinSettings.Value;
_deezerSettings = deezerSettings.Value;
_qobuzSettings = qobuzSettings.Value;
_squidWtfSettings = squidWtfSettings.Value;
_spotifyClient = spotifyClient; _spotifyClient = spotifyClient;
_playlistFetcher = playlistFetcher; _playlistFetcher = playlistFetcher;
_cache = cache; _cache = cache;
@@ -63,6 +72,11 @@ public class AdminController : ControllerBase
{ {
try try
{ {
// First check if we can get a valid (non-anonymous) token
var token = await _spotifyClient.GetWebAccessTokenAsync();
if (!string.IsNullOrEmpty(token))
{
// Try to get user info (may fail even with valid token)
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync(); var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync();
if (success) if (success)
{ {
@@ -70,6 +84,14 @@ public class AdminController : ControllerBase
spotifyUser = displayName ?? userId; spotifyUser = displayName ?? userId;
} }
else else
{
// Token is valid but /me endpoint failed - still consider it authenticated
// (the token being non-anonymous is the real indicator)
spotifyAuthStatus = "authenticated";
spotifyUser = "(profile not accessible)";
}
}
else
{ {
spotifyAuthStatus = "invalid_cookie"; spotifyAuthStatus = "invalid_cookie";
} }
@@ -95,6 +117,7 @@ public class AdminController : ControllerBase
authStatus = spotifyAuthStatus, authStatus = spotifyAuthStatus,
user = spotifyUser, user = spotifyUser,
hasCookie = !string.IsNullOrEmpty(_spotifyApiSettings.SessionCookie), hasCookie = !string.IsNullOrEmpty(_spotifyApiSettings.SessionCookie),
cookieSetDate = _spotifyApiSettings.SessionCookieSetDate,
cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes, cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes,
preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching
}, },
@@ -104,6 +127,20 @@ public class AdminController : ControllerBase
syncTime = $"{_spotifyImportSettings.SyncStartHour:D2}:{_spotifyImportSettings.SyncStartMinute:D2}", syncTime = $"{_spotifyImportSettings.SyncStartHour:D2}:{_spotifyImportSettings.SyncStartMinute:D2}",
syncWindowHours = _spotifyImportSettings.SyncWindowHours, syncWindowHours = _spotifyImportSettings.SyncWindowHours,
playlistCount = _spotifyImportSettings.Playlists.Count playlistCount = _spotifyImportSettings.Playlists.Count
},
deezer = new
{
hasArl = !string.IsNullOrEmpty(_deezerSettings.Arl),
quality = _deezerSettings.Quality ?? "FLAC"
},
qobuz = new
{
hasToken = !string.IsNullOrEmpty(_qobuzSettings.UserAuthToken),
quality = _qobuzSettings.Quality ?? "FLAC"
},
squidWtf = new
{
quality = _squidWtfSettings.Quality ?? "LOSSLESS"
} }
}); });
} }
@@ -214,8 +251,8 @@ public class AdminController : ControllerBase
spotifyApi = new spotifyApi = new
{ {
enabled = _spotifyApiSettings.Enabled, enabled = _spotifyApiSettings.Enabled,
clientId = MaskValue(_spotifyApiSettings.ClientId),
sessionCookie = MaskValue(_spotifyApiSettings.SessionCookie, showLast: 8), sessionCookie = MaskValue(_spotifyApiSettings.SessionCookie, showLast: 8),
sessionCookieSetDate = _spotifyApiSettings.SessionCookieSetDate,
cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes, cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes,
rateLimitDelayMs = _spotifyApiSettings.RateLimitDelayMs, rateLimitDelayMs = _spotifyApiSettings.RateLimitDelayMs,
preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching
@@ -238,6 +275,22 @@ public class AdminController : ControllerBase
url = _jellyfinSettings.Url, url = _jellyfinSettings.Url,
apiKey = MaskValue(_jellyfinSettings.ApiKey), apiKey = MaskValue(_jellyfinSettings.ApiKey),
libraryId = _jellyfinSettings.LibraryId libraryId = _jellyfinSettings.LibraryId
},
deezer = new
{
arl = MaskValue(_deezerSettings.Arl, showLast: 8),
arlFallback = MaskValue(_deezerSettings.ArlFallback, showLast: 8),
quality = _deezerSettings.Quality ?? "FLAC"
},
qobuz = new
{
userAuthToken = MaskValue(_qobuzSettings.UserAuthToken, showLast: 8),
userId = _qobuzSettings.UserId,
quality = _qobuzSettings.Quality ?? "FLAC"
},
squidWtf = new
{
quality = _squidWtfSettings.Quality ?? "LOSSLESS"
} }
}); });
} }
@@ -291,6 +344,16 @@ public class AdminController : ControllerBase
envContent[key] = value; envContent[key] = value;
appliedUpdates.Add(key); appliedUpdates.Add(key);
_logger.LogInformation(" Setting {Key}", key); _logger.LogInformation(" Setting {Key}", key);
// Auto-set cookie date when Spotify session cookie is updated
if (key == "SPOTIFY_API_SESSION_COOKIE" && !string.IsNullOrEmpty(value))
{
var dateKey = "SPOTIFY_API_SESSION_COOKIE_SET_DATE";
var dateValue = DateTime.UtcNow.ToString("o"); // ISO 8601 format
envContent[dateKey] = dateValue;
appliedUpdates.Add(dateKey);
_logger.LogInformation(" Auto-setting {Key} to {Value}", dateKey, dateValue);
}
} }
// Write back to .env file // Write back to .env file

View File

@@ -63,4 +63,10 @@ public class SpotifyApiSettings
/// Default: true /// Default: true
/// </summary> /// </summary>
public bool PreferIsrcMatching { get; set; } = true; public bool PreferIsrcMatching { get; set; } = true;
/// <summary>
/// ISO date string of when the session cookie was last set/updated.
/// Used to track cookie age and warn when it's approaching expiration (~1 year).
/// </summary>
public string? SessionCookieSetDate { get; set; }
} }

View File

@@ -622,6 +622,8 @@ public class SpotifyApiClient : IDisposable
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogWarning("Spotify /me endpoint returned {StatusCode}: {Body}", response.StatusCode, errorBody);
return (false, null, null); return (false, null, null);
} }

View File

@@ -210,18 +210,28 @@ public class SpotifyPlaylistFetcher : BackgroundService
return; return;
} }
// Verify authentication // Verify we can get an access token (the most reliable auth check)
_logger.LogInformation("Attempting Spotify authentication..."); _logger.LogInformation("Attempting Spotify authentication...");
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync(stoppingToken); var token = await _spotifyClient.GetWebAccessTokenAsync(stoppingToken);
if (!success) if (string.IsNullOrEmpty(token))
{ {
_logger.LogError("Failed to authenticate with Spotify - check session cookie"); _logger.LogError("Failed to get Spotify access token - check session cookie");
_logger.LogInformation("========================================"); _logger.LogInformation("========================================");
return; return;
} }
// Try to get user info (may fail even with valid token due to scope limitations)
var (gotUser, userId, displayName) = await _spotifyClient.GetCurrentUserAsync(stoppingToken);
_logger.LogInformation("Spotify API ENABLED"); _logger.LogInformation("Spotify API ENABLED");
if (gotUser)
{
_logger.LogInformation("Authenticated as: {DisplayName} ({UserId})", displayName, userId); _logger.LogInformation("Authenticated as: {DisplayName} ({UserId})", displayName, userId);
}
else
{
_logger.LogInformation("Authenticated (user profile not accessible, but token is valid)");
}
_logger.LogInformation("Cache duration: {Minutes} minutes", _spotifyApiSettings.CacheDurationMinutes); _logger.LogInformation("Cache duration: {Minutes} minutes", _spotifyApiSettings.CacheDurationMinutes);
_logger.LogInformation("ISRC matching: {Enabled}", _spotifyApiSettings.PreferIsrcMatching ? "enabled" : "disabled"); _logger.LogInformation("ISRC matching: {Enabled}", _spotifyApiSettings.PreferIsrcMatching ? "enabled" : "disabled");
_logger.LogInformation("Configured Playlists: {Count}", _spotifyImportSettings.Playlists.Count); _logger.LogInformation("Configured Playlists: {Count}", _spotifyImportSettings.Playlists.Count);

View File

@@ -473,6 +473,10 @@
<span class="stat-label">User</span> <span class="stat-label">User</span>
<span class="stat-value" id="spotify-user">-</span> <span class="stat-value" id="spotify-user">-</span>
</div> </div>
<div class="stat-row">
<span class="stat-label">Cookie Age</span>
<span class="stat-value" id="spotify-cookie-age">-</span>
</div>
<div class="stat-row"> <div class="stat-row">
<span class="stat-label">Cache Duration</span> <span class="stat-label">Cache Duration</span>
<span class="stat-value" id="cache-duration">-</span> <span class="stat-value" id="cache-duration">-</span>
@@ -552,23 +556,69 @@
<div class="config-item"> <div class="config-item">
<span class="label">API Enabled</span> <span class="label">API Enabled</span>
<span class="value" id="config-spotify-enabled">-</span> <span class="value" id="config-spotify-enabled">-</span>
<button onclick="openEditSetting('SPOTIFY_API_ENABLED', 'Spotify API Enabled', 'toggle')">Edit</button>
</div> </div>
<div class="config-item"> <div class="config-item">
<span class="label">Client ID</span> <span class="label">Session Cookie (sp_dc)</span>
<span class="value" id="config-spotify-client-id">-</span>
</div>
<div class="config-item">
<span class="label">Session Cookie</span>
<span class="value" id="config-spotify-cookie">-</span> <span class="value" id="config-spotify-cookie">-</span>
<button onclick="openUpdateCookie()">Update</button> <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>
</div> </div>
<div class="config-item"> <div class="config-item">
<span class="label">Cache Duration</span> <span class="label">Cache Duration</span>
<span class="value" id="config-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>
</div> </div>
<div class="config-item"> <div class="config-item">
<span class="label">ISRC Matching</span> <span class="label">ISRC Matching</span>
<span class="value" id="config-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>
</div>
</div>
</div>
<div class="card">
<h2>Deezer Settings</h2>
<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>
</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>
</div>
</div>
</div>
<div class="card">
<h2>SquidWTF / Tidal Settings</h2>
<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', '', ['LOSSLESS', 'HIGH', 'LOW'])">Edit</button>
</div>
</div>
</div>
<div class="card">
<h2>Qobuz Settings</h2>
<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>
</div>
<div class="config-item">
<span class="label">Quality</span>
<span class="value" id="config-qobuz-quality">-</span>
<button onclick="openEditSetting('QOBUZ_QUALITY', 'Qobuz Quality', 'select', '', ['FLAC_24_192', 'FLAC_24_96', 'FLAC_16_44', 'MP3_320'])">Edit</button>
</div> </div>
</div> </div>
</div> </div>
@@ -579,14 +629,33 @@
<div class="config-item"> <div class="config-item">
<span class="label">URL</span> <span class="label">URL</span>
<span class="value" id="config-jellyfin-url">-</span> <span class="value" id="config-jellyfin-url">-</span>
<button onclick="openEditSetting('JELLYFIN_URL', 'Jellyfin URL', 'text')">Edit</button>
</div> </div>
<div class="config-item"> <div class="config-item">
<span class="label">API Key</span> <span class="label">API Key</span>
<span class="value" id="config-jellyfin-api-key">-</span> <span class="value" id="config-jellyfin-api-key">-</span>
<button onclick="openEditSetting('JELLYFIN_API_KEY', 'Jellyfin API Key', 'password')">Update</button>
</div> </div>
<div class="config-item"> <div class="config-item">
<span class="label">Library ID</span> <span class="label">Library ID</span>
<span class="value" id="config-jellyfin-library-id">-</span> <span class="value" id="config-jellyfin-library-id">-</span>
<button onclick="openEditSetting('JELLYFIN_LIBRARY_ID', 'Jellyfin Library ID', 'text')">Edit</button>
</div>
</div>
</div>
<div class="card">
<h2>Sync Schedule</h2>
<div class="config-section">
<div class="config-item">
<span class="label">Sync Start Time</span>
<span class="value" id="config-sync-time">-</span>
<button onclick="openEditSetting('SPOTIFY_IMPORT_SYNC_START_HOUR', 'Sync Start Hour (0-23)', 'number')">Edit</button>
</div>
<div class="config-item">
<span class="label">Sync Window</span>
<span class="value" id="config-sync-window">-</span>
<button onclick="openEditSetting('SPOTIFY_IMPORT_SYNC_WINDOW_HOURS', 'Sync Window (hours)', 'number')">Edit</button>
</div> </div>
</div> </div>
</div> </div>
@@ -596,7 +665,10 @@
<p style="color: var(--text-secondary); margin-bottom: 16px;"> <p style="color: var(--text-secondary); margin-bottom: 16px;">
These actions can affect your data. Use with caution. These actions can affect your data. Use with caution.
</p> </p>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button class="danger" onclick="clearCache()">Clear All Cache</button> <button class="danger" onclick="clearCache()">Clear All Cache</button>
<button class="danger" onclick="restartContainer()">Restart Container</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -627,20 +699,20 @@
</div> </div>
</div> </div>
<!-- Update Cookie Modal --> <!-- Edit Setting Modal -->
<div class="modal" id="update-cookie-modal"> <div class="modal" id="edit-setting-modal">
<div class="modal-content"> <div class="modal-content">
<h3>Update Spotify Cookie</h3> <h3 id="edit-setting-title">Edit Setting</h3>
<p style="color: var(--text-secondary); margin-bottom: 16px;"> <p id="edit-setting-help" style="color: var(--text-secondary); margin-bottom: 16px; display: none;"></p>
Get the sp_dc cookie from your browser's dev tools while logged into Spotify.
</p>
<div class="form-group"> <div class="form-group">
<label>sp_dc Cookie Value</label> <label id="edit-setting-label">Value</label>
<input type="text" id="new-cookie-value" placeholder="Paste cookie value here"> <div id="edit-setting-input-container">
<input type="text" id="edit-setting-value" placeholder="Enter value">
</div>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button onclick="closeModal('update-cookie-modal')">Cancel</button> <button onclick="closeModal('edit-setting-modal')">Cancel</button>
<button class="primary" onclick="updateCookie()">Update Cookie</button> <button class="primary" onclick="saveEditSetting()">Save</button>
</div> </div>
</div> </div>
</div> </div>
@@ -661,6 +733,11 @@
</div> </div>
<script> <script>
// Current edit setting state
let currentEditKey = null;
let currentEditType = null;
let currentEditOptions = null;
// Tab switching // Tab switching
document.querySelectorAll('.tab').forEach(tab => { document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
@@ -696,6 +773,37 @@
}); });
}); });
// Format cookie age with color coding
function formatCookieAge(setDateStr) {
if (!setDateStr) return { text: 'Unknown age', class: 'warning', detail: 'Set the cookie to start tracking its age' };
const setDate = new Date(setDateStr);
const now = new Date();
const diffMs = now - setDate;
const daysAgo = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const monthsAgo = daysAgo / 30;
let status = 'success'; // green: < 6 months
if (monthsAgo >= 10) status = 'error'; // red: > 10 months
else if (monthsAgo >= 6) status = 'warning'; // yellow: 6-10 months
let text;
if (daysAgo === 0) text = 'Set today';
else if (daysAgo === 1) text = 'Set yesterday';
else if (daysAgo < 30) text = `Set ${daysAgo} days ago`;
else if (daysAgo < 60) text = 'Set ~1 month ago';
else text = `Set ~${Math.floor(monthsAgo)} months ago`;
const remaining = 12 - monthsAgo;
let detail;
if (remaining > 6) detail = 'Cookie typically lasts ~1 year';
else if (remaining > 2) detail = `~${Math.floor(remaining)} months until expiration`;
else if (remaining > 0) detail = 'Cookie may expire soon!';
else detail = 'Cookie may have expired - update if having issues';
return { text, class: status, detail };
}
// API calls // API calls
async function fetchStatus() { async function fetchStatus() {
try { try {
@@ -710,9 +818,10 @@
document.getElementById('isrc-matching').textContent = data.spotify.preferIsrcMatching ? 'Enabled' : 'Disabled'; document.getElementById('isrc-matching').textContent = data.spotify.preferIsrcMatching ? 'Enabled' : 'Disabled';
document.getElementById('spotify-user').textContent = data.spotify.user || '-'; document.getElementById('spotify-user').textContent = data.spotify.user || '-';
// Update status badge // Update status badge and cookie age
const statusBadge = document.getElementById('spotify-status'); const statusBadge = document.getElementById('spotify-status');
const authStatus = document.getElementById('spotify-auth-status'); const authStatus = document.getElementById('spotify-auth-status');
const cookieAgeEl = document.getElementById('spotify-cookie-age');
if (data.spotify.authStatus === 'authenticated') { if (data.spotify.authStatus === 'authenticated') {
statusBadge.className = 'status-badge success'; statusBadge.className = 'status-badge success';
@@ -735,6 +844,12 @@
authStatus.textContent = 'Not Configured'; authStatus.textContent = 'Not Configured';
authStatus.className = 'stat-value'; authStatus.className = 'stat-value';
} }
// Update cookie age display
if (cookieAgeEl) {
const age = formatCookieAge(data.spotify.cookieSetDate);
cookieAgeEl.innerHTML = `<span class="${age.class}">${age.text}</span><br><small style="color:var(--text-secondary)">${age.detail}</small>`;
}
} catch (error) { } catch (error) {
console.error('Failed to fetch status:', error); console.error('Failed to fetch status:', error);
showToast('Failed to fetch status', 'error'); showToast('Failed to fetch status', 'error');
@@ -777,14 +892,40 @@
const res = await fetch('/api/admin/config'); const res = await fetch('/api/admin/config');
const data = await res.json(); const data = await res.json();
// Spotify API settings
document.getElementById('config-spotify-enabled').textContent = data.spotifyApi.enabled ? 'Yes' : 'No'; document.getElementById('config-spotify-enabled').textContent = data.spotifyApi.enabled ? 'Yes' : 'No';
document.getElementById('config-spotify-client-id').textContent = data.spotifyApi.clientId;
document.getElementById('config-spotify-cookie').textContent = data.spotifyApi.sessionCookie; document.getElementById('config-spotify-cookie').textContent = data.spotifyApi.sessionCookie;
document.getElementById('config-cache-duration').textContent = data.spotifyApi.cacheDurationMinutes + ' minutes'; document.getElementById('config-cache-duration').textContent = data.spotifyApi.cacheDurationMinutes + ' minutes';
document.getElementById('config-isrc-matching').textContent = data.spotifyApi.preferIsrcMatching ? 'Enabled' : 'Disabled'; document.getElementById('config-isrc-matching').textContent = data.spotifyApi.preferIsrcMatching ? 'Enabled' : 'Disabled';
// Cookie age in config tab
const configCookieAge = document.getElementById('config-cookie-age');
if (configCookieAge) {
const age = formatCookieAge(data.spotifyApi.sessionCookieSetDate);
configCookieAge.innerHTML = `<span class="${age.class}">${age.text}</span> - ${age.detail}`;
}
// Deezer settings
document.getElementById('config-deezer-arl').textContent = data.deezer.arl || '(not set)';
document.getElementById('config-deezer-quality').textContent = data.deezer.quality;
// SquidWTF settings
document.getElementById('config-squid-quality').textContent = data.squidWtf.quality;
// Qobuz settings
document.getElementById('config-qobuz-token').textContent = data.qobuz.userAuthToken || '(not set)';
document.getElementById('config-qobuz-quality').textContent = data.qobuz.quality || 'FLAC';
// Jellyfin settings
document.getElementById('config-jellyfin-url').textContent = data.jellyfin.url || '-'; document.getElementById('config-jellyfin-url').textContent = data.jellyfin.url || '-';
document.getElementById('config-jellyfin-api-key').textContent = data.jellyfin.apiKey; document.getElementById('config-jellyfin-api-key').textContent = data.jellyfin.apiKey;
document.getElementById('config-jellyfin-library-id').textContent = data.jellyfin.libraryId || '-'; document.getElementById('config-jellyfin-library-id').textContent = data.jellyfin.libraryId || '-';
// Sync settings
const syncHour = data.spotifyImport.syncStartHour;
const syncMin = data.spotifyImport.syncStartMinute;
document.getElementById('config-sync-time').textContent = `${String(syncHour).padStart(2, '0')}:${String(syncMin).padStart(2, '0')}`;
document.getElementById('config-sync-window').textContent = data.spotifyImport.syncWindowHours + ' hours';
} catch (error) { } catch (error) {
console.error('Failed to fetch config:', error); console.error('Failed to fetch config:', error);
} }
@@ -815,6 +956,10 @@
} }
} }
function restartContainer() {
alert('To apply configuration changes, please restart the container manually via Dockge or docker-compose:\n\ndocker-compose restart allstarr\n\nor use Dockge UI to restart the stack.');
}
function openAddPlaylist() { function openAddPlaylist() {
document.getElementById('new-playlist-name').value = ''; document.getElementById('new-playlist-name').value = '';
document.getElementById('new-playlist-id').value = ''; document.getElementById('new-playlist-id').value = '';
@@ -905,16 +1050,54 @@
} }
} }
function openUpdateCookie() { // Generic edit setting modal
document.getElementById('new-cookie-value').value = ''; function openEditSetting(envKey, label, inputType, helpText = '', options = []) {
openModal('update-cookie-modal'); currentEditKey = envKey;
currentEditType = inputType;
currentEditOptions = options;
document.getElementById('edit-setting-title').textContent = 'Edit ' + label;
document.getElementById('edit-setting-label').textContent = label;
const helpEl = document.getElementById('edit-setting-help');
if (helpText) {
helpEl.textContent = helpText;
helpEl.style.display = 'block';
} else {
helpEl.style.display = 'none';
} }
async function updateCookie() { const container = document.getElementById('edit-setting-input-container');
const cookie = document.getElementById('new-cookie-value').value.trim();
if (!cookie) { if (inputType === 'toggle') {
showToast('Cookie value is required', 'error'); container.innerHTML = `
<select id="edit-setting-value">
<option value="true">Enabled</option>
<option value="false">Disabled</option>
</select>
`;
} else if (inputType === 'select') {
container.innerHTML = `
<select id="edit-setting-value">
${options.map(opt => `<option value="${opt}">${opt}</option>`).join('')}
</select>
`;
} else if (inputType === 'password') {
container.innerHTML = `<input type="password" id="edit-setting-value" placeholder="Enter new value" autocomplete="off">`;
} else if (inputType === 'number') {
container.innerHTML = `<input type="number" id="edit-setting-value" placeholder="Enter value">`;
} else {
container.innerHTML = `<input type="text" id="edit-setting-value" placeholder="Enter value">`;
}
openModal('edit-setting-modal');
}
async function saveEditSetting() {
const value = document.getElementById('edit-setting-value').value.trim();
if (!value && currentEditType !== 'toggle') {
showToast('Value is required', 'error');
return; return;
} }
@@ -922,20 +1105,21 @@
const res = await fetch('/api/admin/config', { const res = await fetch('/api/admin/config', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updates: { 'SPOTIFY_API_SESSION_COOKIE': cookie } }) body: JSON.stringify({ updates: { [currentEditKey]: value } })
}); });
const data = await res.json(); const data = await res.json();
if (res.ok) { if (res.ok) {
showToast('Cookie updated. Restart container to apply.', 'success'); showToast('Setting updated. Restart container to apply.', 'success');
closeModal('update-cookie-modal'); closeModal('edit-setting-modal');
fetchConfig(); fetchConfig();
fetchStatus();
} else { } else {
showToast(data.error || 'Failed to update cookie', 'error'); showToast(data.error || 'Failed to update setting', 'error');
} }
} catch (error) { } catch (error) {
showToast('Failed to update cookie', 'error'); showToast('Failed to update setting', 'error');
} }
} }