< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.MigrateKeyframeData
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 57
Coverable lines: 57
Total lines: 152
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 16
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%210%
get_KeyframeCachePath()100%210%
Perform()0%110100%
TryGetKeyframeData(...)0%2040%
GetCachePath(...)100%210%
TryReadFromCache(...)0%620%

File(s)

/srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs

#LineLine coverage
 1using System;
 2using System.Diagnostics;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Text.Json;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Database.Implementations;
 10using Jellyfin.Database.Implementations.Entities;
 11using Jellyfin.Extensions.Json;
 12using Jellyfin.Server.ServerSetupApp;
 13using MediaBrowser.Common.Configuration;
 14using MediaBrowser.Common.Extensions;
 15using Microsoft.EntityFrameworkCore;
 16using Microsoft.Extensions.Logging;
 17
 18namespace Jellyfin.Server.Migrations.Routines;
 19
 20/// <summary>
 21/// Migration to move extracted files to the new directories.
 22/// </summary>
 23[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))]
 24public class MigrateKeyframeData : IDatabaseMigrationRoutine
 25{
 26    private readonly IStartupLogger _logger;
 27    private readonly IApplicationPaths _appPaths;
 28    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 029    private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 30
 31    /// <summary>
 32    /// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
 33    /// </summary>
 34    /// <param name="startupLogger">The startup logger for Startup UI intigration.</param>
 35    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 36    /// <param name="dbProvider">The EFCore db factory.</param>
 37    public MigrateKeyframeData(
 38        IStartupLogger startupLogger,
 39        IApplicationPaths appPaths,
 40        IDbContextFactory<JellyfinDbContext> dbProvider)
 41    {
 042        _logger = startupLogger;
 043        _appPaths = appPaths;
 044        _dbProvider = dbProvider;
 045    }
 46
 047    private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
 48
 49    /// <inheritdoc />
 50    public void Perform()
 51    {
 52        const int Limit = 5000;
 053        int itemCount = 0, offset = 0;
 54
 055        var sw = Stopwatch.StartNew();
 56
 057        using var context = _dbProvider.CreateDbContext();
 058        var baseQuery = context.BaseItems.Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b
 059        var records = baseQuery.Count();
 060        _logger.LogInformation("Checking {Count} items for importable keyframe data.", records);
 61
 062        context.KeyframeData.ExecuteDelete();
 063        using var transaction = context.Database.BeginTransaction();
 64        do
 65        {
 066            var results = baseQuery.Skip(offset).Take(Limit).Select(b => new Tuple<Guid, string?>(b.Id, b.Path)).ToList(
 067            foreach (var result in results)
 68            {
 069                if (TryGetKeyframeData(result.Item1, result.Item2, out var data))
 70                {
 071                    itemCount++;
 072                    context.KeyframeData.Add(data);
 73                }
 74            }
 75
 076            offset += Limit;
 077            if (offset > records)
 78            {
 079                offset = records;
 80            }
 81
 082            _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed)
 083        } while (offset < records);
 84
 085        context.SaveChanges();
 086        transaction.Commit();
 87
 088        _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
 89
 090        if (Directory.Exists(KeyframeCachePath))
 91        {
 092            Directory.Delete(KeyframeCachePath, true);
 93        }
 094    }
 95
 96    private bool TryGetKeyframeData(Guid id, string? path, [NotNullWhen(true)] out KeyframeData? data)
 97    {
 098        data = null;
 099        if (!string.IsNullOrEmpty(path))
 100        {
 0101            var cachePath = GetCachePath(KeyframeCachePath, path);
 0102            if (TryReadFromCache(cachePath, out var keyframeData))
 103            {
 0104                data = new()
 0105                {
 0106                    ItemId = id,
 0107                    KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
 0108                    TotalDuration = keyframeData.TotalDuration
 0109                };
 110
 0111                return true;
 112            }
 113        }
 114
 0115        return false;
 116    }
 117
 118    private string? GetCachePath(string keyframeCachePath, string filePath)
 119    {
 120        DateTime? lastWriteTimeUtc;
 121        try
 122        {
 0123            lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
 0124        }
 0125        catch (IOException e)
 126        {
 0127            _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
 128
 0129            return null;
 130        }
 131
 0132        ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCultu
 0133        var prefix = filename[..1];
 134
 0135        return Path.Join(keyframeCachePath, prefix, filename);
 0136    }
 137
 138    private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData
 139    {
 0140        if (File.Exists(cachePath))
 141        {
 0142            var bytes = File.ReadAllBytes(cachePath);
 0143            cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
 144
 0145            return cachedResult is not null;
 146        }
 147
 0148        cachedResult = null;
 149
 0150        return false;
 151    }
 152}