v0.2: All videos folder, shared watched state, release-order sorting, official icon, OAuth plumbing, docs
Some checks failed
release / build-release (push) Has been cancelled
Some checks failed
release / build-release (push) Has been cancelled
- per-creator "All videos" folder (release order across sub-channels); sub-channel copies use cpost: ids and UserDataMirror keeps played/resume state in sync - SortNameUpdater forces release-order sort names so every client lists newest first by default - official Floatplane icon as channel image (Primary only — a channel Thumb hid video thumbnails in Continue Watching) and as 16:9 plugin thumbnail; one-time image replacement on upgrade - OAuth device-flow login (FloatplaneAuthService + settings UI); needs a Floatplane client id; Turnstile verified domain-bound so password login inside Jellyfin is impossible - generic install.sh (linuxserver / official image / native), thumb.png in package + manifest - README rewritten for a general audience, doc/usage.md with screenshots, 60 tests Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -18,18 +18,90 @@ namespace Jellyfin.Plugin.Floatplane.Api;
|
||||
public class FloatplaneAdminController : ControllerBase
|
||||
{
|
||||
private readonly FloatplaneApiClient _api;
|
||||
private readonly FloatplaneAuthService _auth;
|
||||
private readonly CatalogService _catalog;
|
||||
private readonly DeliveryResolver _delivery;
|
||||
private readonly ILogger<FloatplaneAdminController> _logger;
|
||||
|
||||
public FloatplaneAdminController(FloatplaneApiClient api, CatalogService catalog, DeliveryResolver delivery, ILogger<FloatplaneAdminController> logger)
|
||||
public FloatplaneAdminController(FloatplaneApiClient api, FloatplaneAuthService auth, CatalogService catalog, DeliveryResolver delivery, ILogger<FloatplaneAdminController> logger)
|
||||
{
|
||||
_api = api;
|
||||
_auth = auth;
|
||||
_catalog = catalog;
|
||||
_delivery = delivery;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// ───────────────────────────── OAuth device flow ─────────────────────────────
|
||||
|
||||
/// <summary>Starts an OAuth device login with the given client id (saved to the configuration).</summary>
|
||||
[HttpPost("OAuth/Start")]
|
||||
public async Task<ActionResult<DeviceLoginStart>> StartOAuth([FromBody] OAuthStartRequest request)
|
||||
{
|
||||
var clientId = request.ClientId?.Trim() ?? string.Empty;
|
||||
if (clientId.Length == 0)
|
||||
{
|
||||
return BadRequest("Client id is empty");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var start = await _auth.StartDeviceLoginAsync(clientId, HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
var cfg = Plugin.Instance?.Configuration;
|
||||
if (cfg != null && cfg.OAuthClientId != clientId)
|
||||
{
|
||||
cfg.OAuthClientId = clientId;
|
||||
Plugin.Instance!.SaveConfiguration();
|
||||
}
|
||||
|
||||
start.DeviceCode = string.Empty; // never hand the device code to the browser
|
||||
return start;
|
||||
}
|
||||
catch (FloatplaneApiException ex)
|
||||
{
|
||||
_logger.LogWarning("OAuth device start failed: {Error} {Message}", ex.ErrorName, ex.Message);
|
||||
return StatusCode(502, $"{ex.ErrorName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Polls the pending device login once.</summary>
|
||||
[HttpGet("OAuth/Poll")]
|
||||
public async Task<ActionResult<DeviceLoginStatus>> PollOAuth()
|
||||
{
|
||||
var status = await _auth.PollDeviceLoginAsync(HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
if (status.State == "done")
|
||||
{
|
||||
_catalog.Invalidate();
|
||||
try
|
||||
{
|
||||
var self = await _api.GetSelfAsync(HttpContext.RequestAborted).ConfigureAwait(false);
|
||||
var cfg = Plugin.Instance?.Configuration;
|
||||
if (cfg != null)
|
||||
{
|
||||
cfg.SessionUsername = self.Username;
|
||||
Plugin.Instance!.SaveConfiguration();
|
||||
}
|
||||
|
||||
status.Message = "Logged in as " + self.Username;
|
||||
}
|
||||
catch (FloatplaneApiException ex)
|
||||
{
|
||||
status.Message = "Logged in, but user lookup failed: " + ex.Message;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/// <summary>Drops the stored OAuth tokens.</summary>
|
||||
[HttpPost("OAuth/Logout")]
|
||||
public ActionResult<PluginStatus> LogoutOAuth()
|
||||
{
|
||||
FloatplaneAuthService.Logout();
|
||||
_catalog.Invalidate();
|
||||
return GetStatus();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
@@ -78,6 +150,8 @@ public class FloatplaneAdminController : ControllerBase
|
||||
{
|
||||
Version = typeof(Plugin).Assembly.GetName().Version?.ToString() ?? "?",
|
||||
HasSessionCookie = _api.HasSession,
|
||||
SessionKind = _api.SessionKind,
|
||||
OAuthClientId = Plugin.Instance?.Configuration.OAuthClientId ?? string.Empty,
|
||||
SessionState = _catalog.SessionState ?? "unknown",
|
||||
SessionUsername = Plugin.Instance?.Configuration.SessionUsername ?? string.Empty,
|
||||
LastError = _catalog.LastError,
|
||||
@@ -131,6 +205,11 @@ public class FloatplaneAdminController : ControllerBase
|
||||
=> _api.GetTagsAsync(creatorId, HttpContext.RequestAborted);
|
||||
}
|
||||
|
||||
public class OAuthStartRequest
|
||||
{
|
||||
public string? ClientId { get; set; }
|
||||
}
|
||||
|
||||
public class SessionValidationRequest
|
||||
{
|
||||
public string? Cookie { get; set; }
|
||||
@@ -155,6 +234,11 @@ public class PluginStatus
|
||||
|
||||
public bool HasSessionCookie { get; set; }
|
||||
|
||||
/// <summary>"oauth", "cookie" or "none".</summary>
|
||||
public string SessionKind { get; set; } = string.Empty;
|
||||
|
||||
public string OAuthClientId { get; set; } = string.Empty;
|
||||
|
||||
public string SessionState { get; set; } = string.Empty;
|
||||
|
||||
public string SessionUsername { get; set; } = string.Empty;
|
||||
|
||||
Reference in New Issue
Block a user