Fix Jellyfin playlists tab: linked Spotify ID, track count, and user/library filters

This commit is contained in:
2026-02-03 15:13:29 -05:00
parent c7f6783fa2
commit 75c7acb745
2 changed files with 183 additions and 20 deletions

View File

@@ -595,10 +595,10 @@ public class AdminController : ControllerBase
} }
/// <summary> /// <summary>
/// Get all playlists from Jellyfin /// Get all Jellyfin users
/// </summary> /// </summary>
[HttpGet("jellyfin/playlists")] [HttpGet("jellyfin/users")]
public async Task<IActionResult> GetJellyfinPlaylists() public async Task<IActionResult> GetJellyfinUsers()
{ {
if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey)) if (string.IsNullOrEmpty(_jellyfinSettings.Url) || string.IsNullOrEmpty(_jellyfinSettings.ApiKey))
{ {
@@ -607,8 +607,117 @@ public class AdminController : ControllerBase
try try
{ {
// Call Jellyfin API to get all playlists var url = $"{_jellyfinSettings.Url}/Users";
var url = $"{_jellyfinSettings.Url}/Items?IncludeItemTypes=Playlist&Recursive=true&Fields=ProviderIds,ChildCount";
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<object>();
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 });
}
}
/// <summary>
/// Get all Jellyfin libraries (virtual folders)
/// </summary>
[HttpGet("jellyfin/libraries")]
public async Task<IActionResult> 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<object>();
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 });
}
}
/// <summary>
/// Get all playlists from Jellyfin
/// </summary>
[HttpGet("jellyfin/playlists")]
public async Task<IActionResult> 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); var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader()); request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
@@ -633,21 +742,21 @@ public class AdminController : ControllerBase
{ {
var id = item.GetProperty("Id").GetString(); var id = item.GetProperty("Id").GetString();
var name = item.GetProperty("Name").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 // Try multiple fields for track count - Jellyfin may use different fields
string? linkedSpotifyId = null; var childCount = 0;
if (item.TryGetProperty("ProviderIds", out var providerIds)) if (item.TryGetProperty("ChildCount", out var cc) && cc.ValueKind == JsonValueKind.Number)
{ childCount = cc.GetInt32();
if (providerIds.TryGetProperty("Spotify", out var spotifyId)) else if (item.TryGetProperty("SongCount", out var sc) && sc.ValueKind == JsonValueKind.Number)
{ childCount = sc.GetInt32();
linkedSpotifyId = spotifyId.GetString(); else if (item.TryGetProperty("RecursiveItemCount", out var ric) && ric.ValueKind == JsonValueKind.Number)
} childCount = ric.GetInt32();
}
// Check if this playlist is already configured in allstarr // Check if this playlist is configured in allstarr and get linked Spotify ID
var isConfigured = _spotifyImportSettings.Playlists.Any(p => var configuredPlaylist = _spotifyImportSettings.Playlists
p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
var isConfigured = configuredPlaylist != null;
var linkedSpotifyId = configuredPlaylist?.Id;
playlists.Add(new playlists.Add(new
{ {

View File

@@ -565,8 +565,24 @@
</h2> </h2>
<p style="color: var(--text-secondary); margin-bottom: 16px;"> <p style="color: var(--text-secondary); margin-bottom: 16px;">
Link Jellyfin playlists to Spotify playlists to fill in missing tracks. 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.
</p> </p>
<div style="display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap;">
<div class="form-group" style="margin: 0; flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 4px; color: var(--text-secondary); font-size: 0.85rem;">User</label>
<select id="jellyfin-user-select" onchange="fetchJellyfinPlaylists()" style="width: 100%; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);">
<option value="">All Users</option>
</select>
</div>
<div class="form-group" style="margin: 0; flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 4px; color: var(--text-secondary); font-size: 0.85rem;">Library</label>
<select id="jellyfin-library-select" onchange="fetchJellyfinPlaylists()" style="width: 100%; padding: 8px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; color: var(--text-primary);">
<option value="">All Libraries</option>
</select>
</div>
</div>
<table class="playlist-table"> <table class="playlist-table">
<thead> <thead>
<tr> <tr>
@@ -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 = '<option value="">All Users</option>' +
data.users.map(u => `<option value="${u.id}">${escapeHtml(u.name)}</option>`).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 = '<option value="">All Libraries</option>' +
data.libraries.map(l => `<option value="${l.id}">${escapeHtml(l.name)}${l.collectionType ? ' (' + l.collectionType + ')' : ''}</option>`).join('');
} catch (error) {
console.error('Failed to fetch libraries:', error);
}
}
async function fetchJellyfinPlaylists() { async function fetchJellyfinPlaylists() {
const tbody = document.getElementById('jellyfin-playlist-table-body'); const tbody = document.getElementById('jellyfin-playlist-table-body');
tbody.innerHTML = '<tr><td colspan="5" class="loading"><span class="spinner"></span> Loading Jellyfin playlists...</td></tr>'; tbody.innerHTML = '<tr><td colspan="5" class="loading"><span class="spinner"></span> Loading Jellyfin playlists...</td></tr>';
try { 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) { if (!res.ok) {
const errorData = await res.json(); const errorData = await res.json();
@@ -1415,6 +1467,8 @@
// Initial load // Initial load
fetchStatus(); fetchStatus();
fetchPlaylists(); fetchPlaylists();
fetchJellyfinUsers();
fetchJellyfinLibraries();
fetchJellyfinPlaylists(); fetchJellyfinPlaylists();
fetchConfig(); fetchConfig();