aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs')
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs176
1 files changed, 176 insertions, 0 deletions
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
new file mode 100644
index 000000000..b8e69db8e
--- /dev/null
+++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions.Json;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to move extracted files to the new directories.
+/// </summary>
+public class MigrateKeyframeData : IDatabaseMigrationRoutine
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILogger<MoveTrickplayFiles> _logger;
+ private readonly IApplicationPaths _appPaths;
+ private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+ private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ /// <param name="dbProvider">The EFCore db factory.</param>
+ public MigrateKeyframeData(
+ ILibraryManager libraryManager,
+ ILogger<MoveTrickplayFiles> logger,
+ IApplicationPaths appPaths,
+ IDbContextFactory<JellyfinDbContext> dbProvider)
+ {
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _appPaths = appPaths;
+ _dbProvider = dbProvider;
+ }
+
+ private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
+
+ /// <inheritdoc />
+ public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
+
+ /// <inheritdoc />
+ public string Name => "MigrateKeyframeData";
+
+ /// <inheritdoc />
+ public bool PerformOnNewInstall => false;
+
+ /// <inheritdoc />
+ public void Perform()
+ {
+ const int Limit = 100;
+ int itemCount = 0, offset = 0, previousCount;
+
+ var sw = Stopwatch.StartNew();
+ var itemsQuery = new InternalItemsQuery
+ {
+ MediaTypes = [MediaType.Video],
+ SourceTypes = [SourceType.Library],
+ IsVirtualItem = false,
+ IsFolder = false
+ };
+
+ using var context = _dbProvider.CreateDbContext();
+ context.KeyframeData.ExecuteDelete();
+ using var transaction = context.Database.BeginTransaction();
+ List<KeyframeData> keyframes = [];
+
+ do
+ {
+ var result = _libraryManager.GetItemsResult(itemsQuery);
+ _logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount);
+
+ var items = result.Items;
+ previousCount = items.Count;
+ offset += Limit;
+ foreach (var item in items)
+ {
+ if (TryGetKeyframeData(item, out var data))
+ {
+ keyframes.Add(data);
+ }
+
+ if (++itemCount % 10_000 == 0)
+ {
+ context.KeyframeData.AddRange(keyframes);
+ keyframes.Clear();
+ _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
+ }
+ }
+ } while (previousCount == Limit);
+
+ context.KeyframeData.AddRange(keyframes);
+ context.SaveChanges();
+ transaction.Commit();
+
+ _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
+
+ if (Directory.Exists(KeyframeCachePath))
+ {
+ Directory.Delete(KeyframeCachePath, true);
+ }
+ }
+
+ private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data)
+ {
+ data = null;
+ var path = item.Path;
+ if (!string.IsNullOrEmpty(path))
+ {
+ var cachePath = GetCachePath(KeyframeCachePath, path);
+ if (TryReadFromCache(cachePath, out var keyframeData))
+ {
+ data = new()
+ {
+ ItemId = item.Id,
+ KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
+ TotalDuration = keyframeData.TotalDuration
+ };
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private string? GetCachePath(string keyframeCachePath, string filePath)
+ {
+ DateTime? lastWriteTimeUtc;
+ try
+ {
+ lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
+ }
+ catch (IOException e)
+ {
+ _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
+
+ return null;
+ }
+
+ ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5() + ".json";
+ var prefix = filename[..1];
+
+ return Path.Join(keyframeCachePath, prefix, filename);
+ }
+
+ private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData? cachedResult)
+ {
+ if (File.Exists(cachePath))
+ {
+ var bytes = File.ReadAllBytes(cachePath);
+ cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
+
+ return cachedResult is not null;
+ }
+
+ cachedResult = null;
+
+ return false;
+ }
+}