diff --git a/.env.example b/.env.example index f8f4992..5953953 100644 --- a/.env.example +++ b/.env.example @@ -98,9 +98,9 @@ CACHE_DURATION_HOURS=1 # ===== SPOTIFY PLAYLIST INJECTION (JELLYFIN ONLY) ===== # REQUIRES: Jellyfin Spotify Import Plugin (https://github.com/Viperinius/jellyfin-plugin-spotify-import) -# This feature injects virtual Spotify playlists (Release Radar, Discover Weekly) into Jellyfin +# This feature intercepts Spotify Import plugin playlists (Release Radar, Discover Weekly) and fills them # with tracks auto-matched from external providers (SquidWTF, Deezer, Qobuz) -# Uses JELLYFIN_URL and JELLYFIN_API_KEY for API access +# Uses JELLYFIN_URL and JELLYFIN_API_KEY configured above (no separate credentials needed) # Enable Spotify playlist injection (optional, default: false) SPOTIFY_IMPORT_ENABLED=false diff --git a/README.md b/README.md index f8a3c66..b3432b7 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ This project brings together all the music streaming providers into one unified - **Artist Deduplication**: Merges local and streaming artists to avoid duplicates - **Album Enrichment**: Adds missing tracks to local albums from streaming providers - **Cover Art Proxy**: Serves cover art for external content -- **Spotify Playlist Injection** (Jellyfin only): Injects virtual Spotify playlists (Release Radar, Discover Weekly) with tracks auto-matched from streaming providers +- **Spotify Playlist Injection** (Jellyfin only): Intercepts Spotify Import plugin playlists (Release Radar, Discover Weekly) and fills them with tracks auto-matched from streaming providers ## Supported Backends @@ -290,7 +290,7 @@ Subsonic__EnableExternalPlaylists=false ### Spotify Playlist Injection (Jellyfin Only) -Allstarr can inject virtual Spotify playlists (Release Radar, Discover Weekly) into Jellyfin with tracks automatically matched from your configured streaming provider. +Allstarr can intercept Spotify Import plugin playlists (Release Radar, Discover Weekly) and fill them with tracks automatically matched from your configured streaming provider (SquidWTF, Deezer, or Qobuz). **Requirements:** - [Jellyfin Spotify Import Plugin](https://github.com/Viperinius/jellyfin-plugin-spotify-import) installed and configured @@ -308,11 +308,12 @@ Allstarr can inject virtual Spotify playlists (Release Radar, Discover Weekly) i | `SpotifyImport:Playlists` | Array of playlists to inject (Name, SpotifyName, Enabled) | **How it works:** -1. Jellyfin Spotify Import plugin runs daily and creates missing tracks files for playlists -2. Allstarr fetches these files within the configured time window +1. Jellyfin Spotify Import plugin runs daily and creates playlists + missing tracks files +2. Allstarr fetches these missing tracks files within the configured time window 3. For each missing track, Allstarr searches your streaming provider (SquidWTF, Deezer, or Qobuz) -4. Virtual playlists appear in Jellyfin with matched tracks ready to stream +4. When you open the playlist in Jellyfin, Allstarr intercepts the request and returns matched tracks 5. Tracks are downloaded on-demand when played +6. On startup, Allstarr will fetch missing tracks if it hasn't run in the last 24 hours **Environment variables:** ```bash diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs index 445f1d8..b4ef2f2 100644 --- a/allstarr/Controllers/JellyfinController.cs +++ b/allstarr/Controllers/JellyfinController.cs @@ -1210,6 +1210,17 @@ public class JellyfinController : ControllerBase #region Playlists + /// + /// Intercepts playlist items requests to inject Spotify playlists. + /// This route must have lower Order than the catch-all ProxyRequest route. + /// + [HttpGet("Playlists/{playlistId}/Items", Order = 5)] + public async Task GetPlaylistItems(string playlistId) + { + _logger.LogInformation("GetPlaylistItems called for playlist {PlaylistId}", playlistId); + return await GetPlaylistTracks(playlistId); + } + /// /// Gets playlist tracks displayed as an album. /// diff --git a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs index 0fb114a..893b883 100644 --- a/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs +++ b/allstarr/Services/Spotify/SpotifyMissingTracksFetcher.cs @@ -13,6 +13,7 @@ public class SpotifyMissingTracksFetcher : BackgroundService private readonly IHttpClientFactory _httpClientFactory; private readonly RedisCacheService _cache; private readonly ILogger _logger; + private bool _hasRunOnce = false; public SpotifyMissingTracksFetcher( IOptions spotifySettings, @@ -47,6 +48,30 @@ public class SpotifyMissingTracksFetcher : BackgroundService _logger.LogInformation("Spotify missing tracks fetcher started"); + // Run once on startup if we haven't run in the last 24 hours + if (!_hasRunOnce) + { + var shouldRunOnStartup = await ShouldRunOnStartupAsync(); + if (shouldRunOnStartup) + { + _logger.LogInformation("Running initial fetch on startup"); + try + { + await FetchMissingTracksAsync(stoppingToken); + _hasRunOnce = true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during startup fetch"); + } + } + else + { + _logger.LogInformation("Skipping startup fetch - already ran within last 24 hours"); + _hasRunOnce = true; + } + } + while (!stoppingToken.IsCancellationRequested) { try @@ -62,6 +87,20 @@ public class SpotifyMissingTracksFetcher : BackgroundService } } + private async Task ShouldRunOnStartupAsync() + { + // Check if any playlist has cached data from the last 24 hours + foreach (var playlist in _spotifySettings.Value.Playlists.Where(p => p.Enabled)) + { + var cacheKey = $"spotify:missing:{playlist.SpotifyName}"; + if (await _cache.ExistsAsync(cacheKey)) + { + return false; // Already have recent data + } + } + return true; // No recent data, should fetch + } + private async Task FetchMissingTracksAsync(CancellationToken cancellationToken) { var settings = _spotifySettings.Value;