mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 07:58:39 -05:00
fix: accurate playlist counting and three-color progress bars
- Fix playlist counting logic to use fuzzy matching (same as track view) - Count local tracks by matching Jellyfin tracks to Spotify tracks - Count external matched tracks from cache - Count missing tracks (not found locally or externally) - Progress bars now show three colors: * Green: Local tracks in Jellyfin * Orange: External matched tracks (SquidWTF/Deezer/Qobuz) * Grey: Missing tracks (not found anywhere) - Add 'Missing' badge to tracks that couldn't be found - Missing tracks can still be manually mapped - Fixes incorrect counts like '28 matched • 1 missing' showing 29 external tracks All 225 tests pass.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using allstarr.Models.Settings;
|
||||
using allstarr.Models.Spotify;
|
||||
using allstarr.Services.Spotify;
|
||||
using allstarr.Services.Jellyfin;
|
||||
using allstarr.Services.Common;
|
||||
@@ -246,58 +247,96 @@ public class AdminController : ControllerBase
|
||||
|
||||
if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items))
|
||||
{
|
||||
var localCount = 0;
|
||||
var externalMatchedCount = 0;
|
||||
|
||||
// Count tracks in Jellyfin playlist
|
||||
var jellyfinTrackCount = items.GetArrayLength();
|
||||
|
||||
// Count local vs external tracks
|
||||
// Build list of local tracks from Jellyfin (match by name only)
|
||||
var localTracks = new List<(string Title, string Artist)>();
|
||||
foreach (var item in items.EnumerateArray())
|
||||
{
|
||||
// Check if it's an external track by looking at the ID format
|
||||
// External tracks have IDs like "deezer:123456" or "qobuz:123456"
|
||||
var isExternal = false;
|
||||
if (item.TryGetProperty("Id", out var idProp))
|
||||
var title = item.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() ?? "" : "";
|
||||
var artist = "";
|
||||
|
||||
if (item.TryGetProperty("Artists", out var artistsEl) && artistsEl.GetArrayLength() > 0)
|
||||
{
|
||||
var id = idProp.GetString() ?? "";
|
||||
isExternal = id.Contains(":"); // External IDs contain provider prefix
|
||||
artist = artistsEl[0].GetString() ?? "";
|
||||
}
|
||||
else if (item.TryGetProperty("AlbumArtist", out var albumArtistEl))
|
||||
{
|
||||
artist = albumArtistEl.GetString() ?? "";
|
||||
}
|
||||
|
||||
if (isExternal)
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
externalMatchedCount++;
|
||||
localTracks.Add((title, artist));
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
// Get Spotify tracks to match against
|
||||
var spotifyTracks = await _playlistFetcher.GetPlaylistTracksAsync(config.Name);
|
||||
|
||||
// Get matched external tracks cache once
|
||||
var matchedTracksKey = $"spotify:matched:ordered:{config.Name}";
|
||||
var matchedTracks = await _cache.GetAsync<List<MatchedTrack>>(matchedTracksKey);
|
||||
var matchedSpotifyIds = new HashSet<string>(
|
||||
matchedTracks?.Select(m => m.SpotifyId) ?? Enumerable.Empty<string>()
|
||||
);
|
||||
|
||||
var localCount = 0;
|
||||
var externalMatchedCount = 0;
|
||||
var externalMissingCount = 0;
|
||||
|
||||
// Match each Spotify track to determine if it's local, external, or missing
|
||||
foreach (var track in spotifyTracks)
|
||||
{
|
||||
var isLocal = false;
|
||||
|
||||
if (localTracks.Count > 0)
|
||||
{
|
||||
var bestMatch = localTracks
|
||||
.Select(local => new
|
||||
{
|
||||
Local = local,
|
||||
TitleScore = FuzzyMatcher.CalculateSimilarity(track.Title, local.Title),
|
||||
ArtistScore = FuzzyMatcher.CalculateSimilarity(track.PrimaryArtist, local.Artist)
|
||||
})
|
||||
.Select(x => new
|
||||
{
|
||||
x.Local,
|
||||
x.TitleScore,
|
||||
x.ArtistScore,
|
||||
TotalScore = (x.TitleScore * 0.7) + (x.ArtistScore * 0.3)
|
||||
})
|
||||
.OrderByDescending(x => x.TotalScore)
|
||||
.FirstOrDefault();
|
||||
|
||||
// Use 70% threshold (same as playback matching)
|
||||
if (bestMatch != null && bestMatch.TotalScore >= 70)
|
||||
{
|
||||
isLocal = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLocal)
|
||||
{
|
||||
localCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check matched tracks cache to get accurate external count
|
||||
// External tracks are injected on-the-fly, not stored in Jellyfin
|
||||
var matchedTracksKey = $"spotify:matched:ordered:{config.Name}";
|
||||
var matchedTracks = await _cache.GetAsync<List<object>>(matchedTracksKey);
|
||||
|
||||
if (matchedTracks != null && matchedTracks.Count > 0)
|
||||
{
|
||||
// Count how many matched tracks are external (not local)
|
||||
// Assume tracks beyond local count are external
|
||||
var totalMatched = matchedTracks.Count;
|
||||
if (totalMatched > localCount)
|
||||
else
|
||||
{
|
||||
externalMatchedCount = totalMatched - localCount;
|
||||
// Check if external track is matched
|
||||
if (matchedSpotifyIds.Contains(track.SpotifyId))
|
||||
{
|
||||
externalMatchedCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
externalMissingCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalInJellyfin = localCount + externalMatchedCount;
|
||||
var externalMissingCount = Math.Max(0, spotifyTrackCount - totalInJellyfin);
|
||||
|
||||
playlistInfo["localTracks"] = localCount;
|
||||
playlistInfo["externalMatched"] = externalMatchedCount;
|
||||
playlistInfo["externalMissing"] = externalMissingCount;
|
||||
playlistInfo["externalTotal"] = externalMatchedCount + externalMissingCount;
|
||||
playlistInfo["totalInJellyfin"] = totalInJellyfin;
|
||||
playlistInfo["totalInJellyfin"] = localCount + externalMatchedCount;
|
||||
|
||||
_logger.LogDebug("Playlist {Name}: {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing",
|
||||
config.Name, spotifyTrackCount, localCount, externalMatchedCount, externalMissingCount);
|
||||
|
||||
Reference in New Issue
Block a user