Enhance Trickplay (#11883)
This commit is contained in:
parent
675a8a9ec9
commit
c56dbc1c44
|
@ -149,6 +149,26 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
|
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -327,11 +347,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets the creation time UTC.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <returns>DateTime.</returns>
|
|
||||||
public virtual DateTime GetCreationTimeUtc(string path)
|
public virtual DateTime GetCreationTimeUtc(string path)
|
||||||
{
|
{
|
||||||
return GetCreationTimeUtc(GetFileSystemInfo(path));
|
return GetCreationTimeUtc(GetFileSystemInfo(path));
|
||||||
|
@ -368,11 +384,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets the last write time UTC.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <returns>DateTime.</returns>
|
|
||||||
public virtual DateTime GetLastWriteTimeUtc(string path)
|
public virtual DateTime GetLastWriteTimeUtc(string path)
|
||||||
{
|
{
|
||||||
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
|
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
|
||||||
|
@ -446,11 +458,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
File.SetAttributes(path, attributes);
|
File.SetAttributes(path, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Swaps the files.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="file1">The file1.</param>
|
|
||||||
/// <param name="file2">The file2.</param>
|
|
||||||
public virtual void SwapFiles(string file1, string file2)
|
public virtual void SwapFiles(string file1, string file2)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(file1);
|
ArgumentException.ThrowIfNullOrEmpty(file1);
|
||||||
|
|
|
@ -131,5 +131,7 @@
|
||||||
"TaskKeyframeExtractor": "Keyframe Extractor",
|
"TaskKeyframeExtractor": "Keyframe Extractor",
|
||||||
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
|
"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",
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ public class TrickplayController : BaseJellyfinApiController
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public ActionResult GetTrickplayTileImage(
|
public async Task<ActionResult> GetTrickplayTileImageAsync(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] int width,
|
[FromRoute, Required] int width,
|
||||||
[FromRoute, Required] int index,
|
[FromRoute, Required] int index,
|
||||||
|
@ -92,8 +92,9 @@ public class TrickplayController : BaseJellyfinApiController
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
|
var saveWithMedia = _libraryManager.GetLibraryOptions(item).SaveTrickplayWithMedia;
|
||||||
if (System.IO.File.Exists(path))
|
var path = await _trickplayManager.GetTrickplayTilePathAsync(item, width, index, saveWithMedia).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
Response.Headers.ContentDisposition = "attachment";
|
Response.Headers.ContentDisposition = "attachment";
|
||||||
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
|
||||||
|
|
|
@ -76,7 +76,65 @@ public class TrickplayManager : ITrickplayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
||||||
|
|
||||||
|
@ -95,6 +153,7 @@ public class TrickplayManager : ITrickplayManager
|
||||||
replace,
|
replace,
|
||||||
width,
|
width,
|
||||||
options,
|
options,
|
||||||
|
libraryOptions,
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +163,7 @@ public class TrickplayManager : ITrickplayManager
|
||||||
bool replace,
|
bool replace,
|
||||||
int width,
|
int width,
|
||||||
TrickplayOptions options,
|
TrickplayOptions options,
|
||||||
|
LibraryOptions? libraryOptions,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!CanGenerateTrickplay(video, options.Interval))
|
if (!CanGenerateTrickplay(video, options.Interval))
|
||||||
|
@ -144,14 +204,53 @@ public class TrickplayManager : ITrickplayManager
|
||||||
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
|
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);
|
var existingFiles = Directory.GetFiles(outputDir);
|
||||||
return;
|
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 mediaStream = mediaSource.VideoStream;
|
||||||
var container = mediaSource.Container;
|
var container = mediaSource.Container;
|
||||||
|
|
||||||
|
@ -224,7 +323,7 @@ public class TrickplayManager : ITrickplayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
|
public TrickplayInfo CreateTiles(IReadOnlyList<string> images, int width, TrickplayOptions options, string outputDir)
|
||||||
{
|
{
|
||||||
if (images.Count == 0)
|
if (images.Count == 0)
|
||||||
{
|
{
|
||||||
|
@ -264,7 +363,7 @@ public class TrickplayManager : ITrickplayManager
|
||||||
var tilePath = Path.Combine(workDir, $"{i}.jpg");
|
var tilePath = Path.Combine(workDir, $"{i}.jpg");
|
||||||
|
|
||||||
imageOptions.OutputPath = tilePath;
|
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
|
// Generate image and use returned height for tiles info
|
||||||
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
|
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);
|
Directory.Delete(outputDir, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
MoveDirectory(workDir, outputDir);
|
_fileSystem.MoveDirectory(workDir, outputDir);
|
||||||
|
|
||||||
return trickplayInfo;
|
return trickplayInfo;
|
||||||
}
|
}
|
||||||
|
@ -355,6 +454,24 @@ public class TrickplayManager : ITrickplayManager
|
||||||
return trickplayResolutions;
|
return trickplayResolutions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<Guid>> GetTrickplayItemsAsync()
|
||||||
|
{
|
||||||
|
List<Guid> 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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SaveTrickplayInfo(TrickplayInfo info)
|
public async Task SaveTrickplayInfo(TrickplayInfo info)
|
||||||
{
|
{
|
||||||
|
@ -392,9 +509,15 @@ public class TrickplayManager : ITrickplayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string GetTrickplayTilePath(BaseItem item, int width, int index)
|
public async Task<string> 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -470,29 +593,33 @@ public class TrickplayManager : ITrickplayManager
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetTrickplayDirectory(BaseItem item, int? width = null)
|
/// <inheritdoc />
|
||||||
|
public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
var path = saveWithMedia
|
||||||
|
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
|
||||||
|
: Path.Combine(item.GetInternalMetadataPath(), "trickplay");
|
||||||
|
|
||||||
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
|
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<bool> HasTrickplayResolutionAsync(Guid itemId, int width)
|
||||||
{
|
{
|
||||||
try
|
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||||
|
await using (dbContext.ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
Directory.Move(source, destination);
|
return await dbContext.TrickplayInfos
|
||||||
}
|
.AsNoTracking()
|
||||||
catch (IOException)
|
.Where(i => i.ItemId.Equals(itemId))
|
||||||
{
|
.AnyAsync(i => i.Width == width)
|
||||||
// Cross device move requires a copy
|
.ConfigureAwait(false);
|
||||||
Directory.CreateDirectory(destination);
|
|
||||||
foreach (string file in Directory.GetFiles(source))
|
|
||||||
{
|
|
||||||
File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.Delete(source, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ namespace Jellyfin.Server.Migrations
|
||||||
typeof(Routines.AddDefaultCastReceivers),
|
typeof(Routines.AddDefaultCastReceivers),
|
||||||
typeof(Routines.UpdateDefaultPluginRepository),
|
typeof(Routines.UpdateDefaultPluginRepository),
|
||||||
typeof(Routines.FixAudioData),
|
typeof(Routines.FixAudioData),
|
||||||
|
typeof(Routines.MoveTrickplayFiles)
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
73
Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
Normal file
73
Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
Normal file
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migration to move trickplay files to the new directory.
|
||||||
|
/// </summary>
|
||||||
|
public class MoveTrickplayFiles : IMigrationRoutine
|
||||||
|
{
|
||||||
|
private readonly ITrickplayManager _trickplayManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MoveTrickplayFiles"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trickplayManager">Instance of the <see cref="ITrickplayManager"/> interface.</param>
|
||||||
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
public MoveTrickplayFiles(ITrickplayManager trickplayManager, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||||
|
{
|
||||||
|
_trickplayManager = trickplayManager;
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => "MoveTrickplayFiles";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool PerformOnNewInstall => true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ namespace MediaBrowser.Controller.Providers
|
||||||
IsAutomated = copy.IsAutomated;
|
IsAutomated = copy.IsAutomated;
|
||||||
ImageRefreshMode = copy.ImageRefreshMode;
|
ImageRefreshMode = copy.ImageRefreshMode;
|
||||||
ReplaceAllImages = copy.ReplaceAllImages;
|
ReplaceAllImages = copy.ReplaceAllImages;
|
||||||
|
RegenerateTrickplay = copy.RegenerateTrickplay;
|
||||||
ReplaceImages = copy.ReplaceImages;
|
ReplaceImages = copy.ReplaceImages;
|
||||||
SearchResult = copy.SearchResult;
|
SearchResult = copy.SearchResult;
|
||||||
RemoveOldMetadata = copy.RemoveOldMetadata;
|
RemoveOldMetadata = copy.RemoveOldMetadata;
|
||||||
|
@ -47,6 +48,12 @@ namespace MediaBrowser.Controller.Providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ReplaceAllMetadata { get; set; }
|
public bool ReplaceAllMetadata { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether all existing trickplay images should be overwritten
|
||||||
|
/// when paired with MetadataRefreshMode=FullRefresh.
|
||||||
|
/// </summary>
|
||||||
|
public bool RegenerateTrickplay { get; set; }
|
||||||
|
|
||||||
public MetadataRefreshMode MetadataRefreshMode { get; set; }
|
public MetadataRefreshMode MetadataRefreshMode { get; set; }
|
||||||
|
|
||||||
public RemoteSearchResult SearchResult { get; set; }
|
public RemoteSearchResult SearchResult { get; set; }
|
||||||
|
|
|
@ -18,9 +18,10 @@ public interface ITrickplayManager
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="video">The video.</param>
|
/// <param name="video">The video.</param>
|
||||||
/// <param name="replace">Whether or not existing data should be replaced.</param>
|
/// <param name="replace">Whether or not existing data should be replaced.</param>
|
||||||
|
/// <param name="libraryOptions">The library options.</param>
|
||||||
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken);
|
Task RefreshTrickplayDataAsync(Video video, bool replace, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates trickplay tiles out of individual thumbnails.
|
/// Creates trickplay tiles out of individual thumbnails.
|
||||||
|
@ -33,7 +34,7 @@ public interface ITrickplayManager
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The output directory will be DELETED and replaced if it already exists.
|
/// The output directory will be DELETED and replaced if it already exists.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir);
|
TrickplayInfo CreateTiles(IReadOnlyList<string> images, int width, TrickplayOptions options, string outputDir);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get available trickplay resolutions and corresponding info.
|
/// Get available trickplay resolutions and corresponding info.
|
||||||
|
@ -42,6 +43,12 @@ public interface ITrickplayManager
|
||||||
/// <returns>Map of width resolutions to trickplay tiles info.</returns>
|
/// <returns>Map of width resolutions to trickplay tiles info.</returns>
|
||||||
Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
|
Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item ids of all items with trickplay info.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The list of item ids that have trickplay info.</returns>
|
||||||
|
public Task<IReadOnlyList<Guid>> GetTrickplayItemsAsync();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves trickplay info.
|
/// Saves trickplay info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -62,8 +69,29 @@ public interface ITrickplayManager
|
||||||
/// <param name="item">The item.</param>
|
/// <param name="item">The item.</param>
|
||||||
/// <param name="width">The width of a single thumbnail.</param>
|
/// <param name="width">The width of a single thumbnail.</param>
|
||||||
/// <param name="index">The tile's index.</param>
|
/// <param name="index">The tile's index.</param>
|
||||||
|
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
|
||||||
/// <returns>The absolute path.</returns>
|
/// <returns>The absolute path.</returns>
|
||||||
string GetTrickplayTilePath(BaseItem item, int width, int index);
|
Task<string> GetTrickplayTilePathAsync(BaseItem item, int width, int index, bool saveWithMedia);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the path to a trickplay tile image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item.</param>
|
||||||
|
/// <param name="tileWidth">The amount of images for the tile width.</param>
|
||||||
|
/// <param name="tileHeight">The amount of images for the tile height.</param>
|
||||||
|
/// <param name="width">The width of a single thumbnail.</param>
|
||||||
|
/// <param name="saveWithMedia">Whether or not the tile should be saved next to the media file.</param>
|
||||||
|
/// <returns>The absolute path.</returns>
|
||||||
|
string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migrates trickplay images between local and media directories.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="video">The video.</param>
|
||||||
|
/// <param name="libraryOptions">The library options.</param>
|
||||||
|
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task MoveGeneratedTrickplayDataAsync(Video video, LibraryOptions? libraryOptions, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the trickplay HLS playlist.
|
/// Gets the trickplay HLS playlist.
|
||||||
|
|
|
@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Configuration
|
||||||
EnablePhotos = true;
|
EnablePhotos = true;
|
||||||
SaveSubtitlesWithMedia = true;
|
SaveSubtitlesWithMedia = true;
|
||||||
SaveLyricsWithMedia = false;
|
SaveLyricsWithMedia = false;
|
||||||
|
SaveTrickplayWithMedia = false;
|
||||||
PathInfos = Array.Empty<MediaPathInfo>();
|
PathInfos = Array.Empty<MediaPathInfo>();
|
||||||
EnableAutomaticSeriesGrouping = true;
|
EnableAutomaticSeriesGrouping = true;
|
||||||
SeasonZeroDisplayName = "Specials";
|
SeasonZeroDisplayName = "Specials";
|
||||||
|
@ -99,6 +100,9 @@ namespace MediaBrowser.Model.Configuration
|
||||||
[DefaultValue(false)]
|
[DefaultValue(false)]
|
||||||
public bool SaveLyricsWithMedia { get; set; }
|
public bool SaveLyricsWithMedia { get; set; }
|
||||||
|
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool SaveTrickplayWithMedia { get; set; }
|
||||||
|
|
||||||
public string[] DisabledLyricFetchers { get; set; }
|
public string[] DisabledLyricFetchers { get; set; }
|
||||||
|
|
||||||
public string[] LyricFetcherOrder { get; set; }
|
public string[] LyricFetcherOrder { get; set; }
|
||||||
|
|
|
@ -33,6 +33,13 @@ namespace MediaBrowser.Model.IO
|
||||||
|
|
||||||
string MakeAbsolutePath(string folderPath, string filePath);
|
string MakeAbsolutePath(string folderPath, string filePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves a directory to a new location.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">Source directory.</param>
|
||||||
|
/// <param name="destination">Destination directory.</param>
|
||||||
|
void MoveDirectory(string source, string destination);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="FileSystemMetadata" /> object for the specified file or directory path.
|
/// Returns a <see cref="FileSystemMetadata" /> object for the specified file or directory path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -98,7 +98,8 @@ public class TrickplayImagesTask : IScheduledTask
|
||||||
|
|
||||||
try
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
110
MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
Normal file
110
MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs
Normal file
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class TrickplayMoveImagesTask.
|
||||||
|
/// </summary>
|
||||||
|
public class TrickplayMoveImagesTask : IScheduledTask
|
||||||
|
{
|
||||||
|
private const int QueryPageLimit = 100;
|
||||||
|
|
||||||
|
private readonly ILogger<TrickplayMoveImagesTask> _logger;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILocalizationManager _localization;
|
||||||
|
private readonly ITrickplayManager _trickplayManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TrickplayMoveImagesTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
/// <param name="localization">The localization manager.</param>
|
||||||
|
/// <param name="trickplayManager">The trickplay manager.</param>
|
||||||
|
public TrickplayMoveImagesTask(
|
||||||
|
ILogger<TrickplayMoveImagesTask> logger,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILocalizationManager localization,
|
||||||
|
ITrickplayManager trickplayManager)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_logger = logger;
|
||||||
|
_localization = localization;
|
||||||
|
_trickplayManager = trickplayManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name => _localization.GetLocalizedString("TaskMoveTrickplayImages");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Description => _localization.GetLocalizedString("TaskMoveTrickplayImagesDescription");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Key => "MoveTrickplayImages";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task ExecuteAsync(IProgress<double> 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<Video>().ToList();
|
||||||
|
videos.RemoveAll(i => !trickplayItems.Contains(i.Id));
|
||||||
|
|
||||||
|
foreach (var video in videos)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||||
|
await _trickplayManager.MoveGeneratedTrickplayDataAsync(video, libraryOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error moving trickplay files for {ItemName}", video.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
numComplete++;
|
||||||
|
progress.Report(100d * numComplete / numberOfVideos);
|
||||||
|
}
|
||||||
|
|
||||||
|
startIndex += QueryPageLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report(100);
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,7 +99,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
|
||||||
{
|
{
|
||||||
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||||
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
||||||
bool replace = options.ReplaceAllImages;
|
bool replace = options.RegenerateTrickplay && options.MetadataRefreshMode > MetadataRefreshMode.Default;
|
||||||
|
|
||||||
if (!enableDuringScan.GetValueOrDefault(false))
|
if (!enableDuringScan.GetValueOrDefault(false))
|
||||||
{
|
{
|
||||||
|
@ -108,11 +108,11 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
|
||||||
|
|
||||||
if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
|
if (_config.Configuration.TrickplayOptions.ScanBehavior == TrickplayScanBehavior.Blocking)
|
||||||
{
|
{
|
||||||
await _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
|
await _trickplayManager.RefreshTrickplayDataAsync(video, replace, libraryOptions, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, cancellationToken).ConfigureAwait(false);
|
_ = _trickplayManager.RefreshTrickplayDataAsync(video, replace, libraryOptions, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The core doesn't need to trigger any save operations over this
|
// The core doesn't need to trigger any save operations over this
|
||||||
|
|
Loading…
Reference in New Issue
Block a user