diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 6da2235..7a271e3 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -259,6 +259,7 @@ public class AdminController : ControllerBase ["id"] = config.Id, ["jellyfinId"] = config.JellyfinId, ["localTracksPosition"] = config.LocalTracksPosition.ToString(), + ["syncSchedule"] = config.SyncSchedule ?? "0 8 * * 1", ["trackCount"] = 0, ["localTracks"] = 0, ["externalTracks"] = 0, @@ -2311,6 +2312,60 @@ public class AdminController : ControllerBase return await RemovePlaylist(decodedName); } + /// + /// Update playlist sync schedule + /// + [HttpPut("playlists/{name}/schedule")] + public async Task UpdatePlaylistSchedule(string name, [FromBody] UpdateScheduleRequest request) + { + var decodedName = Uri.UnescapeDataString(name); + + if (string.IsNullOrWhiteSpace(request.SyncSchedule)) + { + return BadRequest(new { error = "SyncSchedule is required" }); + } + + // Basic cron validation + var cronParts = request.SyncSchedule.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + if (cronParts.Length != 5) + { + return BadRequest(new { error = "Invalid cron format. Expected: minute hour day month dayofweek" }); + } + + // Read current playlists + var currentPlaylists = await ReadPlaylistsFromEnvFile(); + var playlist = currentPlaylists.FirstOrDefault(p => p.Name.Equals(decodedName, StringComparison.OrdinalIgnoreCase)); + + if (playlist == null) + { + return NotFound(new { error = $"Playlist '{decodedName}' not found" }); + } + + // Update the schedule + playlist.SyncSchedule = request.SyncSchedule.Trim(); + + // Save back to .env + var playlistsJson = JsonSerializer.Serialize( + currentPlaylists.Select(p => new[] { + p.Name, + p.Id, + p.JellyfinId, + p.LocalTracksPosition.ToString().ToLower(), + p.SyncSchedule ?? "0 8 * * 1" + }).ToArray() + ); + + var updateRequest = new ConfigUpdateRequest + { + Updates = new Dictionary + { + ["SPOTIFY_IMPORT_PLAYLISTS"] = playlistsJson + } + }; + + return await UpdateConfig(updateRequest); + } + private string GetJellyfinAuthHeader() { return $"MediaBrowser Client=\"Allstarr\", Device=\"Server\", DeviceId=\"allstarr-admin\", Version=\"1.0.0\", Token=\"{_jellyfinSettings.ApiKey}\""; @@ -3417,6 +3472,11 @@ public class LinkPlaylistRequest public string SyncSchedule { get; set; } = "0 8 * * 1"; // Default: 8 AM every Monday } +public class UpdateScheduleRequest +{ + public string SyncSchedule { get; set; } = string.Empty; +} + /// /// GET /api/admin/downloads /// Lists all downloaded files in the KEPT folder only (favorited tracks) diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 1a3ce1e..d40785d 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -537,7 +537,7 @@
Dashboard
Link Playlists
-
Active Playlists
+
Injected Playlists
Configuration
API Analytics
@@ -652,7 +652,7 @@

- Active Spotify Playlists + Injected Spotify Playlists
@@ -660,13 +660,14 @@

- These are the Spotify playlists currently being monitored and filled with tracks from your music service. + These are the Spotify playlists currently being injected into Jellyfin with tracks from your music service.

+ @@ -675,7 +676,7 @@ - @@ -1545,7 +1546,7 @@ if (data.playlists.length === 0) { if (!silent) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; } return; } @@ -1599,10 +1600,16 @@ // Debug logging console.log(`Progress bar for ${p.name}: local=${localPct}%, external=${externalPct}%, missing=${missingPct}%, total=${completionPct}%`); + const syncSchedule = p.syncSchedule || '0 8 * * 1'; + return ` +
Name Spotify IDSync Schedule Tracks Completion Cache Age
+ Loading playlists...
No playlists configured. Link playlists from the Jellyfin Playlists tab.
No playlists configured. Link playlists from the Jellyfin Playlists tab.
${escapeHtml(p.name)} ${p.id || '-'} + ${escapeHtml(syncSchedule)} + + ${statsHtml}${breakdown}
@@ -2025,7 +2032,14 @@ const res = await fetch('/api/admin/spotify/user-playlists'); if (!res.ok) { const error = await res.json(); - console.error('Failed to fetch Spotify playlists:', error); + console.error('Failed to fetch Spotify playlists:', res.status, error); + + // Show user-friendly error message + if (res.status === 429) { + showToast('Spotify rate limit reached. Please wait a moment and try again.', 'warning', 5000); + } else if (res.status === 401) { + showToast('Spotify authentication failed. Check your sp_dc cookie.', 'error', 5000); + } return []; } const data = await res.json(); @@ -2563,6 +2577,39 @@ } } + async function editPlaylistSchedule(playlistName, currentSchedule) { + const newSchedule = prompt(`Edit sync schedule for "${playlistName}"\n\nCron format: minute hour day month dayofweek\nExamples:\n• 0 8 * * 1 = Monday 8 AM\n• 0 6 * * * = Daily 6 AM\n• 0 20 * * 5 = Friday 8 PM`, currentSchedule); + + if (!newSchedule || newSchedule === currentSchedule) return; + + // Validate cron format + const cronParts = newSchedule.trim().split(/\s+/); + if (cronParts.length !== 5) { + showToast('Invalid cron format. Expected: minute hour day month dayofweek', 'error'); + return; + } + + try { + const res = await fetch(`/api/admin/playlists/${encodeURIComponent(playlistName)}/schedule`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ syncSchedule: newSchedule.trim() }) + }); + + if (res.ok) { + showToast('Sync schedule updated!', 'success'); + showRestartBanner(); + fetchPlaylists(); + } else { + const error = await res.json(); + showToast(error.error || 'Failed to update schedule', 'error'); + } + } catch (error) { + console.error('Failed to update schedule:', error); + showToast('Failed to update schedule', 'error'); + } + } + async function removePlaylist(name) { if (!confirm(`Remove playlist "${name}"?`)) return;