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;
///
/// 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 ForcedSortName is set to its release time reversed
/// ((10^13 - unixMs), 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.
///
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 _logger;
private readonly SemaphoreSlim _gate = new(1, 1);
private int _scheduled;
public SortNameUpdater(ILibraryManager libraryManager, ILogger logger)
{
_libraryManager = libraryManager;
_logger = logger;
}
/// Sort name for a release date: newest first under ascending name order.
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);
}
/// Schedules one pass shortly after a listing (coalesces bursts of calls).
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");
}
});
}
/// Sets the forced sort name on every Floatplane video item that lacks the right one.
public async Task 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();
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();
}
}
}