Fix: Add UserId parameter for Jellyfin playlist operations + UI improvements

- CRITICAL FIX: Add UserId parameter to all Jellyfin playlist item fetches (fixes 400 BadRequest errors)
- Fix GetPlaylists to correctly count local/missing tracks
- Fix GetSpotifyPlaylistTracksOrderedAsync to find local tracks (was serving external tracks for everything)
- Fix SpotifyTrackMatchingService to skip tracks already in Jellyfin
- Add detailed debug logging for track matching (LOCAL by ISRC/Spotify ID, EXTERNAL match, NO MATCH)
- Add 'Match Tracks' button for individual playlists (not all playlists)
- Add 'Match All Tracks' button for matching all playlists at once
- Add JELLYFIN_USER_ID to web UI configuration tab for easy setup
- Add /api/admin/playlists/match-all endpoint

This fixes the issue where local tracks weren't being used - the system was downloading
from SquidWTF even when files existed locally in Jellyfin.
This commit is contained in:
2026-02-03 18:14:13 -05:00
parent b16d16c9c9
commit c44be48eb9
4 changed files with 219 additions and 29 deletions

View File

@@ -201,35 +201,64 @@ public class AdminController : ControllerBase
{
try
{
var url = $"{_jellyfinSettings.Url}/Playlists/{config.JellyfinId}/Items?Fields=Path";
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
// Jellyfin requires UserId parameter to fetch playlist items
var userId = _jellyfinSettings.UserId;
_logger.LogDebug("Fetching Jellyfin playlist items for {Name} from {Url}", config.Name, url);
var response = await _jellyfinHttpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
// If no user configured, try to get the first user
if (string.IsNullOrEmpty(userId))
{
var jellyfinJson = await response.Content.ReadAsStringAsync();
using var jellyfinDoc = JsonDocument.Parse(jellyfinJson);
var usersResponse = await _jellyfinHttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{_jellyfinSettings.Url}/Users")
{
Headers = { { "X-Emby-Authorization", GetJellyfinAuthHeader() } }
});
if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items))
if (usersResponse.IsSuccessStatusCode)
{
var localCount = items.GetArrayLength();
playlistInfo["localTracks"] = localCount;
playlistInfo["externalTracks"] = Math.Max(0, spotifyTrackCount - localCount);
_logger.LogDebug("Playlist {Name}: {Local} local tracks, {Missing} missing",
config.Name, localCount, spotifyTrackCount - localCount);
}
else
{
_logger.LogWarning("No Items property in Jellyfin response for {Name}", config.Name);
var usersJson = await usersResponse.Content.ReadAsStringAsync();
using var usersDoc = JsonDocument.Parse(usersJson);
if (usersDoc.RootElement.GetArrayLength() > 0)
{
userId = usersDoc.RootElement[0].GetProperty("Id").GetString();
}
}
}
if (string.IsNullOrEmpty(userId))
{
_logger.LogWarning("No user ID available to fetch playlist items for {Name}", config.Name);
}
else
{
_logger.LogWarning("Failed to get Jellyfin playlist {Name}: {StatusCode}",
config.Name, response.StatusCode);
var url = $"{_jellyfinSettings.Url}/Playlists/{config.JellyfinId}/Items?UserId={userId}&Fields=Path";
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("X-Emby-Authorization", GetJellyfinAuthHeader());
_logger.LogDebug("Fetching Jellyfin playlist items for {Name} from {Url}", config.Name, url);
var response = await _jellyfinHttpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var jellyfinJson = await response.Content.ReadAsStringAsync();
using var jellyfinDoc = JsonDocument.Parse(jellyfinJson);
if (jellyfinDoc.RootElement.TryGetProperty("Items", out var items))
{
var localCount = items.GetArrayLength();
playlistInfo["localTracks"] = localCount;
playlistInfo["externalTracks"] = Math.Max(0, spotifyTrackCount - localCount);
_logger.LogDebug("Playlist {Name}: {Local} local tracks, {Missing} missing",
config.Name, localCount, spotifyTrackCount - localCount);
}
else
{
_logger.LogWarning("No Items property in Jellyfin response for {Name}", config.Name);
}
}
else
{
_logger.LogWarning("Failed to get Jellyfin playlist {Name}: {StatusCode}",
config.Name, response.StatusCode);
}
}
}
catch (Exception ex)
@@ -302,7 +331,7 @@ public class AdminController : ControllerBase
try
{
await _matchingService.TriggerMatchingAsync();
await _matchingService.TriggerMatchingForPlaylistAsync(decodedName);
return Ok(new { message = $"Track matching triggered for {decodedName}", timestamp = DateTime.UtcNow });
}
catch (Exception ex)
@@ -312,6 +341,31 @@ public class AdminController : ControllerBase
}
}
/// <summary>
/// Trigger track matching for all playlists
/// </summary>
[HttpPost("playlists/match-all")]
public async Task<IActionResult> MatchAllPlaylistTracks()
{
_logger.LogInformation("Manual track matching triggered for all playlists");
if (_matchingService == null)
{
return BadRequest(new { error = "Track matching service is not available" });
}
try
{
await _matchingService.TriggerMatchingAsync();
return Ok(new { message = "Track matching triggered for all playlists", timestamp = DateTime.UtcNow });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to trigger track matching for all playlists");
return StatusCode(500, new { error = "Failed to trigger track matching", details = ex.Message });
}
}
/// <summary>
/// Get current configuration (safe values only)
/// </summary>
@@ -346,6 +400,7 @@ public class AdminController : ControllerBase
{
url = _jellyfinSettings.Url,
apiKey = MaskValue(_jellyfinSettings.ApiKey),
userId = _jellyfinSettings.UserId ?? "(not set)",
libraryId = _jellyfinSettings.LibraryId
},
deezer = new