feat: add per-playlist cron sync schedules

Each playlist now has its own cron schedule for syncing with Spotify. Default is 0 8 * * 1 (Monday 8 AM). Removed global MatchingIntervalHours in favor of per-playlist scheduling.
This commit is contained in:
2026-02-09 13:15:04 -05:00
parent faa07c2791
commit 2b4cd35cf7
3 changed files with 51 additions and 6 deletions

View File

@@ -2274,12 +2274,19 @@ public class AdminController : ControllerBase
Name = request.Name, Name = request.Name,
Id = request.SpotifyPlaylistId, Id = request.SpotifyPlaylistId,
JellyfinId = jellyfinPlaylistId, JellyfinId = jellyfinPlaylistId,
LocalTracksPosition = LocalTracksPosition.First // Use Spotify order LocalTracksPosition = LocalTracksPosition.First, // Use Spotify order
SyncSchedule = request.SyncSchedule ?? "0 8 * * 1" // Default to Monday 8 AM
}); });
// Convert to JSON format for env var: [["Name","SpotifyId","JellyfinId","first|last"],...] // Convert to JSON format for env var: [["Name","SpotifyId","JellyfinId","first|last","cronSchedule"],...]
var playlistsJson = JsonSerializer.Serialize( var playlistsJson = JsonSerializer.Serialize(
currentPlaylists.Select(p => new[] { p.Name, p.Id, p.JellyfinId, p.LocalTracksPosition.ToString().ToLower() }).ToArray() currentPlaylists.Select(p => new[] {
p.Name,
p.Id,
p.JellyfinId,
p.LocalTracksPosition.ToString().ToLower(),
p.SyncSchedule ?? "0 8 * * 1"
}).ToArray()
); );
// Update .env file // Update .env file
@@ -2335,7 +2342,7 @@ public class AdminController : ControllerBase
return playlists; return playlists;
} }
// Parse JSON array format: [["Name","SpotifyId","JellyfinId","first|last"],...] // Parse JSON array format: [["Name","SpotifyId","JellyfinId","first|last","cronSchedule"],...]
var playlistArrays = JsonSerializer.Deserialize<string[][]>(value); var playlistArrays = JsonSerializer.Deserialize<string[][]>(value);
if (playlistArrays != null) if (playlistArrays != null)
{ {
@@ -2351,7 +2358,8 @@ public class AdminController : ControllerBase
LocalTracksPosition = arr.Length >= 4 && LocalTracksPosition = arr.Length >= 4 &&
arr[3].Trim().Equals("last", StringComparison.OrdinalIgnoreCase) arr[3].Trim().Equals("last", StringComparison.OrdinalIgnoreCase)
? LocalTracksPosition.Last ? LocalTracksPosition.Last
: LocalTracksPosition.First : LocalTracksPosition.First,
SyncSchedule = arr.Length >= 5 ? arr[4].Trim() : "0 8 * * 1"
}); });
} }
} }
@@ -3406,6 +3414,7 @@ public class LinkPlaylistRequest
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string SpotifyPlaylistId { get; set; } = string.Empty; public string SpotifyPlaylistId { get; set; } = string.Empty;
public string SyncSchedule { get; set; } = "0 8 * * 1"; // Default: 8 AM every Monday
} }
/// <summary> /// <summary>

View File

@@ -45,6 +45,14 @@ public class SpotifyPlaylistConfig
/// Where to position local tracks: "first" or "last" /// Where to position local tracks: "first" or "last"
/// </summary> /// </summary>
public LocalTracksPosition LocalTracksPosition { get; set; } = LocalTracksPosition.First; public LocalTracksPosition LocalTracksPosition { get; set; } = LocalTracksPosition.First;
/// <summary>
/// Cron schedule for syncing this playlist with Spotify
/// Format: minute hour day month dayofweek
/// Example: "0 8 * * 1" = 8 AM every Monday
/// Default: "0 8 * * 1" (weekly on Monday at 8 AM)
/// </summary>
public string SyncSchedule { get; set; } = "0 8 * * 1";
} }
/// <summary> /// <summary>

View File

@@ -1254,6 +1254,17 @@
</small> </small>
</div> </div>
<!-- Sync Schedule -->
<div class="form-group">
<label>Sync Schedule (Cron)</label>
<input type="text" id="link-sync-schedule" placeholder="0 8 * * 1" value="0 8 * * 1" style="font-family: monospace;">
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
Cron format: <code>minute hour day month dayofweek</code><br>
Default: <code>0 8 * * 1</code> = 8 AM every Monday<br>
Examples: <code>0 6 * * *</code> = daily at 6 AM, <code>0 20 * * 5</code> = Fridays at 8 PM
</small>
</div>
<div class="modal-actions"> <div class="modal-actions">
<button onclick="closeModal('link-playlist-modal')">Cancel</button> <button onclick="closeModal('link-playlist-modal')">Cancel</button>
<button class="primary" onclick="linkPlaylist()">Link Playlist</button> <button class="primary" onclick="linkPlaylist()">Link Playlist</button>
@@ -2080,6 +2091,19 @@
async function linkPlaylist() { async function linkPlaylist() {
const jellyfinId = document.getElementById('link-jellyfin-id').value; const jellyfinId = document.getElementById('link-jellyfin-id').value;
const name = document.getElementById('link-jellyfin-name').value; const name = document.getElementById('link-jellyfin-name').value;
const syncSchedule = document.getElementById('link-sync-schedule').value.trim();
// Validate sync schedule (basic cron format check)
if (!syncSchedule) {
showToast('Sync schedule is required', 'error');
return;
}
const cronParts = syncSchedule.split(/\s+/);
if (cronParts.length !== 5) {
showToast('Invalid cron format. Expected: minute hour day month dayofweek', 'error');
return;
}
// Get Spotify ID based on current mode // Get Spotify ID based on current mode
let spotifyId = ''; let spotifyId = '';
@@ -2119,7 +2143,11 @@
const res = await fetch(`/api/admin/jellyfin/playlists/${encodeURIComponent(jellyfinId)}/link`, { const res = await fetch(`/api/admin/jellyfin/playlists/${encodeURIComponent(jellyfinId)}/link`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, spotifyPlaylistId: cleanSpotifyId }) body: JSON.stringify({
name,
spotifyPlaylistId: cleanSpotifyId,
syncSchedule: syncSchedule
})
}); });
const data = await res.json(); const data = await res.json();