diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 28bb29df85..4b68f21d55 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO
}
}
+ ///
+ public void MoveDirectory(string source, string destination)
+ {
+ try
+ {
+ Directory.Move(source, destination);
+ }
+ catch (IOException)
+ {
+ // Cross device move requires a copy
+ Directory.CreateDirectory(destination);
+ foreach (string file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
+ }
+
+ Directory.Delete(source, true);
+ }
+ }
+
///
/// Returns a object for the specified file or directory path.
///
@@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO
}
}
- ///
- /// Gets the creation time UTC.
- ///
- /// The path.
- /// DateTime.
+ ///
public virtual DateTime GetCreationTimeUtc(string path)
{
return GetCreationTimeUtc(GetFileSystemInfo(path));
@@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO
}
}
- ///
- /// Gets the last write time UTC.
- ///
- /// The path.
- /// DateTime.
+ ///
public virtual DateTime GetLastWriteTimeUtc(string path)
{
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
@@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO
File.SetAttributes(path, attributes);
}
- ///
- /// Swaps the files.
- ///
- /// The file1.
- /// The file2.
+ ///
public virtual void SwapFiles(string file1, string file2)
{
ArgumentException.ThrowIfNullOrEmpty(file1);
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index d1410ef5e6..d248fc303b 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -131,5 +131,7 @@
"TaskKeyframeExtractor": "Keyframe Extractor",
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists",
- "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist."
+ "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.",
+ "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
+ "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
}
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
index 60d49af9e3..c1ff0f3401 100644
--- a/Jellyfin.Api/Controllers/TrickplayController.cs
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
- public ActionResult GetTrickplayTileImage(
+ public async Task GetTrickplayTileImageAsync(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
@@ -92,8 +92,9 @@ public class TrickplayController : BaseJellyfinApiController
return NotFound();
}
- var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
- if (System.IO.File.Exists(path))
+ var saveWithMedia = _libraryManager.GetLibraryOptions(item).SaveTrickplayWithMedia;
+ var path = await _trickplayManager.GetTrickplayTilePathAsync(item, width, index, saveWithMedia).ConfigureAwait(false);
+ if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
Response.Headers.ContentDisposition = "attachment";
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
index bb32b7c20e..861037c1fe 100644
--- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
+++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
@@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager
}
///
- public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+ public async Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
+ {
+ var options = _config.Configuration.TrickplayOptions;
+ if (!CanGenerateTrickplay(video, options.Interval))
+ {
+ return;
+ }
+
+ var existingTrickplayResolutions = await GetTrickplayResolutions(video.Id).ConfigureAwait(false);
+ foreach (var resolution in existingTrickplayResolutions)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var existingResolution = resolution.Key;
+ var tileWidth = resolution.Value.TileWidth;
+ var tileHeight = resolution.Value.TileHeight;
+ var shouldBeSavedWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
+ var localOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, false);
+ var mediaOutputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, existingResolution, true);
+ if (shouldBeSavedWithMedia && Directory.Exists(localOutputDir))
+ {
+ var localDirFiles = Directory.GetFiles(localOutputDir);
+ var mediaDirExists = Directory.Exists(mediaOutputDir);
+ if (localDirFiles.Length > 0 && ((mediaDirExists && Directory.GetFiles(mediaOutputDir).Length == 0) || !mediaDirExists))
+ {
+ // Move images from local dir to media dir
+ MoveContent(localOutputDir, mediaOutputDir);
+ _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, mediaOutputDir);
+ }
+ }
+ else if (Directory.Exists(mediaOutputDir))
+ {
+ var mediaDirFiles = Directory.GetFiles(mediaOutputDir);
+ var localDirExists = Directory.Exists(localOutputDir);
+ if (mediaDirFiles.Length > 0 && ((localDirExists && Directory.GetFiles(localOutputDir).Length == 0) || !localDirExists))
+ {
+ // Move images from media dir to local dir
+ MoveContent(mediaOutputDir, localOutputDir);
+ _logger.LogInformation("Moved trickplay images for {ItemName} to {Location}", video.Name, localOutputDir);
+ }
+ }
+ }
+ }
+
+ private void MoveContent(string sourceFolder, string destinationFolder)
+ {
+ _fileSystem.MoveDirectory(sourceFolder, destinationFolder);
+ var parent = Directory.GetParent(sourceFolder);
+ if (parent is not null)
+ {
+ var parentContent = Directory.GetDirectories(parent.FullName);
+ if (parentContent.Length == 0)
+ {
+ Directory.Delete(parent.FullName);
+ }
+ }
+ }
+
+ ///
+ public async Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken)
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
@@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager
replace,
width,
options,
+ libraryOptions,
cancellationToken).ConfigureAwait(false);
}
}
@@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager
bool replace,
int width,
TrickplayOptions options,
+ LibraryOptions? libraryOptions,
CancellationToken cancellationToken)
{
if (!CanGenerateTrickplay(video, options.Interval))
@@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
}
- var outputDir = GetTrickplayDirectory(video, actualWidth);
+ var tileWidth = options.TileWidth;
+ var tileHeight = options.TileHeight;
+ var saveWithMedia = libraryOptions is null ? false : libraryOptions.SaveTrickplayWithMedia;
+ var outputDir = GetTrickplayDirectory(video, tileWidth, tileHeight, actualWidth, saveWithMedia);
- if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
+ // Import existing trickplay tiles
+ if (!replace && Directory.Exists(outputDir))
{
- _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
- return;
+ var existingFiles = Directory.GetFiles(outputDir);
+ if (existingFiles.Length > 0)
+ {
+ var hasTrickplayResolution = await HasTrickplayResolutionAsync(video.Id, actualWidth).ConfigureAwait(false);
+ if (hasTrickplayResolution)
+ {
+ _logger.LogDebug("Found existing trickplay files for {ItemId}.", video.Id);
+ return;
+ }
+
+ // Import tiles
+ var localTrickplayInfo = new TrickplayInfo
+ {
+ ItemId = video.Id,
+ Width = width,
+ Interval = options.Interval,
+ TileWidth = options.TileWidth,
+ TileHeight = options.TileHeight,
+ ThumbnailCount = existingFiles.Length,
+ Height = 0,
+ Bandwidth = 0
+ };
+
+ foreach (var tile in existingFiles)
+ {
+ var image = _imageEncoder.GetImageSize(tile);
+ localTrickplayInfo.Height = Math.Max(localTrickplayInfo.Height, image.Height);
+ var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tile).Length * 8 / localTrickplayInfo.TileWidth / localTrickplayInfo.TileHeight / (localTrickplayInfo.Interval / 1000));
+ localTrickplayInfo.Bandwidth = Math.Max(localTrickplayInfo.Bandwidth, bitrate);
+ }
+
+ await SaveTrickplayInfo(localTrickplayInfo).ConfigureAwait(false);
+
+ _logger.LogDebug("Imported existing trickplay files for {ItemId}.", video.Id);
+ return;
+ }
}
+ // Generate trickplay tiles
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
@@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager
}
///
- public TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir)
+ public TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir)
{
if (images.Count == 0)
{
@@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
- imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
+ imageOptions.InputPaths = images.Skip(i * thumbnailsPerTile).Take(Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile))).ToList();
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
@@ -289,7 +388,7 @@ public class TrickplayManager : ITrickplayManager
Directory.Delete(outputDir, true);
}
- MoveDirectory(workDir, outputDir);
+ _fileSystem.MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
@@ -355,6 +454,24 @@ public class TrickplayManager : ITrickplayManager
return trickplayResolutions;
}
+ ///
+ public async Task> GetTrickplayItemsAsync()
+ {
+ List trickplayItems;
+
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
+ {
+ trickplayItems = await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Select(i => i.ItemId)
+ .ToListAsync()
+ .ConfigureAwait(false);
+ }
+
+ return trickplayItems;
+ }
+
///
public async Task SaveTrickplayInfo(TrickplayInfo info)
{
@@ -392,9 +509,15 @@ public class TrickplayManager : ITrickplayManager
}
///
- public string GetTrickplayTilePath(BaseItem item, int width, int index)
+ public async Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia)
{
- return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+ var trickplayResolutions = await GetTrickplayResolutions(item.Id).ConfigureAwait(false);
+ if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+ {
+ return Path.Combine(GetTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, width, saveWithMedia), index + ".jpg");
+ }
+
+ return string.Empty;
}
///
@@ -470,29 +593,33 @@ public class TrickplayManager : ITrickplayManager
return null;
}
- private string GetTrickplayDirectory(BaseItem item, int? width = null)
+ ///
+ public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
{
- var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
-
- return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ var path = saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ var subdirectory = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} - {1}x{2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ tileWidth.ToString(CultureInfo.InvariantCulture),
+ tileHeight.ToString(CultureInfo.InvariantCulture));
+
+ return Path.Combine(path, subdirectory);
}
- private void MoveDirectory(string source, string destination)
+ private async Task HasTrickplayResolutionAsync(Guid itemId, int width)
{
- try
- {
- Directory.Move(source, destination);
- }
- catch (IOException)
+ var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+ await using (dbContext.ConfigureAwait(false))
{
- // Cross device move requires a copy
- Directory.CreateDirectory(destination);
- foreach (string file in Directory.GetFiles(source))
- {
- File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
- }
-
- Directory.Delete(source, true);
+ return await dbContext.TrickplayInfos
+ .AsNoTracking()
+ .Where(i => i.ItemId.Equals(itemId))
+ .AnyAsync(i => i.Width == width)
+ .ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs
index 81fecc9a13..8682f28e04 100644
--- a/Jellyfin.Server/Migrations/MigrationRunner.cs
+++ b/Jellyfin.Server/Migrations/MigrationRunner.cs
@@ -46,6 +46,7 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
+ typeof(Routines.MoveTrickplayFiles)
};
///
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
new file mode 100644
index 0000000000..e8abee95ad
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Globalization;
+using System.IO;
+using DiscUtils;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+///
+/// Migration to move trickplay files to the new directory.
+///
+public class MoveTrickplayFiles : IMigrationRoutine
+{
+ private readonly ITrickplayManager _trickplayManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly ILibraryManager _libraryManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager)
+ {
+ _trickplayManager = trickplayManager;
+ _fileSystem = fileSystem;
+ _libraryManager = libraryManager;
+ }
+
+ ///
+ public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
+
+ ///
+ public string Name => "MoveTrickplayFiles";
+
+ ///
+ public bool PerformOnNewInstall => true;
+
+ ///
+ public void Perform()
+ {
+ var trickplayItems = _trickplayManager.GetTrickplayItemsAsync().GetAwaiter().GetResult();
+ foreach (var itemId in trickplayItems)
+ {
+ var resolutions = _trickplayManager.GetTrickplayResolutions(itemId).GetAwaiter().GetResult();
+ var item = _libraryManager.GetItemById(itemId);
+ if (item is null)
+ {
+ continue;
+ }
+
+ foreach (var resolution in resolutions)
+ {
+ var oldPath = GetOldTrickplayDirectory(item, resolution.Key);
+ var newPath = _trickplayManager.GetTrickplayDirectory(item, resolution.Value.TileWidth, resolution.Value.TileHeight, resolution.Value.Width, false);
+ if (_fileSystem.DirectoryExists(oldPath))
+ {
+ _fileSystem.MoveDirectory(oldPath, newPath);
+ }
+ }
+ }
+ }
+
+ private string GetOldTrickplayDirectory(BaseItem item, int? width = null)
+ {
+ var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 9e91a8bcd7..0bab2a6b9c 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.Providers
IsAutomated = copy.IsAutomated;
ImageRefreshMode = copy.ImageRefreshMode;
ReplaceAllImages = copy.ReplaceAllImages;
+ RegenerateTrickplay = copy.RegenerateTrickplay;
ReplaceImages = copy.ReplaceImages;
SearchResult = copy.SearchResult;
RemoveOldMetadata = copy.RemoveOldMetadata;
@@ -47,6 +48,12 @@ namespace MediaBrowser.Controller.Providers
///
public bool ReplaceAllMetadata { get; set; }
+ ///
+ /// Gets or sets a value indicating whether all existing trickplay images should be overwritten
+ /// when paired with MetadataRefreshMode=FullRefresh.
+ ///
+ public bool RegenerateTrickplay { get; set; }
+
public MetadataRefreshMode MetadataRefreshMode { get; set; }
public RemoteSearchResult SearchResult { get; set; }
diff --git a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
index 0c41f30235..bda794aa64 100644
--- a/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
+++ b/MediaBrowser.Controller/Trickplay/ITrickplayManager.cs
@@ -18,9 +18,10 @@ public interface ITrickplayManager
///
/// The video.
/// Whether or not existing data should be replaced.
+ /// The library options.
/// CancellationToken to use for operation.
/// Task.
- Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
+ Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
///
/// Creates trickplay tiles out of individual thumbnails.
@@ -33,7 +34,7 @@ public interface ITrickplayManager
///
/// The output directory will be DELETED and replaced if it already exists.
///
- TrickplayInfo CreateTiles(List images, int width, TrickplayOptions options, string outputDir);
+ TrickplayInfo CreateTiles(IReadOnlyList images, int width, TrickplayOptions options, string outputDir);
///
/// Get available trickplay resolutions and corresponding info.
@@ -42,6 +43,12 @@ public interface ITrickplayManager
/// Map of width resolutions to trickplay tiles info.
Task> GetTrickplayResolutions(Guid itemId);
+ ///
+ /// Gets the item ids of all items with trickplay info.
+ ///
+ /// The list of item ids that have trickplay info.
+ public Task> GetTrickplayItemsAsync();
+
///
/// Saves trickplay info.
///
@@ -62,8 +69,29 @@ public interface ITrickplayManager
/// The item.
/// The width of a single thumbnail.
/// The tile's index.
+ /// Whether or not the tile should be saved next to the media file.
+ /// The absolute path.
+ Task GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia);
+
+ ///
+ /// Gets the path to a trickplay tile image.
+ ///
+ /// The item.
+ /// The amount of images for the tile width.
+ /// The amount of images for the tile height.
+ /// The width of a single thumbnail.
+ /// Whether or not the tile should be saved next to the media file.
/// The absolute path.
- string GetTrickplayTilePath(BaseItem item, int width, int index);
+ string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false);
+
+ ///
+ /// Migrates trickplay images between local and media directories.
+ ///
+ /// The video.
+ /// The library options.
+ /// CancellationToken to use for operation.
+ /// Task.
+ Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
///
/// Gets the trickplay HLS playlist.
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index b0f5c2a11f..688a6418d0 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs
@@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Configuration
EnablePhotos = true;
SaveSubtitlesWithMedia = true;
SaveLyricsWithMedia = false;
+ SaveTrickplayWithMedia = false;
PathInfos = Array.Empty();
EnableAutomaticSeriesGrouping = true;
SeasonZeroDisplayName = "Specials";
@@ -99,6 +100,9 @@ namespace MediaBrowser.Model.Configuration
[DefaultValue(false)]
public bool SaveLyricsWithMedia { get; set; }
+ [DefaultValue(false)]
+ public bool SaveTrickplayWithMedia { get; set; }
+
public string[] DisabledLyricFetchers { get; set; }
public string[] LyricFetcherOrder { get; set; }
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index ec381d4231..2085328ddc 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -33,6 +33,13 @@ namespace MediaBrowser.Model.IO
string MakeAbsolutePath(string folderPath, string filePath);
+ ///
+ /// Moves a directory to a new location.
+ ///
+ /// Source directory.
+ /// Destination directory.
+ void MoveDirectory(string source, string destination);
+
///
/// Returns a object for the specified file or directory path.
///
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
index 90c2ff8ddf..31c0eeb31e 100644
--- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
+++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs
@@ -98,7 +98,8 @@ public class TrickplayImagesTask : IScheduledTask
try
{
- await _trickplayManager.RefreshTrickplayDataAsync(video, false, cancellationToken).ConfigureAwait(false);
+ var libraryOptions = _libraryManager.GetLibraryOptions(video);
+ await _trickplayManager.RefreshTrickplayDataAsync(video, false, libraryOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
diff --git a/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
new file mode 100644
index 0000000000..c581fd26cb
--- /dev/null
+++ b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Trickplay;
+
+///
+/// Class TrickplayMoveImagesTask.
+///
+public class TrickplayMoveImagesTask : IScheduledTask
+{
+ private const int QueryPageLimit = 100;
+
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILocalizationManager _localization;
+ private readonly ITrickplayManager _trickplayManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The library manager.
+ /// The localization manager.
+ /// The trickplay manager.
+ public TrickplayMoveImagesTask(
+ ILogger logger,
+ ILibraryManager libraryManager,
+ ILocalizationManager localization,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _localization = localization;
+ _trickplayManager = trickplayManager;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskMoveTrickplayImages");
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskMoveTrickplayImagesDescription");
+
+ ///
+ public string Key => "MoveTrickplayImages";
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ ///
+ public IEnumerable GetDefaultTriggers() => [];
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ var trickplayItems = await _trickplayManager.GetTrickplayItemsAsync().ConfigureAwait(false);
+ var query = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false,
+ Recursive = true,
+ Limit = QueryPageLimit
+ };
+
+ var numberOfVideos = _libraryManager.GetCount(query);
+
+ var startIndex = 0;
+ var numComplete = 0;
+
+ while (startIndex < numberOfVideos)
+ {
+ query.StartIndex = startIndex;
+ var videos = _libraryManager.GetItemList(query).OfType