fix: progress bar and add missing tracks section

- Fix external track detection in progress bar (check for external provider names in ProviderIds)
- Add missing tracks section at bottom of Active Playlists tab
- Shows all unmatched tracks across all playlists
- Includes Map to Local and Map to External buttons for each missing track
- Auto-refreshes with other playlist data
This commit is contained in:
2026-02-06 22:12:15 -05:00
parent a6ac0dfbd2
commit ac1fbd4b34
2 changed files with 108 additions and 4 deletions

View File

@@ -374,12 +374,17 @@ public class AdminController : ControllerBase
foreach (var item in cachedPlaylistItems) foreach (var item in cachedPlaylistItems)
{ {
// Check if it's external by looking for ProviderIds (external songs have this) // Check if it's external by looking for external provider in ProviderIds
// External providers: SquidWTF, Deezer, Qobuz, Tidal
var isExternal = false; var isExternal = false;
if (item.TryGetValue("ProviderIds", out var providerIdsObj) && providerIdsObj != null) if (item.TryGetValue("ProviderIds", out var providerIdsObj) && providerIdsObj is Dictionary<string, string> providerIds)
{ {
// Has ProviderIds = external track // Check for external provider keys (not MusicBrainz, ISRC, etc)
isExternal = true; isExternal = providerIds.Keys.Any(k =>
k.Equals("SquidWTF", StringComparison.OrdinalIgnoreCase) ||
k.Equals("Deezer", StringComparison.OrdinalIgnoreCase) ||
k.Equals("Qobuz", StringComparison.OrdinalIgnoreCase) ||
k.Equals("Tidal", StringComparison.OrdinalIgnoreCase));
} }
if (isExternal) if (isExternal)

View File

@@ -718,6 +718,43 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Missing Tracks Section -->
<div class="card">
<h2>
Missing Tracks (All Playlists)
<div class="actions">
<button onclick="fetchMissingTracks()">Refresh</button>
</div>
</h2>
<p style="color: var(--text-secondary); margin-bottom: 12px;">
Tracks that couldn't be matched locally or externally. Map them manually to add them to your playlists.
</p>
<div id="missing-summary" style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
<div>
<span style="color: var(--text-secondary);">Total Missing:</span>
<span style="font-weight: 600; margin-left: 8px; color: var(--warning);" id="missing-total">0</span>
</div>
</div>
<table class="playlist-table">
<thead>
<tr>
<th>Playlist</th>
<th>Track</th>
<th>Artist</th>
<th>Album</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="missing-tracks-table-body">
<tr>
<td colspan="5" class="loading">
<span class="spinner"></span> Loading missing tracks...
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- Configuration Tab --> <!-- Configuration Tab -->
@@ -1429,6 +1466,66 @@
} }
} }
async function fetchMissingTracks() {
try {
const res = await fetch('/api/admin/playlists');
const data = await res.json();
const tbody = document.getElementById('missing-tracks-table-body');
const missingTracks = [];
// Collect all missing tracks from all playlists
for (const playlist of data.playlists) {
if (playlist.externalMissing > 0) {
// Fetch tracks for this playlist
try {
const tracksRes = await fetch(`/api/admin/playlists/${encodeURIComponent(playlist.name)}/tracks`);
const tracksData = await tracksRes.json();
// Filter to only missing tracks (isLocal === null)
const missing = tracksData.tracks.filter(t => t.isLocal === null);
missing.forEach(t => {
missingTracks.push({
playlist: playlist.name,
...t
});
});
} catch (err) {
console.error(`Failed to fetch tracks for ${playlist.name}:`, err);
}
}
}
// Update summary
document.getElementById('missing-total').textContent = missingTracks.length;
if (missingTracks.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-secondary);padding:40px;">🎉 No missing tracks! All tracks are matched.</td></tr>';
return;
}
tbody.innerHTML = missingTracks.map(t => {
return `
<tr>
<td><strong>${escapeHtml(t.playlist)}</strong></td>
<td>${escapeHtml(t.title)}</td>
<td>${escapeHtml(t.artist)}</td>
<td style="color:var(--text-secondary);">${t.album ? escapeHtml(t.album) : '-'}</td>
<td>
<button onclick="openMapToLocal('${escapeJs(t.playlist)}', '${escapeJs(t.spotifyId)}', '${escapeJs(t.title)}', '${escapeJs(t.artist)}')"
style="margin-right:4px;font-size:0.75rem;padding:4px 8px;background:var(--success);border-color:var(--success);">Map to Local</button>
<button onclick="openMapToExternal('${escapeJs(t.playlist)}', '${escapeJs(t.spotifyId)}', '${escapeJs(t.title)}', '${escapeJs(t.artist)}')"
style="font-size:0.75rem;padding:4px 8px;background:var(--warning);border-color:var(--warning);">Map to External</button>
</td>
</tr>
`;
}).join('');
} catch (error) {
console.error('Failed to fetch missing tracks:', error);
showToast('Failed to fetch missing tracks', 'error');
}
}
async function fetchConfig() { async function fetchConfig() {
try { try {
const res = await fetch('/api/admin/config'); const res = await fetch('/api/admin/config');
@@ -2536,6 +2633,7 @@
fetchStatus(); fetchStatus();
fetchPlaylists(); fetchPlaylists();
fetchTrackMappings(); fetchTrackMappings();
fetchMissingTracks();
fetchJellyfinUsers(); fetchJellyfinUsers();
fetchJellyfinPlaylists(); fetchJellyfinPlaylists();
fetchConfig(); fetchConfig();
@@ -2545,6 +2643,7 @@
fetchStatus(); fetchStatus();
fetchPlaylists(); fetchPlaylists();
fetchTrackMappings(); fetchTrackMappings();
fetchMissingTracks();
}, 30000); }, 30000);
</script> </script>
</body> </body>