Files
claude bf476134c2
Some checks failed
release / build-release (push) Has been cancelled
v0.2: All videos folder, shared watched state, release-order sorting, official icon, OAuth plumbing, docs
- 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>
2026-10-25 16:36:32 +01:00

121 lines
4.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Plugin.Floatplane.Channel;
/// <summary>
/// Every Jellyfin client lists a channel folder sorted by name by default, and the web
/// client stores a sort choice per folder. To get release order without touching every
/// folder, each video's <c>ForcedSortName</c> is set to its release time reversed
/// (<c>(10^13 - unixMs)</c>, zero-padded), so "Name ascending" = newest first.
/// Runs a few seconds after a listing (the items only exist in the database after
/// Jellyfin has synced them) and during the scheduled refresh.
/// </summary>
public class SortNameUpdater
{
private const long Epoch = 10_000_000_000_000; // > any unix ms this side of year 2286
private readonly ILibraryManager _libraryManager;
private readonly ILogger<SortNameUpdater> _logger;
private readonly SemaphoreSlim _gate = new(1, 1);
private int _scheduled;
public SortNameUpdater(ILibraryManager libraryManager, ILogger<SortNameUpdater> logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
/// <summary>Sort name for a release date: newest first under ascending name order.</summary>
public static string SortNameFor(DateTime? releaseDate)
{
var ms = releaseDate.HasValue ? new DateTimeOffset(DateTime.SpecifyKind(releaseDate.Value, DateTimeKind.Utc)).ToUnixTimeMilliseconds() : 0;
return (Epoch - 1 - Math.Clamp(ms, 0, Epoch - 1)).ToString("D13", CultureInfo.InvariantCulture);
}
/// <summary>Schedules one pass shortly after a listing (coalesces bursts of calls).</summary>
public void ScheduleSoon()
{
if (Interlocked.CompareExchange(ref _scheduled, 1, 0) != 0)
{
return;
}
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false);
Interlocked.Exchange(ref _scheduled, 0);
await UpdateAsync(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Interlocked.Exchange(ref _scheduled, 0);
_logger.LogWarning(ex, "Sort-name update failed");
}
});
}
/// <summary>Sets the forced sort name on every Floatplane video item that lacks the right one.</summary>
public async Task<int> UpdateAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct).ConfigureAwait(false);
try
{
var channelIds = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Channel],
Name = "Floatplane"
}).Select(c => c.Id).ToArray();
if (channelIds.Length == 0)
{
return 0;
}
var items = _libraryManager.GetItemList(new InternalItemsQuery
{
ChannelIds = channelIds,
IncludeItemTypes = [BaseItemKind.Video],
Recursive = true
});
var changed = new List<BaseItem>();
foreach (var item in items)
{
if (FloatplaneChannel.PostIdOf(item.ExternalId) is null)
{
continue;
}
var wanted = SortNameFor(item.PremiereDate ?? item.DateCreated);
if (!string.Equals(item.ForcedSortName, wanted, StringComparison.Ordinal))
{
item.ForcedSortName = wanted;
changed.Add(item);
}
}
if (changed.Count > 0)
{
await _libraryManager.UpdateItemsAsync(changed, changed[0].GetParent(), ItemUpdateType.MetadataEdit, ct).ConfigureAwait(false);
_logger.LogInformation("Set release-order sort names on {Count} Floatplane videos", changed.Count);
}
return changed.Count;
}
finally
{
_gate.Release();
}
}
}