Floatplane channel plugin for Jellyfin 10.10.x: browse, HLS proxy, cookie session, CI release
Some checks failed
release / build-release (push) Has been cancelled
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>
This commit is contained in:
201
Jellyfin.Plugin.Floatplane/Api/FloatplaneAdminController.cs
Normal file
201
Jellyfin.Plugin.Floatplane/Api/FloatplaneAdminController.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user