< 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: 71
Coverable lines: 71
Total lines: 176
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%
get_Id()100%210%
get_Name()100%210%
get_PerformOnNewInstall()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.Collections.Generic;
 3using System.Diagnostics;
 4using System.Diagnostics.CodeAnalysis;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Text.Json;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Database.Implementations;
 11using Jellyfin.Database.Implementations.Entities;
 12using Jellyfin.Extensions.Json;
 13using MediaBrowser.Common.Configuration;
 14using MediaBrowser.Common.Extensions;
 15using MediaBrowser.Controller.Entities;
 16using MediaBrowser.Controller.Library;
 17using Microsoft.EntityFrameworkCore;
 18using Microsoft.Extensions.Logging;
 19
 20namespace Jellyfin.Server.Migrations.Routines;
 21
 22/// <summary>
 23/// Migration to move extracted files to the new directories.
 24/// </summary>
 25public class MigrateKeyframeData : IDatabaseMigrationRoutine
 26{
 27    private readonly ILibraryManager _libraryManager;
 28    private readonly ILogger<MoveTrickplayFiles> _logger;
 29    private readonly IApplicationPaths _appPaths;
 30    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
 031    private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="MigrateKeyframeData"/> class.
 35    /// </summary>
 36    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 37    /// <param name="logger">The logger.</param>
 38    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 39    /// <param name="dbProvider">The EFCore db factory.</param>
 40    public MigrateKeyframeData(
 41        ILibraryManager libraryManager,
 42        ILogger<MoveTrickplayFiles> logger,
 43        IApplicationPaths appPaths,
 44        IDbContextFactory<JellyfinDbContext> dbProvider)
 45    {
 046        _libraryManager = libraryManager;
 047        _logger = logger;
 048        _appPaths = appPaths;
 049        _dbProvider = dbProvider;
 050    }
 51
 052    private string KeyframeCachePath => Path.Combine(_appPaths.DataPath, "keyframes");
 53
 54    /// <inheritdoc />
 055    public Guid Id => new("EA4bCAE1-09A4-428E-9B90-4B4FD2EA1B24");
 56
 57    /// <inheritdoc />
 058    public string Name => "MigrateKeyframeData";
 59
 60    /// <inheritdoc />
 061    public bool PerformOnNewInstall => false;
 62
 63    /// <inheritdoc />
 64    public void Perform()
 65    {
 66        const int Limit = 100;
 067        int itemCount = 0, offset = 0, previousCount;
 68
 069        var sw = Stopwatch.StartNew();
 070        var itemsQuery = new InternalItemsQuery
 071        {
 072            MediaTypes = [MediaType.Video],
 073            SourceTypes = [SourceType.Library],
 074            IsVirtualItem = false,
 075            IsFolder = false
 076        };
 77
 078        using var context = _dbProvider.CreateDbContext();
 079        context.KeyframeData.ExecuteDelete();
 080        using var transaction = context.Database.BeginTransaction();
 081        List<KeyframeData> keyframes = [];
 82
 83        do
 84        {
 085            var result = _libraryManager.GetItemsResult(itemsQuery);
 086            _logger.LogInformation("Importing keyframes for {Count} items", result.TotalRecordCount);
 87
 088            var items = result.Items;
 089            previousCount = items.Count;
 090            offset += Limit;
 091            foreach (var item in items)
 92            {
 093                if (TryGetKeyframeData(item, out var data))
 94                {
 095                    keyframes.Add(data);
 96                }
 97
 098                if (++itemCount % 10_000 == 0)
 99                {
 0100                    context.KeyframeData.AddRange(keyframes);
 0101                    keyframes.Clear();
 0102                    _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
 103                }
 104            }
 0105        } while (previousCount == Limit);
 106
 0107        context.KeyframeData.AddRange(keyframes);
 0108        context.SaveChanges();
 0109        transaction.Commit();
 110
 0111        _logger.LogInformation("Imported keyframes for {Count} items in {Time}", itemCount, sw.Elapsed);
 112
 0113        if (Directory.Exists(KeyframeCachePath))
 114        {
 0115            Directory.Delete(KeyframeCachePath, true);
 116        }
 0117    }
 118
 119    private bool TryGetKeyframeData(BaseItem item, [NotNullWhen(true)] out KeyframeData? data)
 120    {
 0121        data = null;
 0122        var path = item.Path;
 0123        if (!string.IsNullOrEmpty(path))
 124        {
 0125            var cachePath = GetCachePath(KeyframeCachePath, path);
 0126            if (TryReadFromCache(cachePath, out var keyframeData))
 127            {
 0128                data = new()
 0129                {
 0130                    ItemId = item.Id,
 0131                    KeyframeTicks = keyframeData.KeyframeTicks.ToList(),
 0132                    TotalDuration = keyframeData.TotalDuration
 0133                };
 134
 0135                return true;
 136            }
 137        }
 138
 0139        return false;
 140    }
 141
 142    private string? GetCachePath(string keyframeCachePath, string filePath)
 143    {
 144        DateTime? lastWriteTimeUtc;
 145        try
 146        {
 0147            lastWriteTimeUtc = File.GetLastWriteTimeUtc(filePath);
 0148        }
 0149        catch (IOException e)
 150        {
 0151            _logger.LogDebug("Skipping {Path}: {Exception}", filePath, e.Message);
 152
 0153            return null;
 154        }
 155
 0156        ReadOnlySpan<char> filename = (filePath + "_" + lastWriteTimeUtc.Value.Ticks.ToString(CultureInfo.InvariantCultu
 0157        var prefix = filename[..1];
 158
 0159        return Path.Join(keyframeCachePath, prefix, filename);
 0160    }
 161
 162    private static bool TryReadFromCache(string? cachePath, [NotNullWhen(true)] out MediaEncoding.Keyframes.KeyframeData
 163    {
 0164        if (File.Exists(cachePath))
 165        {
 0166            var bytes = File.ReadAllBytes(cachePath);
 0167            cachedResult = JsonSerializer.Deserialize<MediaEncoding.Keyframes.KeyframeData>(bytes, _jsonOptions);
 168
 0169            return cachedResult is not null;
 170        }
 171
 0172        cachedResult = null;
 173
 0174        return false;
 175    }
 176}