mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
UI fixes: Match per playlist, Match All button, local/external labels, preserve tab on reload
This commit is contained in:
@@ -278,19 +278,95 @@ public class AdminController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get tracks for a specific playlist
|
/// Get tracks for a specific playlist with local/external status
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("playlists/{name}/tracks")]
|
[HttpGet("playlists/{name}/tracks")]
|
||||||
public async Task<IActionResult> GetPlaylistTracks(string name)
|
public async Task<IActionResult> GetPlaylistTracks(string name)
|
||||||
{
|
{
|
||||||
var decodedName = Uri.UnescapeDataString(name);
|
var decodedName = Uri.UnescapeDataString(name);
|
||||||
var tracks = await _playlistFetcher.GetPlaylistTracksAsync(decodedName);
|
|
||||||
|
|
||||||
|
// Get Spotify tracks
|
||||||
|
var spotifyTracks = await _playlistFetcher.GetPlaylistTracksAsync(decodedName);
|
||||||
|
|
||||||
|
// Get the playlist config to find Jellyfin ID
|
||||||
|
var playlistConfig = _spotifyImportSettings.Playlists
|
||||||
|
.FirstOrDefault(p => p.Name.Equals(decodedName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var tracksWithStatus = new List<object>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(playlistConfig?.JellyfinId))
|
||||||
|
{
|
||||||
|
// Get existing tracks from Jellyfin to determine local/external status
|
||||||
|
var userId = _jellyfinSettings.UserId;
|
||||||
|
if (!string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var url = $"{_jellyfinSettings.Url}/Playlists/{playlistConfig.JellyfinId}/Items?UserId={userId}";
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
|
||||||
|
|
||||||
|
var response = await _jellyfinHttpClient.SendAsync(request);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
using var doc = JsonDocument.Parse(json);
|
||||||
|
|
||||||
|
var localSpotifyIds = new HashSet<string>();
|
||||||
|
if (doc.RootElement.TryGetProperty("Items", out var items))
|
||||||
|
{
|
||||||
|
foreach (var item in items.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.TryGetProperty("ProviderIds", out var providerIds) &&
|
||||||
|
providerIds.TryGetProperty("Spotify", out var spotifyId))
|
||||||
|
{
|
||||||
|
var id = spotifyId.GetString();
|
||||||
|
if (!string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
localSpotifyIds.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark tracks as local or external
|
||||||
|
foreach (var track in spotifyTracks)
|
||||||
|
{
|
||||||
|
tracksWithStatus.Add(new
|
||||||
|
{
|
||||||
|
position = track.Position,
|
||||||
|
title = track.Title,
|
||||||
|
artists = track.Artists,
|
||||||
|
album = track.Album,
|
||||||
|
isrc = track.Isrc,
|
||||||
|
spotifyId = track.SpotifyId,
|
||||||
|
durationMs = track.DurationMs,
|
||||||
|
albumArtUrl = track.AlbumArtUrl,
|
||||||
|
isLocal = localSpotifyIds.Contains(track.SpotifyId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
name = decodedName,
|
||||||
|
trackCount = spotifyTracks.Count,
|
||||||
|
tracks = tracksWithStatus
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to get local track status for {Playlist}", decodedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: return tracks without local/external status
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
name = decodedName,
|
name = decodedName,
|
||||||
trackCount = tracks.Count,
|
trackCount = spotifyTracks.Count,
|
||||||
tracks = tracks.Select(t => new
|
tracks = spotifyTracks.Select(t => new
|
||||||
{
|
{
|
||||||
position = t.Position,
|
position = t.Position,
|
||||||
title = t.Title,
|
title = t.Title,
|
||||||
@@ -299,7 +375,8 @@ public class AdminController : ControllerBase
|
|||||||
isrc = t.Isrc,
|
isrc = t.Isrc,
|
||||||
spotifyId = t.SpotifyId,
|
spotifyId = t.SpotifyId,
|
||||||
durationMs = t.DurationMs,
|
durationMs = t.DurationMs,
|
||||||
albumArtUrl = t.AlbumArtUrl
|
albumArtUrl = t.AlbumArtUrl,
|
||||||
|
isLocal = (bool?)null // Unknown
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -911,16 +911,35 @@
|
|||||||
document.getElementById('restart-banner').classList.remove('active');
|
document.getElementById('restart-banner').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tab switching
|
// Tab switching with URL hash support
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
const tab = document.querySelector(`.tab[data-tab="${tabName}"]`);
|
||||||
|
const content = document.getElementById('tab-' + tabName);
|
||||||
|
|
||||||
|
if (tab && content) {
|
||||||
|
tab.classList.add('active');
|
||||||
|
content.classList.add('active');
|
||||||
|
window.location.hash = tabName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll('.tab').forEach(tab => {
|
document.querySelectorAll('.tab').forEach(tab => {
|
||||||
tab.addEventListener('click', () => {
|
tab.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
switchTab(tab.dataset.tab);
|
||||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
||||||
tab.classList.add('active');
|
|
||||||
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore tab from URL hash on page load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const hash = window.location.hash.substring(1);
|
||||||
|
if (hash) {
|
||||||
|
switchTab(hash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Toast notification
|
// Toast notification
|
||||||
function showToast(message, type = 'success') {
|
function showToast(message, type = 'success') {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
@@ -1345,7 +1364,9 @@
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showToast(data.message, 'success');
|
showToast(`✓ ${data.message}`, 'success');
|
||||||
|
// Refresh the playlists table after a delay to show updated counts
|
||||||
|
setTimeout(fetchPlaylists, 2000);
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Failed to match tracks', 'error');
|
showToast(data.error || 'Failed to match tracks', 'error');
|
||||||
}
|
}
|
||||||
@@ -1355,13 +1376,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function matchAllPlaylists() {
|
async function matchAllPlaylists() {
|
||||||
|
if (!confirm('Match tracks for ALL playlists? This may take a few minutes.')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast('Matching tracks for all playlists...', 'success');
|
showToast('Matching tracks for all playlists...', 'success');
|
||||||
const res = await fetch('/api/admin/playlists/match-all', { method: 'POST' });
|
const res = await fetch('/api/admin/playlists/match-all', { method: 'POST' });
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showToast(data.message, 'success');
|
showToast(`✓ ${data.message}`, 'success');
|
||||||
|
// Refresh the playlists table after a delay to show updated counts
|
||||||
|
setTimeout(fetchPlaylists, 3000);
|
||||||
} else {
|
} else {
|
||||||
showToast(data.error || 'Failed to match tracks', 'error');
|
showToast(data.error || 'Failed to match tracks', 'error');
|
||||||
}
|
}
|
||||||
@@ -1516,19 +1541,28 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('tracks-list').innerHTML = data.tracks.map(t => `
|
document.getElementById('tracks-list').innerHTML = data.tracks.map(t => {
|
||||||
<div class="track-item">
|
let statusBadge = '';
|
||||||
<span class="track-position">${t.position + 1}</span>
|
if (t.isLocal === true) {
|
||||||
<div class="track-info">
|
statusBadge = '<span class="status-badge success" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>Local</span>';
|
||||||
<h4>${escapeHtml(t.title)}</h4>
|
} else if (t.isLocal === false) {
|
||||||
<span class="artists">${escapeHtml(t.artists.join(', '))}</span>
|
statusBadge = '<span class="status-badge warning" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>External</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="track-item">
|
||||||
|
<span class="track-position">${t.position + 1}</span>
|
||||||
|
<div class="track-info">
|
||||||
|
<h4>${escapeHtml(t.title)}${statusBadge}</h4>
|
||||||
|
<span class="artists">${escapeHtml(t.artists.join(', '))}</span>
|
||||||
|
</div>
|
||||||
|
<div class="track-meta">
|
||||||
|
${t.album ? escapeHtml(t.album) : ''}
|
||||||
|
${t.isrc ? '<br><small>ISRC: ' + t.isrc + '</small>' : ''}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-meta">
|
`;
|
||||||
${t.album ? escapeHtml(t.album) : ''}
|
}).join('');
|
||||||
${t.isrc ? '<br><small>ISRC: ' + t.isrc + '</small>' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
document.getElementById('tracks-list').innerHTML = '<p style="text-align:center;color:var(--error);padding:40px;">Failed to load tracks</p>';
|
document.getElementById('tracks-list').innerHTML = '<p style="text-align:center;color:var(--error);padding:40px;">Failed to load tracks</p>';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user