mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
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:
@@ -24,6 +24,9 @@ public class AdminController : ControllerBase
|
||||
private readonly SpotifyApiSettings _spotifyApiSettings;
|
||||
private readonly SpotifyImportSettings _spotifyImportSettings;
|
||||
private readonly JellyfinSettings _jellyfinSettings;
|
||||
private readonly DeezerSettings _deezerSettings;
|
||||
private readonly QobuzSettings _qobuzSettings;
|
||||
private readonly SquidWTFSettings _squidWtfSettings;
|
||||
private readonly SpotifyApiClient _spotifyClient;
|
||||
private readonly SpotifyPlaylistFetcher _playlistFetcher;
|
||||
private readonly RedisCacheService _cache;
|
||||
@@ -36,6 +39,9 @@ public class AdminController : ControllerBase
|
||||
IOptions<SpotifyApiSettings> spotifyApiSettings,
|
||||
IOptions<SpotifyImportSettings> spotifyImportSettings,
|
||||
IOptions<JellyfinSettings> jellyfinSettings,
|
||||
IOptions<DeezerSettings> deezerSettings,
|
||||
IOptions<QobuzSettings> qobuzSettings,
|
||||
IOptions<SquidWTFSettings> squidWtfSettings,
|
||||
SpotifyApiClient spotifyClient,
|
||||
SpotifyPlaylistFetcher playlistFetcher,
|
||||
RedisCacheService cache)
|
||||
@@ -45,6 +51,9 @@ public class AdminController : ControllerBase
|
||||
_spotifyApiSettings = spotifyApiSettings.Value;
|
||||
_spotifyImportSettings = spotifyImportSettings.Value;
|
||||
_jellyfinSettings = jellyfinSettings.Value;
|
||||
_deezerSettings = deezerSettings.Value;
|
||||
_qobuzSettings = qobuzSettings.Value;
|
||||
_squidWtfSettings = squidWtfSettings.Value;
|
||||
_spotifyClient = spotifyClient;
|
||||
_playlistFetcher = playlistFetcher;
|
||||
_cache = cache;
|
||||
@@ -63,11 +72,24 @@ public class AdminController : ControllerBase
|
||||
{
|
||||
try
|
||||
{
|
||||
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync();
|
||||
if (success)
|
||||
// First check if we can get a valid (non-anonymous) token
|
||||
var token = await _spotifyClient.GetWebAccessTokenAsync();
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
spotifyAuthStatus = "authenticated";
|
||||
spotifyUser = displayName ?? userId;
|
||||
// Try to get user info (may fail even with valid token)
|
||||
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync();
|
||||
if (success)
|
||||
{
|
||||
spotifyAuthStatus = "authenticated";
|
||||
spotifyUser = displayName ?? userId;
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -95,6 +117,7 @@ public class AdminController : ControllerBase
|
||||
authStatus = spotifyAuthStatus,
|
||||
user = spotifyUser,
|
||||
hasCookie = !string.IsNullOrEmpty(_spotifyApiSettings.SessionCookie),
|
||||
cookieSetDate = _spotifyApiSettings.SessionCookieSetDate,
|
||||
cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes,
|
||||
preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching
|
||||
},
|
||||
@@ -104,6 +127,20 @@ public class AdminController : ControllerBase
|
||||
syncTime = $"{_spotifyImportSettings.SyncStartHour:D2}:{_spotifyImportSettings.SyncStartMinute:D2}",
|
||||
syncWindowHours = _spotifyImportSettings.SyncWindowHours,
|
||||
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
|
||||
{
|
||||
enabled = _spotifyApiSettings.Enabled,
|
||||
clientId = MaskValue(_spotifyApiSettings.ClientId),
|
||||
sessionCookie = MaskValue(_spotifyApiSettings.SessionCookie, showLast: 8),
|
||||
sessionCookieSetDate = _spotifyApiSettings.SessionCookieSetDate,
|
||||
cacheDurationMinutes = _spotifyApiSettings.CacheDurationMinutes,
|
||||
rateLimitDelayMs = _spotifyApiSettings.RateLimitDelayMs,
|
||||
preferIsrcMatching = _spotifyApiSettings.PreferIsrcMatching
|
||||
@@ -238,6 +275,22 @@ public class AdminController : ControllerBase
|
||||
url = _jellyfinSettings.Url,
|
||||
apiKey = MaskValue(_jellyfinSettings.ApiKey),
|
||||
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;
|
||||
appliedUpdates.Add(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
|
||||
|
||||
@@ -63,4 +63,10 @@ public class SpotifyApiSettings
|
||||
/// Default: true
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -622,6 +622,8 @@ public class SpotifyApiClient : IDisposable
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -210,18 +210,28 @@ public class SpotifyPlaylistFetcher : BackgroundService
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify authentication
|
||||
// Verify we can get an access token (the most reliable auth check)
|
||||
_logger.LogInformation("Attempting Spotify authentication...");
|
||||
var (success, userId, displayName) = await _spotifyClient.GetCurrentUserAsync(stoppingToken);
|
||||
if (!success)
|
||||
var token = await _spotifyClient.GetWebAccessTokenAsync(stoppingToken);
|
||||
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("========================================");
|
||||
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("Authenticated as: {DisplayName} ({UserId})", displayName, userId);
|
||||
if (gotUser)
|
||||
{
|
||||
_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("ISRC matching: {Enabled}", _spotifyApiSettings.PreferIsrcMatching ? "enabled" : "disabled");
|
||||
_logger.LogInformation("Configured Playlists: {Count}", _spotifyImportSettings.Playlists.Count);
|
||||
|
||||
@@ -473,6 +473,10 @@
|
||||
<span class="stat-label">User</span>
|
||||
<span class="stat-value" id="spotify-user">-</span>
|
||||
</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">
|
||||
<span class="stat-label">Cache Duration</span>
|
||||
<span class="stat-value" id="cache-duration">-</span>
|
||||
@@ -552,23 +556,69 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Client ID</span>
|
||||
<span class="value" id="config-spotify-client-id">-</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">Session Cookie</span>
|
||||
<span class="label">Session Cookie (sp_dc)</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 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>
|
||||
</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>
|
||||
</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>
|
||||
@@ -579,14 +629,33 @@
|
||||
<div class="config-item">
|
||||
<span class="label">URL</span>
|
||||
<span class="value" id="config-jellyfin-url">-</span>
|
||||
<button onclick="openEditSetting('JELLYFIN_URL', 'Jellyfin URL', 'text')">Edit</button>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="label">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 class="config-item">
|
||||
<span class="label">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>
|
||||
@@ -596,7 +665,10 @@
|
||||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||||
These actions can affect your data. Use with caution.
|
||||
</p>
|
||||
<button class="danger" onclick="clearCache()">Clear All Cache</button>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<button class="danger" onclick="clearCache()">Clear All Cache</button>
|
||||
<button class="danger" onclick="restartContainer()">Restart Container</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -627,20 +699,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Cookie Modal -->
|
||||
<div class="modal" id="update-cookie-modal">
|
||||
<!-- Edit Setting Modal -->
|
||||
<div class="modal" id="edit-setting-modal">
|
||||
<div class="modal-content">
|
||||
<h3>Update Spotify Cookie</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||||
Get the sp_dc cookie from your browser's dev tools while logged into Spotify.
|
||||
</p>
|
||||
<h3 id="edit-setting-title">Edit Setting</h3>
|
||||
<p id="edit-setting-help" style="color: var(--text-secondary); margin-bottom: 16px; display: none;"></p>
|
||||
<div class="form-group">
|
||||
<label>sp_dc Cookie Value</label>
|
||||
<input type="text" id="new-cookie-value" placeholder="Paste cookie value here">
|
||||
<label id="edit-setting-label">Value</label>
|
||||
<div id="edit-setting-input-container">
|
||||
<input type="text" id="edit-setting-value" placeholder="Enter value">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button onclick="closeModal('update-cookie-modal')">Cancel</button>
|
||||
<button class="primary" onclick="updateCookie()">Update Cookie</button>
|
||||
<button onclick="closeModal('edit-setting-modal')">Cancel</button>
|
||||
<button class="primary" onclick="saveEditSetting()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -661,6 +733,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Current edit setting state
|
||||
let currentEditKey = null;
|
||||
let currentEditType = null;
|
||||
let currentEditOptions = null;
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
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
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
@@ -710,9 +818,10 @@
|
||||
document.getElementById('isrc-matching').textContent = data.spotify.preferIsrcMatching ? 'Enabled' : 'Disabled';
|
||||
document.getElementById('spotify-user').textContent = data.spotify.user || '-';
|
||||
|
||||
// Update status badge
|
||||
// Update status badge and cookie age
|
||||
const statusBadge = document.getElementById('spotify-status');
|
||||
const authStatus = document.getElementById('spotify-auth-status');
|
||||
const cookieAgeEl = document.getElementById('spotify-cookie-age');
|
||||
|
||||
if (data.spotify.authStatus === 'authenticated') {
|
||||
statusBadge.className = 'status-badge success';
|
||||
@@ -735,6 +844,12 @@
|
||||
authStatus.textContent = 'Not Configured';
|
||||
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) {
|
||||
console.error('Failed to fetch status:', error);
|
||||
showToast('Failed to fetch status', 'error');
|
||||
@@ -777,14 +892,40 @@
|
||||
const res = await fetch('/api/admin/config');
|
||||
const data = await res.json();
|
||||
|
||||
// Spotify API settings
|
||||
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-cache-duration').textContent = data.spotifyApi.cacheDurationMinutes + ' minutes';
|
||||
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-api-key').textContent = data.jellyfin.apiKey;
|
||||
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) {
|
||||
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() {
|
||||
document.getElementById('new-playlist-name').value = '';
|
||||
document.getElementById('new-playlist-id').value = '';
|
||||
@@ -905,16 +1050,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
function openUpdateCookie() {
|
||||
document.getElementById('new-cookie-value').value = '';
|
||||
openModal('update-cookie-modal');
|
||||
// Generic edit setting modal
|
||||
function openEditSetting(envKey, label, inputType, helpText = '', options = []) {
|
||||
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';
|
||||
}
|
||||
|
||||
const container = document.getElementById('edit-setting-input-container');
|
||||
|
||||
if (inputType === 'toggle') {
|
||||
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 updateCookie() {
|
||||
const cookie = document.getElementById('new-cookie-value').value.trim();
|
||||
async function saveEditSetting() {
|
||||
const value = document.getElementById('edit-setting-value').value.trim();
|
||||
|
||||
if (!cookie) {
|
||||
showToast('Cookie value is required', 'error');
|
||||
if (!value && currentEditType !== 'toggle') {
|
||||
showToast('Value is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -922,20 +1105,21 @@
|
||||
const res = await fetch('/api/admin/config', {
|
||||
method: 'POST',
|
||||
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();
|
||||
|
||||
if (res.ok) {
|
||||
showToast('Cookie updated. Restart container to apply.', 'success');
|
||||
closeModal('update-cookie-modal');
|
||||
showToast('Setting updated. Restart container to apply.', 'success');
|
||||
closeModal('edit-setting-modal');
|
||||
fetchConfig();
|
||||
fetchStatus();
|
||||
} else {
|
||||
showToast(data.error || 'Failed to update cookie', 'error');
|
||||
showToast(data.error || 'Failed to update setting', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to update cookie', 'error');
|
||||
showToast('Failed to update setting', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user