mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Enhanced playlist statistics in admin dashboard
Backend changes: - Distinguish between local tracks (in Jellyfin library) and external tracks (downloaded) - Track external matched vs external missing counts - Calculate completion percentage for each playlist Frontend changes: - Show detailed breakdown: X local • Y matched • Z missing - Display completion percentage with progress bar - Color-coded stats (green=local, blue=matched, yellow=missing) - Updated table headers for clarity
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -658,8 +658,8 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Spotify ID</th>
|
||||
<th>Total</th>
|
||||
<th>Local/External</th>
|
||||
<th>Tracks</th>
|
||||
<th>Completion</th>
|
||||
<th>Cache Age</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
@@ -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 = `<span class="track-count">${spotifyTotal}</span>`;
|
||||
|
||||
// Show breakdown with color coding
|
||||
let breakdownParts = [];
|
||||
if (localCount > 0) {
|
||||
breakdownParts.push(`<span style="color:var(--success)">${localCount} local</span>`);
|
||||
}
|
||||
if (externalMatched > 0) {
|
||||
breakdownParts.push(`<span style="color:var(--accent)">${externalMatched} matched</span>`);
|
||||
}
|
||||
if (externalMissing > 0) {
|
||||
breakdownParts.push(`<span style="color:var(--warning)">${externalMissing} missing</span>`);
|
||||
}
|
||||
|
||||
const breakdown = breakdownParts.length > 0
|
||||
? `<br><small style="color:var(--text-secondary)">${breakdownParts.join(' • ')}</small>`
|
||||
: '';
|
||||
|
||||
// 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 `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(p.name)}</strong></td>
|
||||
<td style="font-family:monospace;font-size:0.85rem;color:var(--text-secondary);">${p.id || '-'}</td>
|
||||
<td class="track-count">${p.trackCount || 0}</td>
|
||||
<td class="track-count">${localExternal}</td>
|
||||
<td>${statsHtml}${breakdown}</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div style="flex:1;background:var(--bg-tertiary);height:6px;border-radius:3px;overflow:hidden;">
|
||||
<div style="width:${completionPct}%;height:100%;background:${completionColor};transition:width 0.3s;"></div>
|
||||
</div>
|
||||
<span style="font-size:0.85rem;color:${completionColor};font-weight:500;min-width:40px;">${completionPct}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="cache-age">${p.cacheAge || '-'}</td>
|
||||
<td>
|
||||
<button onclick="matchPlaylistTracks('${escapeJs(p.name)}')">Match Tracks</button>
|
||||
|
||||
Reference in New Issue
Block a user