< 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: 151
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 MediaBrowser.Common.Configuration;
 13using MediaBrowser.Common.Extensions;
 14using Microsoft.EntityFrameworkCore;
 15using Microsoft.Extensions.Logging;
 16
 17namespace Jellyfin.Server.Migrations.Routines;
 18
 19/// <summary>
 20/// Migration to move extracted files to the new directories.
 21/// </summary>
 22[JellyfinMigration("2025-04-21T00:00:00", nameof(MigrateKeyframeData))]
 23public class MigrateKeyframeData : IDatabaseMigrationRoutine
 24{
 25    private readonly ILogger<MigrateKeyframeData> _logger;
 26    private readonly IApplicationPaths _appPaths;
 27    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 028    private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 29
 30    /// <summary>
 31    /// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
 32    /// </summary>
 33    /// <param name="logger">The logger.</param>
 34    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 35    /// <param name="dbProvider">The EFCore db factory.</param>
 36    public MigrateKeyframeData(
 37        ILogger<MigrateKeyframeData> logger,
 38        IApplicationPaths appPaths,
 39        IDbContextFactory<JellyfinDbContext> dbProvider)
 40    {
 041        _logger = logger;
 042        _appPaths = appPaths;
 043        _dbProvider = dbProvider;
 044    }
 45
 046    private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
 47
 48    /// <inheritdoc />
 49    public void Perform()
 50    {
 51        const int Limit = 5000;
 052        int itemCount = 0, offset = 0;
 53
 054        var sw = Stopwatch.StartNew();
 55
 056        using var context = _dbProvider.CreateDbContext();
 057        var baseQuery = context.BaseItems.Where(b => b.MediaType == MediaType.Video.ToString() && !b.IsVirtualItem && !b
 058        var records = baseQuery.Count();
 059        _logger.LogInformation("Checking {Count} items for importable keyframe data.", records);
 60
 061        context.KeyframeData.ExecuteDelete();
 062        using var transaction = context.Database.BeginTransaction();
 63        do
 64        {
 065            var results = baseQuery.Skip(offset).Take(Limit).Select(b => new Tuple<Guid, string?>(b.Id, b.Path)).ToList(
 066            foreach (var result in results)
 67            {
 068                if (TryGetKeyframeData(result.Item1, result.Item2, out var data))
 69                {
 070                    itemCount++;
 071                    context.KeyframeData.Add(data);
 72                }
 73            }
 74
 075            offset += Limit;
 076            if (offset > records)
 77            {
 078                offset = records;
 79            }
 80
 081            _logger.LogInformation("Checked: {Count} - Imported: {Items} - Time: {Time}", offset, itemCount, sw.Elapsed)
 082        } while (offset < records);
 83
 084        context.SaveChanges();
 085        transaction.Commit();
 86
 087        _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
 88
 089        if (Directory.Exists(KeyframeCachePath))
 90        {
 091            Directory.Delete(KeyframeCachePath, true);
 92        }
 093    }
 94
 95    private bool TryGetKeyframeData(Guid id, string? path, [NotNullWhen(true)] out KeyframeData? data)
 96    {
 097        data = null;
 098        if (!string.IsNullOrEmpty(path))
 99        {
 0100            var cachePath = GetCachePath(KeyframeCachePath, path);
 0101            if (TryReadFromCache(cachePath, out var keyframeData))
 102            {
 0103                data = new()
 0104                {
 0105                    ItemId = id,
 0106                    KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
 0107                    TotalDuration = keyframeData.TotalDuration
 0108                };
 109
 0110                return true;
 111            }
 112        }
 113
 0114        return false;
 115    }
 116
 117    private string? GetCachePath(string keyframeCachePath, string filePath)
 118    {
 119        DateTime? lastWriteTimeUtc;
 120        try
 121        {
 0122            lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
 0123        }
 0124        catch (IOException e)
 125        {
 0126            _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
 127
 0128            return null;
 129        }
 130
 0131        ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCultu
 0132        var prefix = filename[..1];
 133
 0134        return Path.Join(keyframeCachePath, prefix, filename);
 0135    }
 136
 137    private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData
 138    {
 0139        if (File.Exists(cachePath))
 140        {
 0141            var bytes = File.ReadAllBytes(cachePath);
 0142            cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
 143
 0144            return cachedResult is not null;
 145        }
 146
 0147        cachedResult = null;
 148
 0149        return false;
 150    }
 151}