From e1a5c16404ac134b132df1ec14998678edda3496 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Tue, 3 Jun 2025 23:25:09 +0200 Subject: [PATCH] Prune trickplay data on regenerate and scan (#14085) --- .../Trickplay/TrickplayManager.cs | 105 +++++++++++++----- .../Trickplay/TrickplayImagesTask.cs | 10 +- 2 files changed, 82 insertions(+), 33 deletions(-) diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index b2df86244b..d7c2845a65 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -7,6 +7,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; +using J2N.Collections.Generic.Extensions; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Common.Configuration; @@ -80,7 +81,7 @@ public class TrickplayManager : ITrickplayManager public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions libraryOptions, CancellationToken cancellationToken) { var options = _config.Configuration.TrickplayOptions; - if (!CanGenerateTrickplay(video, options.Interval, libraryOptions)) + if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction || !CanGenerateTrickplay(video, options.Interval)) { return; } @@ -137,25 +138,84 @@ public class TrickplayManager : ITrickplayManager /// public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions libraryOptions, CancellationToken cancellationToken) { - _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); - var options = _config.Configuration.TrickplayOptions; - if (options.Interval < 1000) + if (!CanGenerateTrickplay(video, options.Interval) || libraryOptions is null) { - _logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval); - options.Interval = 1000; + return; } - foreach (var width in options.WidthResolutions) + var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - cancellationToken.ThrowIfCancellationRequested(); - await RefreshTrickplayDataInternal( - video, - replace, - width, - options, - libraryOptions, - cancellationToken).ConfigureAwait(false); + var saveWithMedia = libraryOptions.SaveTrickplayWithMedia; + var trickplayDirectory = _pathManager.GetTrickplayDirectory(video, saveWithMedia); + if (!libraryOptions.EnableTrickplayImageExtraction || replace) + { + // Prune existing data + if (Directory.Exists(trickplayDirectory)) + { + try + { + Directory.Delete(trickplayDirectory, true); + } + catch (Exception ex) + { + _logger.LogWarning("Unable to clear trickplay directory: {Directory}: {Exception}", trickplayDirectory, ex); + } + } + + await dbContext.TrickplayInfos + .Where(i => i.ItemId.Equals(video.Id)) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + + if (!replace) + { + return; + } + } + + _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace); + + if (options.Interval < 1000) + { + _logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval); + options.Interval = 1000; + } + + foreach (var width in options.WidthResolutions) + { + cancellationToken.ThrowIfCancellationRequested(); + await RefreshTrickplayDataInternal( + video, + replace, + width, + options, + saveWithMedia, + cancellationToken).ConfigureAwait(false); + } + + // Cleanup old trickplay files + var existingFolders = Directory.GetDirectories(trickplayDirectory).ToList(); + var trickplayInfos = await dbContext.TrickplayInfos + .AsNoTracking() + .Where(i => i.ItemId.Equals(video.Id)) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + var expectedFolders = trickplayInfos.Select(i => GetTrickplayDirectory(video, i.TileWidth, i.TileHeight, i.Width, saveWithMedia)).ToList(); + var foldersToRemove = existingFolders.Except(expectedFolders); + foreach (var folder in foldersToRemove) + { + try + { + _logger.LogWarning("Pruning trickplay files for {Item}", video.Path); + Directory.Delete(folder, true); + } + catch (Exception ex) + { + _logger.LogWarning("Unable to remove trickplay directory: {Directory}: {Exception}", folder, ex); + } + } } } @@ -164,14 +224,9 @@ public class TrickplayManager : ITrickplayManager bool replace, int width, TrickplayOptions options, - LibraryOptions libraryOptions, + bool saveWithMedia, CancellationToken cancellationToken) { - if (!CanGenerateTrickplay(video, options.Interval, libraryOptions)) - { - return; - } - var imgTempDir = string.Empty; using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false)) @@ -215,7 +270,6 @@ public class TrickplayManager : ITrickplayManager var tileWidth = options.TileWidth; var tileHeight = options.TileHeight; - var saveWithMedia = libraryOptions is not null && libraryOptions.SaveTrickplayWithMedia; var outputDir = new DirectoryInfo(GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia)); // Import existing trickplay tiles @@ -402,7 +456,7 @@ public class TrickplayManager : ITrickplayManager return trickplayInfo; } - private bool CanGenerateTrickplay(Video video, int interval, LibraryOptions libraryOptions) + private bool CanGenerateTrickplay(Video video, int interval) { var videoType = video.VideoType; if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay) @@ -430,11 +484,6 @@ public class TrickplayManager : ITrickplayManager return false; } - if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction) - { - return false; - } - // Can't extract images if there are no video streams return video.GetMediaStreams().Count > 0; } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 4310f93d4b..81dcbf893e 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -59,14 +59,14 @@ public class TrickplayImagesTask : IScheduledTask /// public IEnumerable GetDefaultTriggers() { - return new[] - { + return + [ new TaskTriggerInfo { Type = TaskTriggerInfoType.DailyTrigger, TimeOfDayTicks = TimeSpan.FromHours(3).Ticks } - }; + ]; } /// @@ -74,8 +74,8 @@ public class TrickplayImagesTask : IScheduledTask { var query = new InternalItemsQuery { - MediaTypes = new[] { MediaType.Video }, - SourceTypes = new[] { SourceType.Library }, + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], IsVirtualItem = false, IsFolder = false, Recursive = true,