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>
121 lines
4.2 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|