diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index cbf54b8..8b6c8d6 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -243,11 +243,49 @@ public class AdminController : ControllerBase if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items)) { - var localCount = items.GetArrayLength(); + var localCount = 0; + var externalMatchedCount = 0; + + // Count local vs external tracks + foreach (var item in items.EnumerateArray()) + { + // Check if track has a real file path (local) or is external + var hasPath = item.TryGetProperty("Path", out var pathProp) && + pathProp.ValueKind == JsonValueKind.String && + !string.IsNullOrEmpty(pathProp.GetString()); + + if (hasPath) + { + var pathStr = pathProp.GetString()!; + // Local tracks have filesystem paths starting with / or containing :\ + if (pathStr.StartsWith("/") || pathStr.Contains(":\\")) + { + localCount++; + } + else + { + // External track (downloaded from Deezer/Qobuz/etc) + externalMatchedCount++; + } + } + else + { + // No path means external + externalMatchedCount++; + } + } + + var totalInJellyfin = localCount + externalMatchedCount; + var externalMissingCount = Math.Max(0, spotifyTrackCount - totalInJellyfin); + playlistInfo["localTracks"] = localCount; - playlistInfo["externalTracks"] = Math.Max(0, spotifyTrackCount - localCount); - _logger.LogDebug("Playlist {Name}: {Local} local tracks, {Missing} missing", - config.Name, localCount, spotifyTrackCount - localCount); + playlistInfo["externalMatched"] = externalMatchedCount; + playlistInfo["externalMissing"] = externalMissingCount; + playlistInfo["externalTotal"] = externalMatchedCount + externalMissingCount; + playlistInfo["totalInJellyfin"] = totalInJellyfin; + + _logger.LogDebug("Playlist {Name}: {Total} Spotify tracks, {Local} local, {ExtMatched} external matched, {ExtMissing} external missing", + config.Name, spotifyTrackCount, localCount, externalMatchedCount, externalMissingCount); } else { diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 94b4a00..105f5a9 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -658,8 +658,8 @@ Name Spotify ID - Total - Local/External + Tracks + Completion Cache Age Actions @@ -1125,17 +1125,49 @@ } tbody.innerHTML = data.playlists.map(p => { - // Show local tracks and missing tracks + // Enhanced statistics display + const spotifyTotal = p.trackCount || 0; const localCount = p.localTracks || 0; - const missingCount = p.externalTracks || 0; - const localExternal = `${localCount} local / ${missingCount} missing`; + const externalMatched = p.externalMatched || 0; + const externalMissing = p.externalMissing || 0; + const totalInJellyfin = p.totalInJellyfin || 0; + + // Build detailed stats string + let statsHtml = `${spotifyTotal}`; + + // Show breakdown with color coding + let breakdownParts = []; + if (localCount > 0) { + breakdownParts.push(`${localCount} local`); + } + if (externalMatched > 0) { + breakdownParts.push(`${externalMatched} matched`); + } + if (externalMissing > 0) { + breakdownParts.push(`${externalMissing} missing`); + } + + const breakdown = breakdownParts.length > 0 + ? `
${breakdownParts.join(' • ')}` + : ''; + + // Calculate completion percentage + const completionPct = spotifyTotal > 0 ? Math.round((totalInJellyfin / spotifyTotal) * 100) : 0; + const completionColor = completionPct === 100 ? 'var(--success)' : completionPct >= 80 ? 'var(--accent)' : 'var(--warning)'; return ` ${escapeHtml(p.name)} ${p.id || '-'} - ${p.trackCount || 0} - ${localExternal} + ${statsHtml}${breakdown} + +
+
+
+
+ ${completionPct}% +
+ ${p.cacheAge || '-'}