Some checks failed
release / build-release (push) Has been cancelled
- IChannel: creators -> sub-channels -> video posts (public listing API, search mapped) - IRequiresMediaInfoCallback -> signed proxy URLs; stream controller rewrites HLS so the AES-128 key is fetched server-side with the session cookie, segments stay on the CDN - delivery/info cached per token lifetime and throttled (~2/min) - config page: paste sails.sid + validate, extra creators, quality, search test - scripts/package.sh -> plugin folder + zip + md5 + manifest.json; Gitea release workflow - research docs, HAR analysis (HAR itself gitignored), install-pi5.sh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
202 lines
6.7 KiB
C#
202 lines
6.7 KiB
C#
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;
|
|
|
|
/// <summary>Helpers for the dashboard configuration page (admin only).</summary>
|
|
[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<FloatplaneAdminController> _logger;
|
|
|
|
public FloatplaneAdminController(FloatplaneApiClient api, CatalogService catalog, DeliveryResolver delivery, ILogger<FloatplaneAdminController> logger)
|
|
{
|
|
_api = api;
|
|
_catalog = catalog;
|
|
_delivery = delivery;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>Validates a session cookie (without saving it) and lists the subscriptions it has.</summary>
|
|
[HttpPost("Session/Validate")]
|
|
public async Task<ActionResult<SessionValidationResult>> 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 };
|
|
}
|
|
}
|
|
|
|
/// <summary>Current plugin state for the settings page.</summary>
|
|
[HttpGet("Status")]
|
|
public ActionResult<PluginStatus> 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
|
|
};
|
|
}
|
|
|
|
/// <summary>Drops caches and reloads creators (post lists refill on browse).</summary>
|
|
[HttpPost("Refresh")]
|
|
public async Task<ActionResult<PluginStatus>> Refresh()
|
|
{
|
|
_catalog.Invalidate();
|
|
await _catalog.GetCreatorsAsync(true, HttpContext.RequestAborted).ConfigureAwait(false);
|
|
return GetStatus();
|
|
}
|
|
|
|
/// <summary>Runs a Floatplane search (public endpoint) — for mapping/testing the search API.</summary>
|
|
[HttpGet("Search")]
|
|
public async Task<ActionResult<List<SearchHit>>> 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();
|
|
}
|
|
|
|
/// <summary>Tags Floatplane knows for a creator (public endpoint).</summary>
|
|
[HttpGet("Tags")]
|
|
public Task<Dictionary<string, int>> 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<string> 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<CreatorSummary> 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<string> 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; }
|
|
}
|