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>
136 lines
5.2 KiB
C#
136 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Linq;
|
|
using Jellyfin.Data.Enums;
|
|
using Jellyfin.Plugin.Floatplane.Channel;
|
|
using MediaBrowser.Controller.Entities;
|
|
using MediaBrowser.Controller.Library;
|
|
using MediaBrowser.Controller.Providers;
|
|
using MediaBrowser.Model.Entities;
|
|
using MediaBrowser.Model.IO;
|
|
using MediaBrowser.Model.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.Floatplane.Tasks;
|
|
|
|
/// <summary>
|
|
/// Scheduled task: refresh creators/channels and warm the newest posts, then bump the
|
|
/// channel cache key so Jellyfin re-lists on the next browse. Found by type scanning.
|
|
/// </summary>
|
|
public class FloatplaneSyncTask : IScheduledTask, IConfigurableScheduledTask
|
|
{
|
|
/// <summary>Bump when Resources/logo.png changes so existing installs get the new channel image.</summary>
|
|
public const int ChannelImageVersion = 3;
|
|
|
|
private readonly CatalogService _catalog;
|
|
private readonly SortNameUpdater _sortNames;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly IProviderManager _providerManager;
|
|
private readonly IFileSystem _fileSystem;
|
|
private readonly ILogger<FloatplaneSyncTask> _logger;
|
|
|
|
public FloatplaneSyncTask(CatalogService catalog, SortNameUpdater sortNames, ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem, ILogger<FloatplaneSyncTask> logger)
|
|
{
|
|
_catalog = catalog;
|
|
_sortNames = sortNames;
|
|
_libraryManager = libraryManager;
|
|
_providerManager = providerManager;
|
|
_fileSystem = fileSystem;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public string Name => "Refresh Floatplane catalogue";
|
|
|
|
/// <inheritdoc />
|
|
public string Key => "FloatplaneSync";
|
|
|
|
/// <inheritdoc />
|
|
public string Description => "Reloads subscribed creators, their channels and the newest videos from Floatplane.";
|
|
|
|
/// <inheritdoc />
|
|
public string Category => "Floatplane";
|
|
|
|
/// <inheritdoc />
|
|
public bool IsHidden => false;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsEnabled => true;
|
|
|
|
/// <inheritdoc />
|
|
public bool IsLogged => true;
|
|
|
|
/// <inheritdoc />
|
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Floatplane sync started");
|
|
await _catalog.RefreshAsync(progress, cancellationToken).ConfigureAwait(false);
|
|
_logger.LogInformation("Floatplane sync finished: {Creators} creators, generation {Gen}", _catalog.Creators.Count, _catalog.Generation);
|
|
await UpdateChannelImageIfNeededAsync(cancellationToken).ConfigureAwait(false);
|
|
await _sortNames.UpdateAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Jellyfin stores the channel's image once, so when the plugin ships a new logo the
|
|
/// stored Primary is replaced explicitly (a metadata refresh does not replace it), and
|
|
/// Thumb/Backdrop images from earlier versions are removed because video cards inherit
|
|
/// the parent thumb in "Continue watching". Runs once per <see cref="ChannelImageVersion"/>.
|
|
/// </summary>
|
|
private async Task UpdateChannelImageIfNeededAsync(CancellationToken ct)
|
|
{
|
|
var cfg = Plugin.Instance?.Configuration;
|
|
if (cfg == null || cfg.ChannelImageVersion >= ChannelImageVersion)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var channels = _libraryManager.GetItemList(new InternalItemsQuery
|
|
{
|
|
IncludeItemTypes = [BaseItemKind.Channel],
|
|
Name = "Floatplane"
|
|
});
|
|
foreach (var channel in channels)
|
|
{
|
|
foreach (var type in new[] { ImageType.Thumb, ImageType.Backdrop, ImageType.Logo })
|
|
{
|
|
while (channel.HasImage(type))
|
|
{
|
|
await channel.DeleteImageAsync(type, 0).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
using var stream = typeof(Plugin).Assembly.GetManifestResourceStream("Jellyfin.Plugin.Floatplane.Resources.logo.png");
|
|
if (stream != null)
|
|
{
|
|
await _providerManager.SaveImage(channel, stream, "image/png", ImageType.Primary, null, ct).ConfigureAwait(false);
|
|
await _libraryManager.UpdateItemAsync(channel, channel.GetParent(), ItemUpdateType.ImageUpdate, ct).ConfigureAwait(false);
|
|
}
|
|
|
|
_logger.LogInformation("Updated channel image for {Name} (logo v{Version})", channel.Name, ChannelImageVersion);
|
|
}
|
|
|
|
cfg.ChannelImageVersion = ChannelImageVersion;
|
|
Plugin.Instance!.SaveConfiguration();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not update the channel image");
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
|
|
{
|
|
var hours = Math.Clamp(Plugin.Instance?.Configuration.SyncIntervalHours ?? 6, 1, 168);
|
|
return
|
|
[
|
|
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerStartup },
|
|
new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(hours).Ticks }
|
|
];
|
|
}
|
|
}
|