From bdd753fd02f48a50de0ffbdbcf6318bafcc6eb35 Mon Sep 17 00:00:00 2001 From: Josh Patra Date: Mon, 9 Feb 2026 12:49:50 -0500 Subject: [PATCH] feat: add admin UI improvements and forwarded headers support Enhanced admin configuration UI with missing fields, required indicators, and sp_dc warning. Added Spotify playlist selector for linking with auto-filtering of already-linked playlists. Configured forwarded headers to pass real client IPs from nginx to Jellyfin. Improved track view modal error handling. --- allstarr/Controllers/AdminController.cs | 114 +++++++++++ allstarr/Program.cs | 26 +++ allstarr/wwwroot/index.html | 250 ++++++++++++++++++++++-- 3 files changed, 375 insertions(+), 15 deletions(-) diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index ea60104..2c5142c 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -1379,6 +1379,12 @@ public class AdminController : ControllerBase { return Ok(new { + backendType = _configuration.GetValue("Backend:Type") ?? "Jellyfin", + musicService = _configuration.GetValue("MusicService") ?? "SquidWTF", + explicitFilter = _configuration.GetValue("ExplicitFilter") ?? "All", + enableExternalPlaylists = _configuration.GetValue("EnableExternalPlaylists", false), + playlistsDirectory = _configuration.GetValue("PlaylistsDirectory") ?? "(not set)", + redisEnabled = _configuration.GetValue("Redis:Enabled", false), spotifyApi = new { enabled = _spotifyApiSettings.Enabled, @@ -1392,6 +1398,9 @@ public class AdminController : ControllerBase { enabled = _spotifyImportSettings.Enabled, matchingIntervalHours = _spotifyImportSettings.MatchingIntervalHours, + syncStartHour = _spotifyImportSettings.SyncStartHour, + syncStartMinute = _spotifyImportSettings.SyncStartMinute, + syncWindowHours = _spotifyImportSettings.SyncWindowHours, playlists = _spotifyImportSettings.Playlists.Select(p => new { name = p.Name, @@ -1919,6 +1928,111 @@ public class AdminController : ControllerBase } } + /// + /// Get all playlists from the user's Spotify account + /// + [HttpGet("spotify/user-playlists")] + public async Task GetSpotifyUserPlaylists() + { + if (!_spotifyApiSettings.Enabled || string.IsNullOrEmpty(_spotifyApiSettings.SessionCookie)) + { + return BadRequest(new { error = "Spotify API not configured. Please set sp_dc session cookie." }); + } + + try + { + var token = await _spotifyClient.GetWebAccessTokenAsync(); + if (string.IsNullOrEmpty(token)) + { + return StatusCode(401, new { error = "Failed to authenticate with Spotify. Check your sp_dc cookie." }); + } + + // Get list of already-configured Spotify playlist IDs + var configuredPlaylists = await ReadPlaylistsFromEnvFile(); + var linkedSpotifyIds = new HashSet( + configuredPlaylists.Select(p => p.Id), + StringComparer.OrdinalIgnoreCase + ); + + var playlists = new List(); + var offset = 0; + const int limit = 50; + + while (true) + { + var url = $"https://api.spotify.com/v1/me/playlists?offset={offset}&limit={limit}"; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + + var response = await _jellyfinHttpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to fetch Spotify playlists: {StatusCode}", response.StatusCode); + break; + } + + var json = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + if (!root.TryGetProperty("items", out var items) || items.GetArrayLength() == 0) + break; + + foreach (var item in items.EnumerateArray()) + { + var id = item.TryGetProperty("id", out var itemId) ? itemId.GetString() : null; + var name = item.TryGetProperty("name", out var n) ? n.GetString() : null; + var trackCount = 0; + + if (item.TryGetProperty("tracks", out var tracks) && + tracks.TryGetProperty("total", out var total)) + { + trackCount = total.GetInt32(); + } + + var owner = ""; + if (item.TryGetProperty("owner", out var ownerObj) && + ownerObj.TryGetProperty("display_name", out var displayName)) + { + owner = displayName.GetString() ?? ""; + } + + var isPublic = item.TryGetProperty("public", out var pub) && pub.GetBoolean(); + + // Check if this playlist is already linked + var isLinked = !string.IsNullOrEmpty(id) && linkedSpotifyIds.Contains(id); + + playlists.Add(new + { + id, + name, + trackCount, + owner, + isPublic, + isLinked + }); + } + + if (items.GetArrayLength() < limit) break; + offset += limit; + + // Rate limiting + if (_spotifyApiSettings.RateLimitDelayMs > 0) + { + await Task.Delay(_spotifyApiSettings.RateLimitDelayMs); + } + } + + return Ok(new { playlists }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Spotify user playlists"); + return StatusCode(500, new { error = "Failed to fetch Spotify playlists", details = ex.Message }); + } + } + /// /// Get all playlists from Jellyfin /// diff --git a/allstarr/Program.cs b/allstarr/Program.cs index 70b47ed..0ec5471 100644 --- a/allstarr/Program.cs +++ b/allstarr/Program.cs @@ -13,6 +13,27 @@ using allstarr.Middleware; using allstarr.Filters; using Microsoft.Extensions.Http; using System.Text; +using System.Net; + +var builder = WebApplication.CreateBuilder(args); + +// Configure forwarded headers for reverse proxy support (nginx, etc.) +// This allows ASP.NET Core to read X-Forwarded-For, X-Real-IP, etc. +builder.Services.Configure(options => +{ + options.ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor + | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto + | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedHost; + + // Clear known networks and proxies to accept headers from any proxy + // This is safe when running behind a trusted reverse proxy (nginx) + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + + // Trust X-Forwarded-* headers from any source + // Only do this if your reverse proxy is properly configured and trusted + options.ForwardLimit = null; +}); var builder = WebApplication.CreateBuilder(args); @@ -638,6 +659,11 @@ catch (Exception ex) } // Configure the HTTP request pipeline. + +// IMPORTANT: UseForwardedHeaders must be called BEFORE other middleware +// This processes X-Forwarded-For, X-Real-IP, etc. from nginx +app.UseForwardedHeaders(); + app.UseExceptionHandler(_ => { }); // Global exception handler // Enable response compression EARLY in the pipeline diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 3946dc9..a555fa9 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -806,8 +806,62 @@
+
+

Core Settings

+
+
+ Backend Type * + - + +
+
+ Music Service * + - + +
+
+ Storage Mode + - + +
+ +
+ Download Mode + - + +
+
+ Explicit Filter + - + +
+
+ Enable External Playlists + - + +
+
+ Playlists Directory + - + +
+
+ Redis Enabled + - + +
+
+
+

Spotify API Settings

+
+ ⚠️ For active playlists and link functionality to work, sp_dc session cookie must be set! +
API Enabled @@ -815,7 +869,7 @@
- Session Cookie (sp_dc) + Session Cookie (sp_dc) * -
@@ -904,17 +958,17 @@

Jellyfin Settings

- URL + URL * -
- API Key + API Key * -
- User ID + User ID * -
@@ -945,11 +999,21 @@

Sync Schedule

+
+ Spotify Import Enabled + - + +
Sync Start Time -
+
+ Sync Start Minute + - + +
Sync Window - @@ -1166,20 +1230,40 @@