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