diff options
| author | Tim Eisele <Ghost_of_Stone@web.de> | 2025-04-03 02:06:40 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-02 18:06:40 -0600 |
| commit | 0573999d5ef7526a3bb3e24523ba0e5599816155 (patch) | |
| tree | 7d35df4c7c8536fc8f2f99d8d05309d97862684d /Jellyfin.Server | |
| parent | 49ac705867234c48e79ceb1cd84bc4394c65313d (diff) | |
Import Keyframes into database (#13771)
* Migrate keyframe data into database
* Clear database table before import to handle failed migrations
Diffstat (limited to 'Jellyfin.Server')
| -rw-r--r-- | Jellyfin.Server/Migrations/MigrationRunner.cs | 1 | ||||
| -rw-r--r-- | Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs | 173 |
2 files changed, 174 insertions, 0 deletions
diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index 68a3491b5..baeea2c14 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -56,6 +56,7 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateLibraryDb), typeof(Routines.MigrateRatingLevels), typeof(Routines.MoveTrickplayFiles), + typeof(Routines.MigrateKeyframeData), }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs new file mode 100644 index 000000000..1cab943c1 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs @@ -0,0 +1,173 @@ +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); + + 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; + } +} |
