Prune trickplay data on regenerate and scan (#14085)

This commit is contained in:
Tim Eisele 2025-06-03 23:25:09 +02:00 committed by GitHub
parent 44b5de1568
commit e1a5c16404
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 82 additions and 33 deletions

View File

@ -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
/// <inheritdoc />
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;
}

View File

@ -59,14 +59,14 @@ public class TrickplayImagesTask : IScheduledTask
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
{
return
[
new TaskTriggerInfo
{
Type = TaskTriggerInfoType.DailyTrigger,
TimeOfDayTicks = TimeSpan.FromHours(3).Ticks
}
};
];
}
/// <inheritdoc />
@ -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,