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 } ]; } }