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:
@@ -27,13 +27,15 @@ public class FloatplaneApiClient
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<FloatplaneApiClient> _logger;
|
||||
private readonly Func<PluginConfiguration> _config;
|
||||
private readonly FloatplaneAuthService? _auth;
|
||||
|
||||
public FloatplaneApiClient(IHttpClientFactory httpClientFactory, ILogger<FloatplaneApiClient> logger)
|
||||
public FloatplaneApiClient(IHttpClientFactory httpClientFactory, ILogger<FloatplaneApiClient> logger, FloatplaneAuthService auth)
|
||||
: this(httpClientFactory, logger, () => Plugin.Instance?.Configuration ?? new PluginConfiguration())
|
||||
{
|
||||
_auth = auth;
|
||||
}
|
||||
|
||||
/// <summary>Constructor for tests: configuration is supplied explicitly.</summary>
|
||||
/// <summary>Constructor for tests: configuration is supplied explicitly, no OAuth.</summary>
|
||||
public FloatplaneApiClient(IHttpClientFactory httpClientFactory, ILogger<FloatplaneApiClient> logger, Func<PluginConfiguration> config)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
@@ -41,8 +43,11 @@ public class FloatplaneApiClient
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>True when a session cookie is configured (not necessarily valid).</summary>
|
||||
public bool HasSession => !string.IsNullOrWhiteSpace(_config().SessionCookie);
|
||||
/// <summary>True when a session (cookie or OAuth refresh token) is configured — not necessarily valid.</summary>
|
||||
public bool HasSession => !string.IsNullOrWhiteSpace(_config().SessionCookie) || !string.IsNullOrEmpty(_config().OAuthRefreshToken);
|
||||
|
||||
/// <summary>"oauth", "cookie" or "none".</summary>
|
||||
public string SessionKind => !string.IsNullOrEmpty(_config().OAuthRefreshToken) ? "oauth" : !string.IsNullOrWhiteSpace(_config().SessionCookie) ? "cookie" : "none";
|
||||
|
||||
// ───────────────────────────── auth / user ─────────────────────────────
|
||||
|
||||
@@ -177,7 +182,7 @@ public class FloatplaneApiClient
|
||||
public async Task PostProgressAsync(string videoId, int seconds, CancellationToken ct)
|
||||
{
|
||||
var body = JsonSerializer.Serialize(new { id = videoId, contentType = "video", progress = seconds });
|
||||
using var request = BuildRequest(HttpMethod.Post, BaseUrl + "/api/v3/content/progress", null, null);
|
||||
using var request = await BuildRequestAsync(HttpMethod.Post, BaseUrl + "/api/v3/content/progress", null, null, ct).ConfigureAwait(false);
|
||||
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
using var response = await SendAsync(request, ct).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, ct).ConfigureAwait(false);
|
||||
@@ -205,7 +210,7 @@ public class FloatplaneApiClient
|
||||
/// </summary>
|
||||
public async Task<byte[]> GetWatchKeyAsync(string keyUrl, CancellationToken ct)
|
||||
{
|
||||
using var request = BuildRequest(HttpMethod.Get, keyUrl, null, null);
|
||||
using var request = await BuildRequestAsync(HttpMethod.Get, keyUrl, null, null, ct).ConfigureAwait(false);
|
||||
request.Headers.Referrer = new Uri(BaseUrl + "/");
|
||||
using var response = await SendAsync(request, ct).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, ct).ConfigureAwait(false);
|
||||
@@ -228,6 +233,23 @@ public class FloatplaneApiClient
|
||||
|
||||
// ───────────────────────────── plumbing ─────────────────────────────
|
||||
|
||||
/// <summary>Builds a request with auth: explicit cookie > OAuth bearer > configured cookie.</summary>
|
||||
internal async Task<HttpRequestMessage> BuildRequestAsync(HttpMethod method, string url, Dictionary<string, string>? query, string? cookieOverride, CancellationToken ct)
|
||||
{
|
||||
var request = BuildRequest(method, url, query, cookieOverride);
|
||||
if (cookieOverride == null && _auth != null && !string.IsNullOrEmpty(_config().OAuthRefreshToken))
|
||||
{
|
||||
var token = await _auth.GetAccessTokenAsync(ct).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
request.Headers.Remove("Cookie");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
internal HttpRequestMessage BuildRequest(HttpMethod method, string url, Dictionary<string, string>? query, string? cookieOverride)
|
||||
{
|
||||
if (query is { Count: > 0 })
|
||||
@@ -262,7 +284,7 @@ public class FloatplaneApiClient
|
||||
|
||||
private async Task<T> GetAsync<T>(string path, Dictionary<string, string>? query, CancellationToken ct, string? cookieOverride = null)
|
||||
{
|
||||
using var request = BuildRequest(HttpMethod.Get, BaseUrl + path, query, cookieOverride);
|
||||
using var request = await BuildRequestAsync(HttpMethod.Get, BaseUrl + path, query, cookieOverride, ct).ConfigureAwait(false);
|
||||
using var response = await SendAsync(request, ct).ConfigureAwait(false);
|
||||
await EnsureSuccessAsync(response, ct).ConfigureAwait(false);
|
||||
var json = await response.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user