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:
2026-02-03 19:49:32 -05:00
parent c1c2212b53
commit af03a53af5
2 changed files with 81 additions and 11 deletions

View File

@@ -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>