Skip matching tracks already in Jellyfin playlist

- Fetch existing tracks from Jellyfin playlist before matching
- Extract Spotify IDs from ProviderIds to identify already-matched tracks
- Only search for tracks not already in Jellyfin
- Logs: 'Matching X/Y tracks (skipping Z already in Jellyfin)'
- Significantly reduces matching time for playlists with local content
- Example: If 20/50 tracks exist locally, only searches for 30 tracks
This commit is contained in:
2026-02-03 17:29:28 -05:00
parent e3546425eb
commit aa9b5c874d

View File

@@ -2,6 +2,7 @@ using allstarr.Models.Domain;
using allstarr.Models.Settings;
using allstarr.Models.Spotify;
using allstarr.Services.Common;
using allstarr.Services.Jellyfin;
using Microsoft.Extensions.Options;
using System.Text.Json;
@@ -146,6 +147,7 @@ public class SpotifyTrackMatchingService : BackgroundService
/// <summary>
/// New matching mode that uses ISRC when available for exact matches.
/// Preserves track position for correct playlist ordering.
/// Only matches tracks that aren't already in the Jellyfin playlist.
/// </summary>
private async Task MatchPlaylistTracksWithIsrcAsync(
string playlistName,
@@ -163,25 +165,84 @@ public class SpotifyTrackMatchingService : BackgroundService
return;
}
// Get the Jellyfin playlist ID to check which tracks already exist
var playlistConfig = _spotifySettings.Playlists
.FirstOrDefault(p => p.Name.Equals(playlistName, StringComparison.OrdinalIgnoreCase));
HashSet<string> existingSpotifyIds = new();
if (!string.IsNullOrEmpty(playlistConfig?.JellyfinId))
{
// Get existing tracks from Jellyfin playlist to avoid re-matching
using var scope = _serviceProvider.CreateScope();
var proxyService = scope.ServiceProvider.GetService<JellyfinProxyService>();
if (proxyService != null)
{
try
{
var (existingTracksResponse, _) = await proxyService.GetJsonAsync(
$"Playlists/{playlistConfig.JellyfinId}/Items",
null,
null);
if (existingTracksResponse != null &&
existingTracksResponse.RootElement.TryGetProperty("Items", out var items))
{
foreach (var item in items.EnumerateArray())
{
if (item.TryGetProperty("ProviderIds", out var providerIds) &&
providerIds.TryGetProperty("Spotify", out var spotifyId))
{
var id = spotifyId.GetString();
if (!string.IsNullOrEmpty(id))
{
existingSpotifyIds.Add(id);
}
}
}
_logger.LogInformation("Found {Count} tracks already in Jellyfin playlist {Playlist}, will skip matching these",
existingSpotifyIds.Count, playlistName);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not fetch existing Jellyfin tracks for {Playlist}, will match all tracks", playlistName);
}
}
}
// Filter to only tracks not already in Jellyfin
var tracksToMatch = spotifyTracks
.Where(t => !existingSpotifyIds.Contains(t.SpotifyId))
.ToList();
if (tracksToMatch.Count == 0)
{
_logger.LogInformation("All {Count} tracks for {Playlist} already exist in Jellyfin, skipping matching",
spotifyTracks.Count, playlistName);
return;
}
_logger.LogInformation("Matching {ToMatch}/{Total} tracks for {Playlist} (skipping {Existing} already in Jellyfin, ISRC: {IsrcEnabled})",
tracksToMatch.Count, spotifyTracks.Count, playlistName, existingSpotifyIds.Count, _spotifyApiSettings.PreferIsrcMatching);
// Check cache - use snapshot/timestamp to detect changes
var existingMatched = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey);
if (existingMatched != null && existingMatched.Count == spotifyTracks.Count)
if (existingMatched != null && existingMatched.Count >= tracksToMatch.Count)
{
_logger.LogInformation("Playlist {Playlist} already has {Count} matched tracks cached, skipping",
playlistName, existingMatched.Count);
return;
}
_logger.LogInformation("Matching {Count} tracks for {Playlist} (ISRC: {IsrcEnabled})",
spotifyTracks.Count, playlistName, _spotifyApiSettings.PreferIsrcMatching);
var matchedTracks = new List<MatchedTrack>();
var isrcMatches = 0;
var fuzzyMatches = 0;
var noMatch = 0;
// Process tracks in batches for parallel searching
var orderedTracks = spotifyTracks.OrderBy(t => t.Position).ToList();
var orderedTracks = tracksToMatch.OrderBy(t => t.Position).ToList();
for (int i = 0; i < orderedTracks.Count; i += BatchSize)
{
if (cancellationToken.IsCancellationRequested) break;
@@ -293,7 +354,7 @@ public class SpotifyTrackMatchingService : BackgroundService
_logger.LogInformation(
"✓ Cached {Matched}/{Total} tracks for {Playlist} (ISRC: {Isrc}, Fuzzy: {Fuzzy}, No match: {NoMatch})",
matchedTracks.Count, spotifyTracks.Count, playlistName, isrcMatches, fuzzyMatches, noMatch);
matchedTracks.Count, tracksToMatch.Count, playlistName, isrcMatches, fuzzyMatches, noMatch);
}
else
{