diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs
index 918f463..810fc34 100644
--- a/allstarr/Controllers/AdminController.cs
+++ b/allstarr/Controllers/AdminController.cs
@@ -2015,6 +2015,284 @@ public class AdminController : ControllerBase
GC.Collect(2, GCCollectionMode.Optimized, blocking: false);
}
}
+
+ #region Spotify Admin Endpoints
+
+ ///
+ /// Manual trigger endpoint to force fetch Spotify missing tracks.
+ ///
+ [HttpGet("spotify/sync")]
+ public async Task TriggerSpotifySync([FromServices] IEnumerable hostedServices)
+ {
+ try
+ {
+ if (!_spotifyImportSettings.Enabled)
+ {
+ return BadRequest(new { error = "Spotify Import is not enabled" });
+ }
+
+ _logger.LogInformation("Manual Spotify sync triggered via admin endpoint");
+
+ // Find the SpotifyMissingTracksFetcher service
+ var fetcherService = hostedServices
+ .OfType()
+ .FirstOrDefault();
+
+ if (fetcherService == null)
+ {
+ return BadRequest(new { error = "SpotifyMissingTracksFetcher service not found" });
+ }
+
+ // Trigger the sync in background
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ // Use reflection to call the private ExecuteOnceAsync method
+ var method = fetcherService.GetType().GetMethod("ExecuteOnceAsync",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (method != null)
+ {
+ await (Task)method.Invoke(fetcherService, new object[] { CancellationToken.None })!;
+ _logger.LogInformation("Manual Spotify sync completed successfully");
+ }
+ else
+ {
+ _logger.LogError("Could not find ExecuteOnceAsync method on SpotifyMissingTracksFetcher");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during manual Spotify sync");
+ }
+ });
+
+ return Ok(new {
+ message = "Spotify sync started in background",
+ timestamp = DateTime.UtcNow
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error triggering Spotify sync");
+ return StatusCode(500, new { error = "Internal server error" });
+ }
+ }
+
+ ///
+ /// Manual trigger endpoint to force Spotify track matching.
+ ///
+ [HttpGet("spotify/match")]
+ public async Task TriggerSpotifyMatch([FromServices] IEnumerable hostedServices)
+ {
+ try
+ {
+ if (!_spotifyApiSettings.Enabled)
+ {
+ return BadRequest(new { error = "Spotify API is not enabled" });
+ }
+
+ _logger.LogInformation("Manual Spotify track matching triggered via admin endpoint");
+
+ // Find the SpotifyTrackMatchingService
+ var matchingService = hostedServices
+ .OfType()
+ .FirstOrDefault();
+
+ if (matchingService == null)
+ {
+ return BadRequest(new { error = "SpotifyTrackMatchingService not found" });
+ }
+
+ // Trigger matching in background
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ // Use reflection to call the private ExecuteOnceAsync method
+ var method = matchingService.GetType().GetMethod("ExecuteOnceAsync",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+
+ if (method != null)
+ {
+ await (Task)method.Invoke(matchingService, new object[] { CancellationToken.None })!;
+ _logger.LogInformation("Manual Spotify track matching completed successfully");
+ }
+ else
+ {
+ _logger.LogError("Could not find ExecuteOnceAsync method on SpotifyTrackMatchingService");
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during manual Spotify track matching");
+ }
+ });
+
+ return Ok(new {
+ message = "Spotify track matching started in background",
+ timestamp = DateTime.UtcNow
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error triggering Spotify track matching");
+ return StatusCode(500, new { error = "Internal server error" });
+ }
+ }
+
+ ///
+ /// Clear Spotify playlist cache to force re-matching.
+ ///
+ [HttpPost("spotify/clear-cache")]
+ public async Task ClearSpotifyCache()
+ {
+ try
+ {
+ var clearedKeys = new List();
+
+ // Clear Redis cache for all configured playlists
+ foreach (var playlist in _spotifyImportSettings.Playlists)
+ {
+ var keys = new[]
+ {
+ $"spotify:playlist:{playlist.Name}",
+ $"spotify:playlist:items:{playlist.Name}",
+ $"spotify:matched:{playlist.Name}"
+ };
+
+ foreach (var key in keys)
+ {
+ await _cache.DeleteAsync(key);
+ clearedKeys.Add(key);
+ }
+ }
+
+ _logger.LogInformation("Cleared Spotify cache for {Count} keys via admin endpoint", clearedKeys.Count);
+
+ return Ok(new {
+ message = "Spotify cache cleared successfully",
+ clearedKeys = clearedKeys,
+ timestamp = DateTime.UtcNow
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error clearing Spotify cache");
+ return StatusCode(500, new { error = "Internal server error" });
+ }
+ }
+
+ #endregion
+
+ #region Debug Endpoints
+
+ ///
+ /// Gets endpoint usage statistics from the log file.
+ ///
+ [HttpGet("debug/endpoint-usage")]
+ public async Task GetEndpointUsage(
+ [FromQuery] int top = 100,
+ [FromQuery] string? since = null)
+ {
+ try
+ {
+ var logFile = "/app/cache/endpoint-usage/endpoints.csv";
+
+ if (!System.IO.File.Exists(logFile))
+ {
+ return Ok(new {
+ message = "No endpoint usage data available",
+ endpoints = new object[0]
+ });
+ }
+
+ var lines = await System.IO.File.ReadAllLinesAsync(logFile);
+ var usage = new Dictionary();
+ DateTime? sinceDate = null;
+
+ if (!string.IsNullOrEmpty(since) && DateTime.TryParse(since, out var parsedDate))
+ {
+ sinceDate = parsedDate;
+ }
+
+ foreach (var line in lines.Skip(1)) // Skip header
+ {
+ var parts = line.Split(',');
+ if (parts.Length >= 3)
+ {
+ var timestamp = parts[0];
+ var endpoint = parts[1];
+
+ // Filter by date if specified
+ if (sinceDate.HasValue && DateTime.TryParse(timestamp, out var logDate))
+ {
+ if (logDate < sinceDate.Value)
+ continue;
+ }
+
+ usage[endpoint] = usage.GetValueOrDefault(endpoint, 0) + 1;
+ }
+ }
+
+ var topEndpoints = usage
+ .OrderByDescending(kv => kv.Value)
+ .Take(top)
+ .Select(kv => new { endpoint = kv.Key, count = kv.Value })
+ .ToArray();
+
+ return Ok(new {
+ totalEndpoints = usage.Count,
+ totalRequests = usage.Values.Sum(),
+ since = since,
+ top = top,
+ endpoints = topEndpoints
+ });
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error getting endpoint usage");
+ return StatusCode(500, new { error = "Internal server error" });
+ }
+ }
+
+ ///
+ /// Clears the endpoint usage log file.
+ ///
+ [HttpDelete("debug/endpoint-usage")]
+ public IActionResult ClearEndpointUsage()
+ {
+ try
+ {
+ var logFile = "/app/cache/endpoint-usage/endpoints.csv";
+
+ if (System.IO.File.Exists(logFile))
+ {
+ System.IO.File.Delete(logFile);
+ _logger.LogInformation("Cleared endpoint usage log via admin endpoint");
+
+ return Ok(new {
+ message = "Endpoint usage log cleared successfully",
+ timestamp = DateTime.UtcNow
+ });
+ }
+ else
+ {
+ return Ok(new {
+ message = "No endpoint usage log file found",
+ timestamp = DateTime.UtcNow
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error clearing endpoint usage log");
+ return StatusCode(500, new { error = "Internal server error" });
+ }
+ }
+
+ #endregion
}
public class ConfigUpdateRequest
diff --git a/allstarr/Controllers/JellyfinController.cs b/allstarr/Controllers/JellyfinController.cs
index bf15e4c..ebbeda0 100644
--- a/allstarr/Controllers/JellyfinController.cs
+++ b/allstarr/Controllers/JellyfinController.cs
@@ -3979,284 +3979,8 @@ public class JellyfinController : ControllerBase
}
}
- ///
- /// Manual trigger endpoint to force fetch Spotify missing tracks.
- /// GET /spotify/sync?api_key=YOUR_KEY
- ///
- [HttpGet("spotify/sync", Order = 1)]
- [ServiceFilter(typeof(ApiKeyAuthFilter))]
- public async Task TriggerSpotifySync([FromServices] IEnumerable hostedServices)
- {
- if (!_spotifySettings.Enabled)
- {
- return BadRequest(new { error = "Spotify Import is not enabled" });
- }
-
- _logger.LogInformation("Manual Spotify sync triggered");
-
- // Find the SpotifyMissingTracksFetcher service
- var fetcherService = hostedServices
- .OfType()
- .FirstOrDefault();
-
- if (fetcherService == null)
- {
- return StatusCode(500, new { error = "SpotifyMissingTracksFetcher not found" });
- }
-
- // Trigger fetch manually
- await fetcherService.TriggerFetchAsync();
-
- // Check what was cached
- var results = new Dictionary();
- foreach (var playlist in _spotifySettings.Playlists)
- {
- var cacheKey = $"spotify:missing:{playlist.Name}";
- var tracks = await _cache.GetAsync>(cacheKey);
-
- if (tracks != null && tracks.Count > 0)
- {
- results[playlist.Name] = new {
- status = "success",
- tracks = tracks.Count,
- localTracksPosition = playlist.LocalTracksPosition.ToString()
- };
- }
- else
- {
- results[playlist.Name] = new {
- status = "not_found",
- message = "No missing tracks found"
- };
- }
- }
-
- return Ok(results);
- }
-
- ///
- /// Manually trigger track matching for all Spotify playlists.
- /// GET /spotify/match?api_key=YOUR_KEY
- ///
- [HttpGet("spotify/match", Order = 1)]
- [ServiceFilter(typeof(ApiKeyAuthFilter))]
- public async Task TriggerSpotifyMatch([FromServices] IEnumerable hostedServices)
- {
- if (!_spotifySettings.Enabled)
- {
- return BadRequest(new { error = "Spotify Import is not enabled" });
- }
-
- _logger.LogInformation("Manual Spotify track matching triggered");
-
- // Find the SpotifyTrackMatchingService
- var matchingService = hostedServices
- .OfType()
- .FirstOrDefault();
-
- if (matchingService == null)
- {
- return StatusCode(500, new { error = "SpotifyTrackMatchingService not found" });
- }
-
- // Trigger matching asynchronously
- _ = Task.Run(async () =>
- {
- try
- {
- await matchingService.TriggerMatchingAsync();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error during manual track matching");
- }
- });
-
- return Ok(new
- {
- status = "started",
- message = "Track matching started in background. Check logs for progress.",
- playlists = _spotifySettings.Playlists.Select(p => new { p.Name, p.Id, localTracksPosition = p.LocalTracksPosition.ToString() })
- });
- }
-
- private List ParseMissingTracksJson(string json)
- {
- var tracks = new List();
-
- try
- {
- var doc = JsonDocument.Parse(json);
-
- foreach (var item in doc.RootElement.EnumerateArray())
- {
- var track = new allstarr.Models.Spotify.MissingTrack
- {
- SpotifyId = item.GetProperty("Id").GetString() ?? "",
- Title = item.GetProperty("Name").GetString() ?? "",
- Album = item.GetProperty("AlbumName").GetString() ?? "",
- Artists = item.GetProperty("ArtistNames")
- .EnumerateArray()
- .Select(a => a.GetString() ?? "")
- .Where(a => !string.IsNullOrEmpty(a))
- .ToList()
- };
-
- if (!string.IsNullOrEmpty(track.Title))
- {
- tracks.Add(track);
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to parse missing tracks JSON");
- }
-
- return tracks;
- }
-
#endregion
-
- #region Spotify Debug
-
- ///
- /// Clear Spotify playlist cache to force re-matching.
- /// GET /spotify/clear-cache?api_key=YOUR_KEY
- ///
- [HttpGet("spotify/clear-cache")]
- [ServiceFilter(typeof(ApiKeyAuthFilter))]
- public async Task ClearSpotifyCache()
- {
- if (!_spotifySettings.Enabled)
- {
- return BadRequest(new { error = "Spotify Import is not enabled" });
- }
- var cleared = new List();
-
- foreach (var playlist in _spotifySettings.Playlists)
- {
- var matchedKey = $"spotify:matched:{playlist.Name}";
- await _cache.DeleteAsync(matchedKey);
- cleared.Add(playlist.Name);
- _logger.LogInformation("Cleared cache for {Playlist}", playlist.Name);
- }
-
- return Ok(new { status = "success", cleared = cleared });
- }
-
- #endregion
-
- #region Debug & Monitoring
-
- ///
- /// Gets endpoint usage statistics from the log file.
- /// GET /debug/endpoint-usage?api_key=YOUR_KEY
- /// Optional query params: top=50 (default 100), since=2024-01-01
- ///
- [HttpGet("debug/endpoint-usage")]
- [ServiceFilter(typeof(ApiKeyAuthFilter))]
- public async Task GetEndpointUsage(
- [FromQuery] int top = 100,
- [FromQuery] string? since = null)
- {
- try
- {
- var logFile = "/app/cache/endpoint-usage/endpoints.csv";
-
- if (!System.IO.File.Exists(logFile))
- {
- return Ok(new
- {
- message = "No endpoint usage data collected yet",
- endpoints = Array.Empty