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

- 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:
2026-10-25 16:36:32 +01:00
parent 2a883c02de
commit bf476134c2
33 changed files with 1478 additions and 155 deletions

View File

@@ -50,6 +50,9 @@ public class CatalogService
public DateTimeOffset LastRefresh => _creatorsFetched;
/// <summary>"oauth", "cookie" or "none".</summary>
public string SessionKind => _api.SessionKind;
/// <summary>Session state as last observed (null = not checked yet).</summary>
public string? SessionState { get; private set; }
@@ -83,7 +86,16 @@ public class CatalogService
var done = 0;
foreach (var creator in creators)
{
var channelIds = creator.Channels.Count > 0 ? creator.Channels.Select(c => c.Id).ToList() : new List<string> { string.Empty };
var channelIds = creator.Channels.Select(c => c.Id).ToList();
try
{
await GetAllPostsAsync(creator.Id, ct).ConfigureAwait(false);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Could not warm all-videos list for {Creator}", creator.Urlname);
}
foreach (var channelId in channelIds)
{
ct.ThrowIfCancellationRequested();
@@ -240,6 +252,22 @@ public class CatalogService
return posts;
}
/// <summary>Newest video posts across all of a creator's channels (the "All videos" folder).</summary>
public async Task<IReadOnlyList<PostModel>> GetAllPostsAsync(string creatorId, CancellationToken ct)
{
var key = creatorId + "|*";
var ttl = TimeSpan.FromMinutes(Math.Max(1, _config().CatalogCacheMinutes));
if (_posts.TryGetValue(key, out var cached) && DateTimeOffset.UtcNow - cached.Fetched < ttl)
{
return cached.Posts;
}
var max = Math.Clamp(_config().MaxPostsAllVideos, 20, 5000);
var posts = await FetchPostsAsync(creatorId, string.Empty, max, ct).ConfigureAwait(false);
_posts[key] = new CachedPosts(posts, DateTimeOffset.UtcNow);
return posts;
}
/// <summary>Pages through /content/creator until <paramref name="max"/> posts or the end.</summary>
public async Task<IReadOnlyList<PostModel>> FetchPostsAsync(string creatorId, string channelId, int max, CancellationToken ct, string? search = null)
{