Add Match Tracks button and Local/External column to Active Playlists

- Added 'Match Tracks' button to trigger matching for specific playlist
- Added Local/External column (shows '-' for now, will populate after matching)
- New endpoint: POST /api/admin/playlists/{name}/match
- Injects SpotifyTrackMatchingService into AdminController
- UI shows: Name | Spotify ID | Total | Local/External | Cache Age | Actions
- Allows users to manually trigger matching without waiting 30 minutes
This commit is contained in:
2026-02-03 17:47:44 -05:00
parent f813fe9eeb
commit 363c9e6f1b
2 changed files with 72 additions and 16 deletions

View File

@@ -657,14 +657,15 @@
<tr>
<th>Name</th>
<th>Spotify ID</th>
<th>Tracks</th>
<th>Total</th>
<th>Local/External</th>
<th>Cache Age</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="playlist-table-body">
<tr>
<td colspan="6" class="loading">
<td colspan="7" class="loading">
<span class="spinner"></span> Loading playlists...
</td>
</tr>
@@ -1061,22 +1062,32 @@
const tbody = document.getElementById('playlist-table-body');
if (data.playlists.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists configured. Link playlists from the Jellyfin Playlists tab.</td></tr>';
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--text-secondary);padding:40px;">No playlists configured. Link playlists from the Jellyfin Playlists tab.</td></tr>';
return;
}
tbody.innerHTML = data.playlists.map(p => `
<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="cache-age">${p.cacheAge || '-'}</td>
<td>
<button onclick="viewTracks('${escapeJs(p.name)}')">View</button>
<button class="danger" onclick="removePlaylist('${escapeJs(p.name)}')">Remove</button>
</td>
</tr>
`).join('');
tbody.innerHTML = data.playlists.map(p => {
// For now, we don't have local/external counts in the API response
// This will show "-" until we add that data
const localExternal = (p.localTracks !== undefined && p.externalTracks !== undefined)
? `${p.localTracks}/${p.externalTracks}`
: '-';
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 class="cache-age">${p.cacheAge || '-'}</td>
<td>
<button onclick="matchPlaylistTracks('${escapeJs(p.name)}')">Match Tracks</button>
<button onclick="viewTracks('${escapeJs(p.name)}')">View</button>
<button class="danger" onclick="removePlaylist('${escapeJs(p.name)}')">Remove</button>
</td>
</tr>
`;
}).join('');
} catch (error) {
console.error('Failed to fetch playlists:', error);
showToast('Failed to fetch playlists', 'error');
@@ -1321,6 +1332,22 @@
}
}
async function matchPlaylistTracks(name) {
try {
showToast(`Matching tracks for ${name}...`, 'success');
const res = await fetch(`/api/admin/playlists/${encodeURIComponent(name)}/match`, { method: 'POST' });
const data = await res.json();
if (res.ok) {
showToast(data.message, 'success');
} else {
showToast(data.error || 'Failed to match tracks', 'error');
}
} catch (error) {
showToast('Failed to match tracks', 'error');
}
}
async function clearCache() {
if (!confirm('Clear all cached playlist data?')) return;