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;
///
/// 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.
///
public class FloatplaneSyncTask : IScheduledTask, IConfigurableScheduledTask
{
/// Bump when Resources/logo.png changes so existing installs get the new channel image.
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 _logger;
public FloatplaneSyncTask(CatalogService catalog, SortNameUpdater sortNames, ILibraryManager libraryManager, IProviderManager providerManager, IFileSystem fileSystem, ILogger logger)
{
_catalog = catalog;
_sortNames = sortNames;
_libraryManager = libraryManager;
_providerManager = providerManager;
_fileSystem = fileSystem;
_logger = logger;
}
///
public string Name => "Refresh Floatplane catalogue";
///
public string Key => "FloatplaneSync";
///
public string Description => "Reloads subscribed creators, their channels and the newest videos from Floatplane.";
///
public string Category => "Floatplane";
///
public bool IsHidden => false;
///
public bool IsEnabled => true;
///
public bool IsLogged => true;
///
public async Task ExecuteAsync(IProgress 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);
}
///
/// 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 .
///
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");
}
}
///
public IEnumerable 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 }
];
}
}