< Summary - Jellyfin

Information
Class: Jellyfin.Server.Migrations.Routines.MoveExtractedFiles
Assembly: jellyfin
File(s): /srv/git/jellyfin/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 119
Coverable lines: 119
Total lines: 299
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 54
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

0255075100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_SubtitleCachePath()100%210%
get_AttachmentCachePath()100%210%
get_Id()100%210%
get_Name()100%210%
get_PerformOnNewInstall()100%210%
Perform()0%156120%
MoveSubtitleAndAttachmentFiles(...)0%812280%
GetOldAttachmentDataPath(...)0%2040%
GetOldAttachmentCachePath(...)0%2040%
GetOldSubtitleCachePath(...)100%210%
GetSubtitleExtension(...)0%4260%

File(s)

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

#LineLine coverage
 1#pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms
 2
 3using System;
 4using System.Diagnostics;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Security.Cryptography;
 9using System.Text;
 10using Jellyfin.Data.Enums;
 11using MediaBrowser.Common.Configuration;
 12using MediaBrowser.Common.Extensions;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.IO;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Model.Entities;
 17using MediaBrowser.Model.MediaInfo;
 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 MoveExtractedFiles : IDatabaseMigrationRoutine
 26{
 27    private readonly IApplicationPaths _appPaths;
 28    private readonly ILibraryManager _libraryManager;
 29    private readonly ILogger<MoveExtractedFiles> _logger;
 30    private readonly IMediaSourceManager _mediaSourceManager;
 31    private readonly IPathManager _pathManager;
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="MoveExtractedFiles"/> class.
 35    /// </summary>
 36    /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 37    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 38    /// <param name="logger">The logger.</param>
 39    /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 40    /// <param name="pathManager">Instance of the <see cref="IPathManager"/> interface.</param>
 41    public MoveExtractedFiles(
 42        IApplicationPaths appPaths,
 43        ILibraryManager libraryManager,
 44        ILogger<MoveExtractedFiles> logger,
 45        IMediaSourceManager mediaSourceManager,
 46        IPathManager pathManager)
 47    {
 048        _appPaths = appPaths;
 049        _libraryManager = libraryManager;
 050        _logger = logger;
 051        _mediaSourceManager = mediaSourceManager;
 052        _pathManager = pathManager;
 053    }
 54
 055    private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
 56
 057    private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
 58
 59    /// <inheritdoc />
 060    public Guid Id => new("9063b0Ef-CFF1-4EDC-9A13-74093681A89B");
 61
 62    /// <inheritdoc />
 063    public string Name => "MoveExtractedFiles";
 64
 65    /// <inheritdoc />
 066    public bool PerformOnNewInstall => false;
 67
 68    /// <inheritdoc />
 69    public void Perform()
 70    {
 71        const int Limit = 500;
 072        int itemCount = 0, offset = 0;
 73
 074        var sw = Stopwatch.StartNew();
 075        var itemsQuery = new InternalItemsQuery
 076        {
 077            MediaTypes = [MediaType.Video],
 078            SourceTypes = [SourceType.Library],
 079            IsVirtualItem = false,
 080            IsFolder = false,
 081            Limit = Limit,
 082            StartIndex = offset,
 083            EnableTotalRecordCount = true,
 084        };
 85
 086        var records = _libraryManager.GetItemsResult(itemsQuery).TotalRecordCount;
 087        _logger.LogInformation("Checking {Count} items for movable extracted files.", records);
 88
 89        // Make sure directories exist
 090        Directory.CreateDirectory(SubtitleCachePath);
 091        Directory.CreateDirectory(AttachmentCachePath);
 92
 093        itemsQuery.EnableTotalRecordCount = false;
 94        do
 95        {
 096            itemsQuery.StartIndex = offset;
 097            var result = _libraryManager.GetItemsResult(itemsQuery);
 98
 099            var items = result.Items;
 0100            foreach (var item in items)
 101            {
 0102                if (MoveSubtitleAndAttachmentFiles(item))
 103                {
 0104                    itemCount++;
 105                }
 106            }
 107
 0108            offset += Limit;
 0109            if (offset % 5_000 == 0)
 110            {
 0111                _logger.LogInformation("Checked extracted files for {Count} items in {Time}.", offset, sw.Elapsed);
 112            }
 0113        } while (offset < records);
 114
 0115        _logger.LogInformation("Checked {Checked} items - Moved files for {Items} items in {Time}.", records, itemCount,
 116
 117        // Get all subdirectories with 1 character names (those are the legacy directories)
 0118        var subdirectories = Directory.GetDirectories(SubtitleCachePath, "*", SearchOption.AllDirectories).Where(s => s.
 0119        subdirectories.AddRange(Directory.GetDirectories(AttachmentCachePath, "*", SearchOption.AllDirectories).Where(s 
 120
 121        // Remove all legacy subdirectories
 0122        foreach (var subdir in subdirectories)
 123        {
 0124            Directory.Delete(subdir, true);
 125        }
 126
 127        // Remove old cache path
 0128        var attachmentCachePath = Path.Join(_appPaths.CachePath, "attachments");
 0129        if (Directory.Exists(attachmentCachePath))
 130        {
 0131            Directory.Delete(attachmentCachePath, true);
 132        }
 133
 0134        _logger.LogInformation("Cleaned up left over subtitles and attachments.");
 0135    }
 136
 137    private bool MoveSubtitleAndAttachmentFiles(BaseItem item)
 138    {
 0139        var mediaStreams = item.GetMediaStreams().Where(s => s.Type == MediaStreamType.Subtitle && !s.IsExternal);
 0140        var itemIdString = item.Id.ToString("N", CultureInfo.InvariantCulture);
 0141        var modified = false;
 0142        foreach (var mediaStream in mediaStreams)
 143        {
 0144            if (mediaStream.Codec is null)
 145            {
 146                continue;
 147            }
 148
 0149            var mediaStreamIndex = mediaStream.Index;
 0150            var extension = GetSubtitleExtension(mediaStream.Codec);
 0151            var oldSubtitleCachePath = GetOldSubtitleCachePath(item.Path, mediaStream.Index, extension);
 0152            if (string.IsNullOrEmpty(oldSubtitleCachePath) || !File.Exists(oldSubtitleCachePath))
 153            {
 154                continue;
 155            }
 156
 0157            var newSubtitleCachePath = _pathManager.GetSubtitlePath(itemIdString, mediaStreamIndex, extension);
 0158            if (File.Exists(newSubtitleCachePath))
 159            {
 0160                File.Delete(oldSubtitleCachePath);
 161            }
 162            else
 163            {
 0164                var newDirectory = Path.GetDirectoryName(newSubtitleCachePath);
 0165                if (newDirectory is not null)
 166                {
 0167                    Directory.CreateDirectory(newDirectory);
 0168                    File.Move(oldSubtitleCachePath, newSubtitleCachePath, false);
 0169                    _logger.LogDebug("Moved subtitle {Index} for {Item} from {Source} to {Destination}", mediaStreamInde
 170
 0171                    modified = true;
 172                }
 173            }
 174        }
 175
 0176        var attachments = _mediaSourceManager.GetMediaAttachments(item.Id).Where(a => !string.Equals(a.Codec, "mjpeg", S
 0177        var shouldExtractOneByOne = attachments.Any(a => !string.IsNullOrEmpty(a.FileName)
 0178                                                                              && (a.FileName.Contains('/', StringCompari
 0179        foreach (var attachment in attachments)
 180        {
 0181            var attachmentIndex = attachment.Index;
 0182            var oldAttachmentPath = GetOldAttachmentDataPath(item.Path, attachmentIndex);
 0183            if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath))
 184            {
 0185                oldAttachmentPath = GetOldAttachmentCachePath(itemIdString, attachment, shouldExtractOneByOne);
 0186                if (string.IsNullOrEmpty(oldAttachmentPath) || !File.Exists(oldAttachmentPath))
 187                {
 188                    continue;
 189                }
 190            }
 191
 0192            var newAttachmentPath = _pathManager.GetAttachmentPath(itemIdString, attachment.FileName ?? attachmentIndex.
 0193            if (File.Exists(newAttachmentPath))
 194            {
 0195                File.Delete(oldAttachmentPath);
 196            }
 197            else
 198            {
 0199                var newDirectory = Path.GetDirectoryName(newAttachmentPath);
 0200                if (newDirectory is not null)
 201                {
 0202                    Directory.CreateDirectory(newDirectory);
 0203                    File.Move(oldAttachmentPath, newAttachmentPath, false);
 0204                    _logger.LogDebug("Moved attachment {Index} for {Item} from {Source} to {Destination}", attachmentInd
 205
 0206                    modified = true;
 207                }
 208            }
 209        }
 210
 0211        return modified;
 212    }
 213
 214    private string? GetOldAttachmentDataPath(string? mediaPath, int attachmentStreamIndex)
 215    {
 0216        if (mediaPath is null)
 217        {
 0218            return null;
 219        }
 220
 221        string filename;
 0222        var protocol = _mediaSourceManager.GetPathProtocol(mediaPath);
 0223        if (protocol == MediaProtocol.File)
 224        {
 225            DateTime? date;
 226            try
 227            {
 0228                date = File.GetLastWriteTimeUtc(mediaPath);
 0229            }
 0230            catch (IOException e)
 231            {
 0232                _logger.LogDebug("Skipping attachment at index {Index} for {Path}: {Exception}", attachmentStreamIndex, 
 233
 0234                return null;
 235            }
 236
 0237            filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Value.Tick
 238        }
 239        else
 240        {
 0241            filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D",
 242        }
 243
 0244        return Path.Join(_appPaths.DataPath, "attachments", filename[..1], filename);
 0245    }
 246
 247    private string? GetOldAttachmentCachePath(string mediaSourceId, MediaAttachment attachment, bool shouldExtractOneByO
 248    {
 0249        var attachmentFolderPath = Path.Join(_appPaths.CachePath, "attachments", mediaSourceId);
 0250        if (shouldExtractOneByOne)
 251        {
 0252            return Path.Join(attachmentFolderPath, attachment.Index.ToString(CultureInfo.InvariantCulture));
 253        }
 254
 0255        if (string.IsNullOrEmpty(attachment.FileName))
 256        {
 0257            return null;
 258        }
 259
 0260        return Path.Join(attachmentFolderPath, attachment.FileName);
 261    }
 262
 263    private string? GetOldSubtitleCachePath(string path, int streamIndex, string outputSubtitleExtension)
 264    {
 265        DateTime? date;
 266        try
 267        {
 0268            date = File.GetLastWriteTimeUtc(path);
 0269        }
 0270        catch (IOException e)
 271        {
 0272            _logger.LogDebug("Skipping subtitle at index {Index} for {Path}: {Exception}", streamIndex, path, e.Message)
 273
 0274            return null;
 275        }
 276
 0277        var ticksParam = string.Empty;
 0278        ReadOnlySpan<char> filename = new Guid(MD5.HashData(Encoding.Unicode.GetBytes(path + "_" + streamIndex.ToString(
 279
 0280        return Path.Join(SubtitleCachePath, filename[..1], filename);
 0281    }
 282
 283    private static string GetSubtitleExtension(string codec)
 284    {
 0285        if (codec.ToLower(CultureInfo.InvariantCulture).Equals("ass", StringComparison.OrdinalIgnoreCase)
 0286            || codec.ToLower(CultureInfo.InvariantCulture).Equals("ssa", StringComparison.OrdinalIgnoreCase))
 287        {
 0288            return "." + codec;
 289        }
 0290        else if (codec.Contains("pgs", StringComparison.OrdinalIgnoreCase))
 291        {
 0292            return ".sup";
 293        }
 294        else
 295        {
 0296            return ".srt";
 297        }
 298    }
 299}