using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Plugin.Floatplane.Channel; using Jellyfin.Plugin.Floatplane.Playback; using MediaBrowser.Common.Api; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.Floatplane.Api; /// Helpers for the dashboard configuration page (admin only). [ApiController] [Route("Floatplane")] [Authorize(Policy = Policies.RequiresElevation)] public class FloatplaneAdminController : ControllerBase { private readonly FloatplaneApiClient _api; private readonly CatalogService _catalog; private readonly DeliveryResolver _delivery; private readonly ILogger _logger; public FloatplaneAdminController(FloatplaneApiClient api, CatalogService catalog, DeliveryResolver delivery, ILogger logger) { _api = api; _catalog = catalog; _delivery = delivery; _logger = logger; } /// Validates a session cookie (without saving it) and lists the subscriptions it has. [HttpPost("Session/Validate")] public async Task> ValidateSession([FromBody] SessionValidationRequest request) { var cookie = request.Cookie?.Trim() ?? string.Empty; if (cookie.StartsWith("sails.sid=", StringComparison.OrdinalIgnoreCase)) { cookie = cookie["sails.sid=".Length..]; } cookie = cookie.Trim().TrimEnd(';'); if (cookie.Length == 0) { return new SessionValidationResult { Ok = false, Message = "Cookie is empty" }; } try { var ct = HttpContext.RequestAborted; var self = await _api.GetSelfAsync(ct, cookie).ConfigureAwait(false); var subs = await _api.GetSubscriptionsAsync(ct, cookie).ConfigureAwait(false); return new SessionValidationResult { Ok = true, Username = self.Username, Cookie = cookie, Subscriptions = subs.Select(s => (s.Plan?.Title ?? "?") + " (" + s.Creator + ")").ToList(), Message = $"Logged in as {self.Username}, {subs.Count} active subscription(s)" }; } catch (FloatplaneAuthException ex) { return new SessionValidationResult { Ok = false, Message = "Floatplane rejected the cookie: " + ex.Message }; } catch (FloatplaneApiException ex) { return new SessionValidationResult { Ok = false, Message = "Floatplane error: " + ex.Message }; } } /// Current plugin state for the settings page. [HttpGet("Status")] public ActionResult GetStatus() { return new PluginStatus { Version = typeof(Plugin).Assembly.GetName().Version?.ToString() ?? "?", HasSessionCookie = _api.HasSession, SessionState = _catalog.SessionState ?? "unknown", SessionUsername = Plugin.Instance?.Configuration.SessionUsername ?? string.Empty, LastError = _catalog.LastError, Generation = _catalog.Generation, LastRefresh = _catalog.LastRefresh == DateTimeOffset.MinValue ? null : _catalog.LastRefresh, Creators = _catalog.Creators.Select(c => new CreatorSummary { Id = c.Id, Title = c.Title, Urlname = c.Urlname, Channels = c.Channels.Select(ch => ch.Title).ToList() }).ToList(), CachedPostLists = _catalog.CachedPostListCount, CachedDeliveries = _delivery.CachedEntries }; } /// Drops caches and reloads creators (post lists refill on browse). [HttpPost("Refresh")] public async Task> Refresh() { _catalog.Invalidate(); await _catalog.GetCreatorsAsync(true, HttpContext.RequestAborted).ConfigureAwait(false); return GetStatus(); } /// Runs a Floatplane search (public endpoint) — for mapping/testing the search API. [HttpGet("Search")] public async Task>> Search( [FromQuery] string creatorId, [FromQuery] string? q, [FromQuery] string? channelId, [FromQuery] int limit = 20) { var posts = await _catalog.FetchPostsAsync(creatorId, channelId ?? string.Empty, Math.Clamp(limit, 1, 100), HttpContext.RequestAborted, q).ConfigureAwait(false); return posts.Select(p => new SearchHit { Id = p.Id, Title = p.Title, Channel = p.Channel?.Title ?? p.Channel?.Id, ReleaseDate = p.ReleaseDate, DurationSeconds = p.DurationSeconds, IsAccessible = p.IsAccessible, Thumbnail = p.Thumbnail?.BestPath(400) }).ToList(); } /// Tags Floatplane knows for a creator (public endpoint). [HttpGet("Tags")] public Task> Tags([FromQuery] string creatorId) => _api.GetTagsAsync(creatorId, HttpContext.RequestAborted); } public class SessionValidationRequest { public string? Cookie { get; set; } } public class SessionValidationResult { public bool Ok { get; set; } public string? Username { get; set; } public string? Cookie { get; set; } public string? Message { get; set; } public List Subscriptions { get; set; } = new(); } public class PluginStatus { public string Version { get; set; } = string.Empty; public bool HasSessionCookie { get; set; } public string SessionState { get; set; } = string.Empty; public string SessionUsername { get; set; } = string.Empty; public string? LastError { get; set; } public int Generation { get; set; } public DateTimeOffset? LastRefresh { get; set; } public List Creators { get; set; } = new(); public int CachedPostLists { get; set; } public int CachedDeliveries { get; set; } } public class CreatorSummary { public string Id { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string Urlname { get; set; } = string.Empty; public List Channels { get; set; } = new(); } public class SearchHit { public string Id { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public string? Channel { get; set; } public DateTimeOffset? ReleaseDate { get; set; } public double DurationSeconds { get; set; } public bool IsAccessible { get; set; } public string? Thumbnail { get; set; } }