mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 07:58:39 -05:00
Fix web UI config persistence and cookie age tracking
- Add SPOTIFY_API_SESSION_COOKIE_SET_DATE to docker-compose.yml env mapping - Mount .env file in container for web UI to update - Add SessionCookieSetDate loading in Program.cs - Improve .env update logic with better error handling and logging - Auto-initialize cookie date when cookie exists but date not set - Simplify local vs external track detection in Jellyfin playlists - Enhanced Spotify playlist ID parsing (supports ID, URI, and URL formats) - Better UI clarity: renamed tabs to 'Link Playlists' and 'Active Playlists'
This commit is contained in:
@@ -32,12 +32,14 @@ public class AdminController : ControllerBase
|
||||
private readonly SpotifyPlaylistFetcher _playlistFetcher;
|
||||
private readonly RedisCacheService _cache;
|
||||
private readonly HttpClient _jellyfinHttpClient;
|
||||
private const string EnvFilePath = "/app/.env";
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
private readonly string _envFilePath;
|
||||
private const string CacheDirectory = "/app/cache/spotify";
|
||||
|
||||
public AdminController(
|
||||
ILogger<AdminController> logger,
|
||||
IConfiguration configuration,
|
||||
IWebHostEnvironment environment,
|
||||
IOptions<SpotifyApiSettings> spotifyApiSettings,
|
||||
IOptions<SpotifyImportSettings> spotifyImportSettings,
|
||||
IOptions<JellyfinSettings> jellyfinSettings,
|
||||
@@ -51,6 +53,7 @@ public class AdminController : ControllerBase
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_environment = environment;
|
||||
_spotifyApiSettings = spotifyApiSettings.Value;
|
||||
_spotifyImportSettings = spotifyImportSettings.Value;
|
||||
_jellyfinSettings = jellyfinSettings.Value;
|
||||
@@ -61,6 +64,14 @@ public class AdminController : ControllerBase
|
||||
_playlistFetcher = playlistFetcher;
|
||||
_cache = cache;
|
||||
_jellyfinHttpClient = httpClientFactory.CreateClient();
|
||||
|
||||
// .env file path is always /app/.env in Docker (mounted from host)
|
||||
// In development, it's in the parent directory of ContentRootPath
|
||||
_envFilePath = _environment.IsDevelopment()
|
||||
? Path.Combine(_environment.ContentRootPath, "..", ".env")
|
||||
: "/app/.env";
|
||||
|
||||
_logger.LogInformation("Admin controller initialized. .env path: {EnvFilePath}", _envFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,15 +301,21 @@ public class AdminController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
// Check if .env file exists
|
||||
if (!System.IO.File.Exists(_envFilePath))
|
||||
{
|
||||
_logger.LogWarning(".env file not found at {Path}, creating new file", _envFilePath);
|
||||
}
|
||||
|
||||
// Read current .env file or create new one
|
||||
var envContent = new Dictionary<string, string>();
|
||||
|
||||
if (System.IO.File.Exists(EnvFilePath))
|
||||
if (System.IO.File.Exists(_envFilePath))
|
||||
{
|
||||
var lines = await System.IO.File.ReadAllLinesAsync(EnvFilePath);
|
||||
var lines = await System.IO.File.ReadAllLinesAsync(_envFilePath);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#'))
|
||||
if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith('#'))
|
||||
continue;
|
||||
|
||||
var eqIndex = line.IndexOf('=');
|
||||
@@ -309,6 +326,7 @@ public class AdminController : ControllerBase
|
||||
envContent[key] = value;
|
||||
}
|
||||
}
|
||||
_logger.LogInformation("Loaded {Count} existing env vars from {Path}", envContent.Count, _envFilePath);
|
||||
}
|
||||
|
||||
// Apply updates with validation
|
||||
@@ -318,12 +336,16 @@ public class AdminController : ControllerBase
|
||||
// Validate key format
|
||||
if (!IsValidEnvKey(key))
|
||||
{
|
||||
_logger.LogWarning("Invalid env key rejected: {Key}", key);
|
||||
return BadRequest(new { error = $"Invalid environment variable key: {key}" });
|
||||
}
|
||||
|
||||
envContent[key] = value;
|
||||
appliedUpdates.Add(key);
|
||||
_logger.LogInformation(" Setting {Key}", key);
|
||||
_logger.LogInformation(" Setting {Key} = {Value}", key,
|
||||
key.Contains("COOKIE") || key.Contains("TOKEN") || key.Contains("KEY") || key.Contains("ARL")
|
||||
? "***" + (value.Length > 8 ? value[^8..] : "")
|
||||
: value);
|
||||
|
||||
// Auto-set cookie date when Spotify session cookie is updated
|
||||
if (key == "SPOTIFY_API_SESSION_COOKIE" && !string.IsNullOrEmpty(value))
|
||||
@@ -338,21 +360,35 @@ public class AdminController : ControllerBase
|
||||
|
||||
// Write back to .env file
|
||||
var newContent = string.Join("\n", envContent.Select(kv => $"{kv.Key}={kv.Value}"));
|
||||
await System.IO.File.WriteAllTextAsync(EnvFilePath, newContent + "\n");
|
||||
await System.IO.File.WriteAllTextAsync(_envFilePath, newContent + "\n");
|
||||
|
||||
_logger.LogInformation("Config file updated successfully");
|
||||
_logger.LogInformation("Config file updated successfully at {Path}", _envFilePath);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
message = "Configuration updated. Restart container to apply changes.",
|
||||
updatedKeys = appliedUpdates,
|
||||
requiresRestart = true
|
||||
requiresRestart = true,
|
||||
envFilePath = _envFilePath
|
||||
});
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Permission denied writing to .env file at {Path}", _envFilePath);
|
||||
return StatusCode(500, new {
|
||||
error = "Permission denied",
|
||||
details = "Cannot write to .env file. Check file permissions and volume mount.",
|
||||
path = _envFilePath
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update configuration");
|
||||
return StatusCode(500, new { error = "Failed to update configuration", details = ex.Message });
|
||||
_logger.LogError(ex, "Failed to update configuration at {Path}", _envFilePath);
|
||||
return StatusCode(500, new {
|
||||
error = "Failed to update configuration",
|
||||
details = ex.Message,
|
||||
path = _envFilePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -793,6 +829,7 @@ public class AdminController : ControllerBase
|
||||
var response = await _jellyfinHttpClient.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to fetch playlist items for {PlaylistId}: {StatusCode}", playlistId, response.StatusCode);
|
||||
return (0, 0, 0);
|
||||
}
|
||||
|
||||
@@ -807,62 +844,48 @@ public class AdminController : ControllerBase
|
||||
{
|
||||
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());
|
||||
// Simpler detection: Check if Path exists and is not empty
|
||||
// External tracks from allstarr won't have a Path property
|
||||
var hasPath = item.TryGetProperty("Path", out var pathProp) &&
|
||||
pathProp.ValueKind == JsonValueKind.String &&
|
||||
!string.IsNullOrEmpty(pathProp.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)
|
||||
if (hasPath)
|
||||
{
|
||||
foreach (var source in mediaSources.EnumerateArray())
|
||||
var pathStr = pathProp.GetString()!;
|
||||
// Check if it's a real file path (not a URL)
|
||||
if (pathStr.StartsWith("/") || pathStr.Contains(":\\"))
|
||||
{
|
||||
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;
|
||||
}
|
||||
localTracks++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's a URL or external source
|
||||
externalTracks++;
|
||||
externalAvailable++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to checking Path property
|
||||
if (!isLocal && hasPath)
|
||||
{
|
||||
isLocal = true;
|
||||
}
|
||||
|
||||
if (isLocal)
|
||||
{
|
||||
localTracks++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No path means it's external
|
||||
externalTracks++;
|
||||
// For now, if it's in the playlist but not local, it means a provider found it
|
||||
externalAvailable++;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Playlist {PlaylistId} stats: {Local} local, {External} external",
|
||||
playlistId, localTracks, externalTracks);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No Items property in playlist response for {PlaylistId}", playlistId);
|
||||
}
|
||||
|
||||
return (localTracks, externalTracks, externalAvailable);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to get track stats for playlist {PlaylistId}", playlistId);
|
||||
_logger.LogError(ex, "Failed to get track stats for playlist {PlaylistId}", playlistId);
|
||||
return (0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user