diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index 3f4358c..11d6a23 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -595,10 +595,10 @@ public class AdminController : ControllerBase } /// - /// Get all playlists from Jellyfin + /// Get all Jellyfin users /// - [HttpGet("jellyfin/playlists")] - public async Task GetJellyfinPlaylists() + [HttpGet("jellyfin/users")] + public async Task GetJellyfinUsers() { if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey)) { @@ -607,8 +607,117 @@ public class AdminController : ControllerBase try { - // Call Jellyfin API to get all playlists - var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount"; + var url = $"{_jellyfinSettings.Url}/Users"; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); + + var response = await _jellyfinHttpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(); + _logger.LogError("Failed to fetch Jellyfin users: {StatusCode} - {Body}", response.StatusCode, errorBody); + return StatusCode((int)response.StatusCode, new { error = "Failed to fetch users from Jellyfin" }); + } + + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + + var users = new List(); + + foreach (var user in doc.RootElement.EnumerateArray()) + { + var id = user.GetProperty("Id").GetString(); + var name = user.GetProperty("Name").GetString(); + + users.Add(new { id, name }); + } + + return Ok(new { users }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Jellyfin users"); + return StatusCode(500, new { error = "Failed to fetch users", details = ex.Message }); + } + } + + /// + /// Get all Jellyfin libraries (virtual folders) + /// + [HttpGet("jellyfin/libraries")] + public async Task GetJellyfinLibraries() + { + if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey)) + { + return BadRequest(new { error = "Jellyfin URL or API key not configured" }); + } + + try + { + var url = $"{_jellyfinSettings.Url}/Library/VirtualFolders"; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); + + var response = await _jellyfinHttpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) + { + var errorBody = await response.Content.ReadAsStringAsync(); + _logger.LogError("Failed to fetch Jellyfin libraries: {StatusCode} - {Body}", response.StatusCode, errorBody); + return StatusCode((int)response.StatusCode, new { error = "Failed to fetch libraries from Jellyfin" }); + } + + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + + var libraries = new List(); + + foreach (var lib in doc.RootElement.EnumerateArray()) + { + var name = lib.GetProperty("Name").GetString(); + var itemId = lib.TryGetProperty("ItemId", out var id) ? id.GetString() : null; + var collectionType = lib.TryGetProperty("CollectionType", out var ct) ? ct.GetString() : null; + + libraries.Add(new { id = itemId, name, collectionType }); + } + + return Ok(new { libraries }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Jellyfin libraries"); + return StatusCode(500, new { error = "Failed to fetch libraries", details = ex.Message }); + } + } + + /// + /// Get all playlists from Jellyfin + /// + [HttpGet("jellyfin/playlists")] + public async Task GetJellyfinPlaylists([FromQuery] string? userId = null, [FromQuery] string? parentId = null) + { + if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey)) + { + return BadRequest(new { error = "Jellyfin URL or API key not configured" }); + } + + try + { + // Build URL with optional userId and parentId (library) filters + var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount,RecursiveItemCount,SongCount"; + + if (!string.IsNullOrEmpty(userId)) + { + url += $"&UserId={userId}"; + } + + if (!string.IsNullOrEmpty(parentId)) + { + url += $"&ParentId={parentId}"; + } var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); @@ -633,21 +742,21 @@ public class AdminController : ControllerBase { var id = item.GetProperty("Id").GetString(); var name = item.GetProperty("Name").GetString(); - var childCount = item.TryGetProperty("ChildCount", out var cc) ? cc.GetInt32() : 0; - // Check if this playlist has a linked Spotify ID in ProviderIds - string? linkedSpotifyId = null; - if (item.TryGetProperty("ProviderIds", out var providerIds)) - { - if (providerIds.TryGetProperty("Spotify", out var spotifyId)) - { - linkedSpotifyId = spotifyId.GetString(); - } - } + // Try multiple fields for track count - Jellyfin may use different fields + var childCount = 0; + if (item.TryGetProperty("ChildCount", out var cc) && cc.ValueKind == JsonValueKind.Number) + childCount = cc.GetInt32(); + else if (item.TryGetProperty("SongCount", out var sc) && sc.ValueKind == JsonValueKind.Number) + childCount = sc.GetInt32(); + else if (item.TryGetProperty("RecursiveItemCount", out var ric) && ric.ValueKind == JsonValueKind.Number) + childCount = ric.GetInt32(); - // Check if this playlist is already configured in allstarr - var isConfigured = _spotifyImportSettings.Playlists.Any(p => - p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + // Check if this playlist is configured in allstarr and get linked Spotify ID + var configuredPlaylist = _spotifyImportSettings.Playlists + .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + var isConfigured = configuredPlaylist != null; + var linkedSpotifyId = configuredPlaylist?.Id; playlists.Add(new { diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index ba67b33..c4ec0a9 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -565,8 +565,24 @@

Link Jellyfin playlists to Spotify playlists to fill in missing tracks. - Playlists created by Spotify Import plugin will appear here. + Select a user and/or library to filter playlists.

+ +
+
+ + +
+
+ + +
+
+ @@ -1047,12 +1063,48 @@ } } + async function fetchJellyfinUsers() { + try { + const res = await fetch('/api/admin/jellyfin/users'); + if (!res.ok) return; + const data = await res.json(); + const select = document.getElementById('jellyfin-user-select'); + select.innerHTML = '' + + data.users.map(u => ``).join(''); + } catch (error) { + console.error('Failed to fetch users:', error); + } + } + + 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 = ''; try { - const res = await fetch('/api/admin/jellyfin/playlists'); + // Build URL with optional filters + 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(); + + const res = await fetch(url); if (!res.ok) { const errorData = await res.json(); @@ -1415,6 +1467,8 @@ // Initial load fetchStatus(); fetchPlaylists(); + fetchJellyfinUsers(); + fetchJellyfinLibraries(); fetchJellyfinPlaylists(); fetchConfig();
Loading Jellyfin playlists...