From df7f11e769829e1204660bc0fdee7357a5ea26d1 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Tue, 3 Feb 2026 16:08:49 -0500 Subject: [PATCH] Add local/external track columns to Jellyfin playlists, remove libraries filter --- allstarr/Controllers/AdminController.cs | 105 ++++++++++++++++++++++-- allstarr/wwwroot/index.html | 47 ++++------- 2 files changed, 112 insertions(+), 40 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 11d6a23..5c8ff36 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -697,7 +697,7 @@ public class AdminController : ControllerBase /// Get all playlists from Jellyfin /// [HttpGet("jellyfin/playlists")] - public async Task GetJellyfinPlaylists([FromQuery] string? userId = null, [FromQuery] string? parentId = null) + public async Task GetJellyfinPlaylists([FromQuery] string? userId = null) { if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey)) { @@ -706,7 +706,7 @@ public class AdminController : ControllerBase try { - // Build URL with optional userId and parentId (library) filters + // Build URL with optional userId filter var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount,RecursiveItemCount,SongCount"; if (!string.IsNullOrEmpty(userId)) @@ -714,11 +714,6 @@ public class AdminController : ControllerBase url += $"&UserId={userId}"; } - if (!string.IsNullOrEmpty(parentId)) - { - url += $"&ParentId={parentId}"; - } - var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); @@ -758,13 +753,19 @@ public class AdminController : ControllerBase var isConfigured = configuredPlaylist != null; var linkedSpotifyId = configuredPlaylist?.Id; + // Fetch track details to categorize local vs external + var trackStats = await GetPlaylistTrackStats(id!); + playlists.Add(new { id, name, trackCount = childCount, linkedSpotifyId, - isConfigured + isConfigured, + localTracks = trackStats.LocalTracks, + externalTracks = trackStats.ExternalTracks, + externalAvailable = trackStats.ExternalAvailable }); } } @@ -778,6 +779,94 @@ public class AdminController : ControllerBase } } + /// + /// Get track statistics for a playlist (local vs external) + /// + private async Task<(int LocalTracks, int ExternalTracks, int ExternalAvailable)> GetPlaylistTrackStats(string playlistId) + { + try + { + var url = $"{_jellyfinSettings.Url}/Playlists/{playlistId}/Items?Fields=ProviderIds,Path,MediaSources"; + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); + + var response = await _jellyfinHttpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + return (0, 0, 0); + } + + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + + var localTracks = 0; + var externalTracks = 0; + var externalAvailable = 0; + + if (doc.RootElement.TryGetProperty("Items", out var items)) + { + foreach (var item in items.EnumerateArray()) + { + // Check if track has a local path (is in user's library) + var hasPath = item.TryGetProperty("Path", out var path) && + path.ValueKind == JsonValueKind.String && + !string.IsNullOrEmpty(path.GetString()); + + // Check MediaSources to see if it's a local file vs external + var isLocal = false; + if (item.TryGetProperty("MediaSources", out var mediaSources) && + mediaSources.ValueKind == JsonValueKind.Array) + { + foreach (var source in mediaSources.EnumerateArray()) + { + if (source.TryGetProperty("Protocol", out var protocol)) + { + var protocolStr = protocol.GetString(); + if (protocolStr == "File") + { + isLocal = true; + break; + } + } + // Also check if Path exists in MediaSource + if (source.TryGetProperty("Path", out var sourcePath) && + sourcePath.ValueKind == JsonValueKind.String && + !string.IsNullOrEmpty(sourcePath.GetString())) + { + isLocal = true; + break; + } + } + } + + // Fallback to checking Path property + if (!isLocal && hasPath) + { + isLocal = true; + } + + if (isLocal) + { + localTracks++; + } + else + { + externalTracks++; + // For now, if it's in the playlist but not local, it means a provider found it + externalAvailable++; + } + } + } + + return (localTracks, externalTracks, externalAvailable); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to get track stats for playlist {PlaylistId}", playlistId); + return (0, 0, 0); + } + } + /// /// Link a Jellyfin playlist to a Spotify playlist /// diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index c4ec0a9..f4c5ff6 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -575,19 +575,14 @@ -
- - -
- + + @@ -595,7 +590,7 @@ - @@ -1076,46 +1071,30 @@ } } - async function fetchJellyfinLibraries() { - try { - const res = await fetch('/api/admin/jellyfin/libraries'); - if (!res.ok) return; - const data = await res.json(); - const select = document.getElementById('jellyfin-library-select'); - select.innerHTML = '' + - data.libraries.map(l => ``).join(''); - } catch (error) { - console.error('Failed to fetch libraries:', error); - } - } async function fetchJellyfinPlaylists() { const tbody = document.getElementById('jellyfin-playlist-table-body'); - tbody.innerHTML = ''; + tbody.innerHTML = ''; try { - // Build URL with optional filters + // Build URL with optional user filter const userId = document.getElementById('jellyfin-user-select').value; - const parentId = document.getElementById('jellyfin-library-select').value; let url = '/api/admin/jellyfin/playlists'; - const params = new URLSearchParams(); - if (userId) params.append('userId', userId); - if (parentId) params.append('parentId', parentId); - if (params.toString()) url += '?' + params.toString(); + if (userId) url += '?userId=' + encodeURIComponent(userId); const res = await fetch(url); if (!res.ok) { const errorData = await res.json(); - tbody.innerHTML = ``; + tbody.innerHTML = ``; return; } const data = await res.json(); if (data.playlists.length === 0) { - tbody.innerHTML = ''; + tbody.innerHTML = ''; return; } @@ -1128,10 +1107,15 @@ ? `` : ``; + const localCount = p.localTracks || 0; + const externalCount = p.externalTracks || 0; + const externalAvail = p.externalAvailable || 0; + return ` - + + @@ -1140,7 +1124,7 @@ }).join(''); } catch (error) { console.error('Failed to fetch Jellyfin playlists:', error); - tbody.innerHTML = ''; + tbody.innerHTML = ''; } } @@ -1468,7 +1452,6 @@ fetchStatus(); fetchPlaylists(); fetchJellyfinUsers(); - fetchJellyfinLibraries(); fetchJellyfinPlaylists(); fetchConfig();
NameTracksLocalExternal Linked Spotify ID Status Actions
+ Loading Jellyfin playlists...
Loading Jellyfin playlists...
Loading Jellyfin playlists...
${errorData.error || 'Failed to fetch playlists'}
${errorData.error || 'Failed to fetch playlists'}
No playlists found in Jellyfin
No playlists found in Jellyfin
${escapeHtml(p.name)}${p.trackCount || 0}${localCount}${externalCount > 0 ? `${externalAvail}/${externalCount}` : '-'} ${p.linkedSpotifyId || '-'} ${statusBadge} ${actionButton}
Failed to fetch playlists
Failed to fetch playlists