diff --git a/.env.example b/.env.example
index 6652c0e..de90e3a 100644
--- a/.env.example
+++ b/.env.example
@@ -116,18 +116,6 @@ CACHE_DURATION_HOURS=1
# Enable Spotify playlist injection (optional, default: false)
SPOTIFY_IMPORT_ENABLED=false
-# Sync schedule: When does the Spotify Import plugin run?
-# Used for the sync window check to avoid fetching too frequently
-# Set these to match your plugin's sync schedule in Jellyfin
-# Example: If plugin runs daily at 4:15 PM, set HOUR=16 and MINUTE=15
-SPOTIFY_IMPORT_SYNC_START_HOUR=16
-SPOTIFY_IMPORT_SYNC_START_MINUTE=15
-
-# Sync window: How long to search for missing tracks files (in hours)
-# The fetcher will check within this window after the sync start time
-# Example: If plugin runs at 4:15 PM and window is 2 hours, checks from 4:15 PM to 6:15 PM
-SPOTIFY_IMPORT_SYNC_WINDOW_HOURS=2
-
# Matching interval: How often to run track matching (in hours)
# Spotify playlists like Discover Weekly update once per week, Release Radar updates weekly
# Set to 0 to only run once on startup (manual trigger via admin UI still works)
diff --git a/.gitignore b/.gitignore
index 35d6d67..89a880d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -103,4 +103,7 @@ apis/api-calls/endpoint-usage.json
originals/
# Sample missing playlists for Spotify integration testing
-sampleMissingPlaylists/
\ No newline at end of file
+sampleMissingPlaylists/
+
+# Migration guide (local only)
+MIGRATION.md
\ No newline at end of file
diff --git a/MIGRATION.md b/MIGRATION.md
deleted file mode 100644
index 1038732..0000000
--- a/MIGRATION.md
+++ /dev/null
@@ -1,157 +0,0 @@
-# Migration Guide: Reorganizing Download Structure
-
-This guide is for upgrading from the old download structure to the new unified structure.
-
-## Old Structure
-```
-./downloads/ # Permanent downloads
-./kept/ # Favorited tracks
-./cache/ # Cached tracks
-```
-
-## New Structure
-```
-./downloads/
-├── permanent/ # Permanent downloads
-├── kept/ # Favorited tracks
-└── cache/ # Cached tracks
-```
-
-## Migration Steps
-
-### 1. Stop the Container
-```bash
-docker-compose down
-```
-
-### 2. Backup Your Data (Recommended)
-```bash
-# Create a backup
-tar -czf allstarr-backup-$(date +%Y%m%d).tar.gz downloads/ kept/ cache/ 2>/dev/null
-```
-
-### 3. Create New Directory Structure
-```bash
-mkdir -p downloads/permanent downloads/kept downloads/cache
-```
-
-### 4. Move Existing Files
-
-**Move permanent downloads:**
-```bash
-# If you have files directly in downloads/
-if [ -d "downloads" ] && [ ! -d "downloads/permanent" ]; then
- # Move all files/folders except the new subdirectories
- find downloads/ -maxdepth 1 -mindepth 1 ! -name 'permanent' ! -name 'kept' ! -name 'cache' ! -name 'playlists' -exec mv {} downloads/permanent/ \;
-fi
-
-# Move playlists folder if it exists
-if [ -d "downloads/playlists" ]; then
- mv downloads/playlists downloads/permanent/
-fi
-```
-
-**Move kept files:**
-```bash
-if [ -d "kept" ]; then
- mv kept/* downloads/kept/ 2>/dev/null || true
- rmdir kept
-fi
-```
-
-**Move cache files:**
-```bash
-if [ -d "cache" ]; then
- mv cache/* downloads/cache/ 2>/dev/null || true
- rmdir cache
-fi
-```
-
-### 5. Update .env File
-```bash
-# Remove old variables
-sed -i.bak '/^KEPT_PATH=/d' .env
-sed -i.bak '/^CACHE_PATH=/d' .env
-
-# Ensure DOWNLOAD_PATH is set correctly
-if ! grep -q "^DOWNLOAD_PATH=" .env; then
- echo "DOWNLOAD_PATH=./downloads" >> .env
-else
- sed -i.bak 's|^DOWNLOAD_PATH=.*|DOWNLOAD_PATH=./downloads|' .env
-fi
-```
-
-### 6. Update Media Server Library Paths
-
-**For Jellyfin:**
-1. Go to Dashboard → Libraries
-2. Edit your Music library
-3. Update the folder path from `downloads` to `downloads/permanent`
-4. Scan library
-
-**For Navidrome/Subsonic:**
-1. Update your music folder configuration
-2. Change from `downloads` to `downloads/permanent`
-3. Restart and rescan
-
-### 7. Verify Migration
-```bash
-# Check the new structure
-ls -la downloads/
-ls -la downloads/permanent/
-ls -la downloads/kept/
-ls -la downloads/cache/
-
-# Count files in each directory
-echo "Permanent: $(find downloads/permanent -type f | wc -l) files"
-echo "Kept: $(find downloads/kept -type f | wc -l) files"
-echo "Cache: $(find downloads/cache -type f | wc -l) files"
-```
-
-### 8. Start the Container
-```bash
-docker-compose up -d
-```
-
-### 9. Check Logs
-```bash
-docker-compose logs -f allstarr
-```
-
-## Rollback (If Needed)
-
-If something goes wrong:
-
-```bash
-# Stop container
-docker-compose down
-
-# Restore from backup
-tar -xzf allstarr-backup-YYYYMMDD.tar.gz
-
-# Restore old .env
-mv .env.bak .env
-
-# Start container
-docker-compose up -d
-```
-
-## Verification Checklist
-
-- [ ] All files moved to new structure
-- [ ] Old directories removed or empty
-- [ ] .env file updated
-- [ ] Media server library paths updated
-- [ ] Container starts without errors
-- [ ] Can play existing tracks
-- [ ] New downloads go to correct folders
-- [ ] Favoriting external tracks works
-- [ ] Cache cleanup works
-
-## Notes
-
-- The migration preserves all your existing files
-- Playlists (.m3u files) are moved to `downloads/permanent/playlists/`
-- Relative paths in M3U files should still work
-- If you have a lot of files, the migration may take a few minutes
-- The backup is optional but highly recommended
diff --git a/README.md b/README.md
index 385630f..82f82ca 100644
--- a/README.md
+++ b/README.md
@@ -360,9 +360,6 @@ Allstarr automatically fills your Spotify playlists (like Release Radar and Disc
| Setting | Description |
|---------|-------------|
| `SpotifyImport:Enabled` | Enable Spotify playlist injection (default: `false`) |
-| `SpotifyImport:SyncStartHour` | Hour when plugin runs (24-hour format, 0-23) - used for sync window check |
-| `SpotifyImport:SyncStartMinute` | Minute when plugin runs (0-59) - used for sync window check |
-| `SpotifyImport:SyncWindowHours` | Hours to check for missing tracks files after sync time (default: 2) |
| `SpotifyImport:MatchingIntervalHours` | How often to run track matching in hours (default: 24, set to 0 for startup only) |
| `SpotifyImport:Playlists` | JSON array of playlists (managed via Web UI) |
@@ -371,11 +368,6 @@ Allstarr automatically fills your Spotify playlists (like Release Radar and Disc
# Enable the feature
SPOTIFY_IMPORT_ENABLED=true
-# Sync window settings (used to avoid fetching too frequently)
-SPOTIFY_IMPORT_SYNC_START_HOUR=16
-SPOTIFY_IMPORT_SYNC_START_MINUTE=15
-SPOTIFY_IMPORT_SYNC_WINDOW_HOURS=2
-
# Matching interval (24 hours = once per day)
SPOTIFY_IMPORT_MATCHING_INTERVAL_HOURS=24
diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs
index 4873b0b..e1d7b1c 100644
--- a/allstarr/Controllers/AdminController.cs
+++ b/allstarr/Controllers/AdminController.cs
@@ -166,8 +166,7 @@ public class AdminController : ControllerBase
spotifyImport = new
{
enabled = _spotifyImportSettings.Enabled,
- syncTime = $"{_spotifyImportSettings.SyncStartHour:D2}:{_spotifyImportSettings.SyncStartMinute:D2}",
- syncWindowHours = _spotifyImportSettings.SyncWindowHours,
+ matchingIntervalHours = _spotifyImportSettings.MatchingIntervalHours,
playlistCount = _spotifyImportSettings.Playlists.Count
},
deezer = new
@@ -1392,9 +1391,7 @@ public class AdminController : ControllerBase
spotifyImport = new
{
enabled = _spotifyImportSettings.Enabled,
- syncStartHour = _spotifyImportSettings.SyncStartHour,
- syncStartMinute = _spotifyImportSettings.SyncStartMinute,
- syncWindowHours = _spotifyImportSettings.SyncWindowHours,
+ matchingIntervalHours = _spotifyImportSettings.MatchingIntervalHours,
playlists = _spotifyImportSettings.Playlists.Select(p => new
{
name = p.Name,
diff --git a/allstarr/Models/Settings/SpotifyImportSettings.cs b/allstarr/Models/Settings/SpotifyImportSettings.cs
index 4296141..aa810c0 100644
--- a/allstarr/Models/Settings/SpotifyImportSettings.cs
+++ b/allstarr/Models/Settings/SpotifyImportSettings.cs
@@ -59,27 +59,6 @@ public class SpotifyImportSettings
///
public bool Enabled { get; set; }
- ///
- /// Hour when Spotify Import plugin runs (24-hour format, 0-23)
- /// NOTE: This setting is now optional and only used for the sync window check.
- /// The fetcher will search backwards from current time for the last 48 hours,
- /// so timezone confusion is avoided.
- ///
- public int SyncStartHour { get; set; } = 16;
-
- ///
- /// Minute when Spotify Import plugin runs (0-59)
- /// NOTE: This setting is now optional and only used for the sync window check.
- ///
- public int SyncStartMinute { get; set; } = 15;
-
- ///
- /// How many hours to search for missing tracks files after sync start time
- /// This prevents the fetcher from running too frequently.
- /// Set to 0 to disable the sync window check and always search on startup.
- ///
- public int SyncWindowHours { get; set; } = 2;
-
///
/// How often to run track matching in hours.
/// Spotify playlists like Discover Weekly update once per week, Release Radar updates weekly.
diff --git a/allstarr/Program.cs b/allstarr/Program.cs
index cb8cc2e..1314dd0 100644
--- a/allstarr/Program.cs
+++ b/allstarr/Program.cs
@@ -359,7 +359,7 @@ builder.Services.Configure(options =>
}
// Log configuration at startup
- Console.WriteLine($"Spotify Import: Enabled={options.Enabled}, SyncHour={options.SyncStartHour}:{options.SyncStartMinute:D2}, WindowHours={options.SyncWindowHours}");
+ Console.WriteLine($"Spotify Import: Enabled={options.Enabled}, MatchingInterval={options.MatchingIntervalHours}h");
Console.WriteLine($"Spotify Import Playlists: {options.Playlists.Count} configured");
foreach (var playlist in options.Playlists)
{
diff --git a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs
index 08bb4d7..ef4d785 100644
--- a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs
+++ b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs
@@ -44,7 +44,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
public async Task TriggerFetchAsync()
{
_logger.LogInformation("Manual fetch triggered");
- await FetchMissingTracksAsync(CancellationToken.None, bypassSyncWindowCheck: true);
+ await FetchMissingTracksAsync(CancellationToken.None);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -84,20 +84,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogInformation("Spotify Import ENABLED");
_logger.LogInformation("Configured Playlists: {Count}", _spotifySettings.Value.Playlists.Count);
-
- // Log the search schedule
- var settings = _spotifySettings.Value;
- var syncTime = DateTime.Today
- .AddHours(settings.SyncStartHour)
- .AddMinutes(settings.SyncStartMinute);
- var syncEndTime = syncTime.AddHours(settings.SyncWindowHours);
-
- _logger.LogInformation("Search Schedule:");
- _logger.LogInformation(" Plugin sync time: {Time:HH:mm} UTC (configured)", syncTime);
- _logger.LogInformation(" Search window: {Start:HH:mm} - {End:HH:mm} UTC ({Hours}h window)",
- syncTime, syncEndTime, settings.SyncWindowHours);
- _logger.LogInformation(" Will search for new files once per day after sync window ends");
- _logger.LogInformation(" Background check interval: 5 minutes");
+ _logger.LogInformation("Background check interval: 5 minutes");
// Fetch playlist names from Jellyfin
await LoadPlaylistNamesAsync();
@@ -109,7 +96,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
}
_logger.LogInformation("========================================");
- // Check if we should run on startup
+ // Run on startup if we don't have cache
if (!_hasRunOnce)
{
var shouldRun = await ShouldRunOnStartupAsync();
@@ -118,7 +105,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService
_logger.LogInformation("Running initial fetch on startup");
try
{
- await FetchMissingTracksAsync(stoppingToken, bypassSyncWindowCheck: true);
+ await FetchMissingTracksAsync(stoppingToken);
_hasRunOnce = true;
}
catch (Exception ex)
@@ -128,21 +115,20 @@ public class SpotifyMissingTracksFetcher : BackgroundService
}
else
{
- _logger.LogInformation("Skipping startup fetch - already have current files");
+ _logger.LogInformation("Skipping startup fetch - already have cached files");
_hasRunOnce = true;
}
}
+ // Background loop - check for new files every 5 minutes
while (!stoppingToken.IsCancellationRequested)
{
try
{
- // Only fetch if we're past today's sync window AND we haven't fetched today yet
var shouldFetch = await ShouldFetchNowAsync();
if (shouldFetch)
{
await FetchMissingTracksAsync(stoppingToken);
- _hasRunOnce = true;
}
}
catch (Exception ex)
@@ -156,42 +142,29 @@ public class SpotifyMissingTracksFetcher : BackgroundService
private async Task ShouldFetchNowAsync()
{
- var settings = _spotifySettings.Value;
+ // Check if we have recent cache files (within last 24 hours)
var now = DateTime.UtcNow;
+ var cacheThreshold = now.AddHours(-24);
- // Calculate today's sync window
- var todaySync = now.Date
- .AddHours(settings.SyncStartHour)
- .AddMinutes(settings.SyncStartMinute);
- var todaySyncEnd = todaySync.AddHours(settings.SyncWindowHours);
-
- // Only fetch if we're past today's sync window
- if (now < todaySyncEnd)
- {
- return false;
- }
-
- // Check if we already have today's files
foreach (var playlistName in _playlistIdToName.Values)
{
var filePath = GetCacheFilePath(playlistName);
- if (File.Exists(filePath))
+ if (!File.Exists(filePath))
{
- var fileTime = File.GetLastWriteTimeUtc(filePath);
-
- // If file is from today's sync or later, we already have it
- if (fileTime >= todaySync)
- {
- continue;
- }
+ // Missing cache file for this playlist
+ return true;
}
- // Missing today's file for this playlist
- return true;
+ var fileTime = File.GetLastWriteTimeUtc(filePath);
+ if (fileTime < cacheThreshold)
+ {
+ // Cache file is older than 24 hours
+ return true;
+ }
}
- // All playlists have today's files
+ // All playlists have recent cache files
return false;
}
@@ -210,120 +183,43 @@ public class SpotifyMissingTracksFetcher : BackgroundService
{
_logger.LogInformation("=== STARTUP CACHE CHECK ===");
- var settings = _spotifySettings.Value;
- var now = DateTime.UtcNow;
+ var allPlaylistsHaveCache = true;
- // Calculate today's sync window
- var todaySync = now.Date
- .AddHours(settings.SyncStartHour)
- .AddMinutes(settings.SyncStartMinute);
- var todaySyncEnd = todaySync.AddHours(settings.SyncWindowHours);
-
- _logger.LogInformation("Today's sync window: {Start:yyyy-MM-dd HH:mm} - {End:yyyy-MM-dd HH:mm} UTC",
- todaySync, todaySyncEnd);
- _logger.LogInformation("Current time: {Now:yyyy-MM-dd HH:mm} UTC", now);
-
- // If we're still before today's sync window end, we should have yesterday's or today's file
- // Don't search again until after today's sync window ends
- if (now < todaySyncEnd)
+ foreach (var playlistName in _playlistIdToName.Values)
{
- _logger.LogInformation("We're before today's sync window end - checking if we have recent cache...");
+ var filePath = GetCacheFilePath(playlistName);
+ var cacheKey = $"spotify:missing:{playlistName}";
- var allPlaylistsHaveCache = true;
-
- foreach (var playlistName in _playlistIdToName.Values)
+ // Check file cache
+ if (File.Exists(filePath))
{
- var filePath = GetCacheFilePath(playlistName);
- var cacheKey = $"spotify:missing:{playlistName}";
+ var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath);
+ _logger.LogInformation(" {Playlist}: Found file cache (age: {Age:F1}h)", playlistName, fileAge.TotalHours);
- // Check file cache
- if (File.Exists(filePath))
+ // Load into Redis if not already there
+ if (!await _cache.ExistsAsync(cacheKey))
{
- var fileAge = DateTime.UtcNow - File.GetLastWriteTimeUtc(filePath);
- _logger.LogInformation(" {Playlist}: Found file cache (age: {Age:F1}h)", playlistName, fileAge.TotalHours);
-
- // Load into Redis if not already there
- if (!await _cache.ExistsAsync(cacheKey))
- {
- await LoadFromFileCache(playlistName);
- }
- continue;
+ await LoadFromFileCache(playlistName);
}
-
- // Check Redis cache
- if (await _cache.ExistsAsync(cacheKey))
- {
- _logger.LogInformation(" {Playlist}: Found in Redis cache", playlistName);
- continue;
- }
-
- // No cache found for this playlist
- _logger.LogInformation(" {Playlist}: No cache found", playlistName);
- allPlaylistsHaveCache = false;
+ continue;
}
- if (allPlaylistsHaveCache)
+ // Check Redis cache
+ if (await _cache.ExistsAsync(cacheKey))
{
- _logger.LogInformation("=== ALL PLAYLISTS HAVE CACHE - SKIPPING STARTUP FETCH ===");
- _logger.LogInformation("Will search again after {Time:yyyy-MM-dd HH:mm} UTC", todaySyncEnd);
- return false;
+ _logger.LogInformation(" {Playlist}: Found in Redis cache", playlistName);
+ continue;
}
+
+ // No cache found for this playlist
+ _logger.LogInformation(" {Playlist}: No cache found", playlistName);
+ allPlaylistsHaveCache = false;
}
- // If we're after today's sync window end, check if we already have today's file
- if (now >= todaySyncEnd)
+ if (allPlaylistsHaveCache)
{
- _logger.LogInformation("We're after today's sync window end - checking if we already fetched today's files...");
-
- var allPlaylistsHaveTodaysFile = true;
-
- foreach (var playlistName in _playlistIdToName.Values)
- {
- var filePath = GetCacheFilePath(playlistName);
- var cacheKey = $"spotify:missing:{playlistName}";
-
- // Check if file exists and was created today (after sync start)
- if (File.Exists(filePath))
- {
- var fileTime = File.GetLastWriteTimeUtc(filePath);
-
- // File should be from today's sync window or later
- if (fileTime >= todaySync)
- {
- var fileAge = DateTime.UtcNow - fileTime;
- _logger.LogInformation(" {Playlist}: Have today's file (created {Time:yyyy-MM-dd HH:mm}, age: {Age:F1}h)",
- playlistName, fileTime, fileAge.TotalHours);
-
- // Load into Redis if not already there
- if (!await _cache.ExistsAsync(cacheKey))
- {
- await LoadFromFileCache(playlistName);
- }
- continue;
- }
- else
- {
- _logger.LogInformation(" {Playlist}: File is old (from {Time:yyyy-MM-dd HH:mm}, before today's sync)",
- playlistName, fileTime);
- }
- }
- else
- {
- _logger.LogInformation(" {Playlist}: No file found", playlistName);
- }
-
- allPlaylistsHaveTodaysFile = false;
- }
-
- if (allPlaylistsHaveTodaysFile)
- {
- _logger.LogInformation("=== ALL PLAYLISTS HAVE TODAY'S FILES - SKIPPING STARTUP FETCH ===");
-
- // Calculate when to search next (tomorrow after sync window)
- var tomorrowSyncEnd = todaySyncEnd.AddDays(1);
- _logger.LogInformation("Will search again after {Time:yyyy-MM-dd HH:mm} UTC", tomorrowSyncEnd);
- return false;
- }
+ _logger.LogInformation("=== ALL PLAYLISTS HAVE CACHE - SKIPPING STARTUP FETCH ===");
+ return false;
}
_logger.LogInformation("=== WILL FETCH ON STARTUP ===");
@@ -380,32 +276,9 @@ public class SpotifyMissingTracksFetcher : BackgroundService
}
}
- private async Task FetchMissingTracksAsync(CancellationToken cancellationToken, bool bypassSyncWindowCheck = false)
+ private async Task FetchMissingTracksAsync(CancellationToken cancellationToken)
{
- var settings = _spotifySettings.Value;
- var now = DateTime.UtcNow;
- var syncStart = now.Date
- .AddHours(settings.SyncStartHour)
- .AddMinutes(settings.SyncStartMinute);
- var syncEnd = syncStart.AddHours(settings.SyncWindowHours);
-
- // Only run after the sync window has passed (unless bypassing for startup)
- if (!bypassSyncWindowCheck && now < syncEnd)
- {
- _logger.LogInformation("Skipping fetch - sync window not passed yet (now: {Now}, window ends: {End})",
- now, syncEnd);
- return;
- }
-
- if (bypassSyncWindowCheck)
- {
- _logger.LogInformation("=== FETCHING MISSING TRACKS (STARTUP MODE) ===");
- }
- else
- {
- _logger.LogInformation("=== FETCHING MISSING TRACKS (SYNC WINDOW PASSED) ===");
- }
-
+ _logger.LogInformation("=== FETCHING MISSING TRACKS ===");
_logger.LogInformation("Processing {Count} playlists", _playlistIdToName.Count);
// Track when we find files to optimize search for other playlists