< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.MediaEncoder.EncodingManager
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
Line coverage
13%
Covered lines: 6
Uncovered lines: 38
Coverable lines: 44
Total lines: 272
Line coverage: 13.6%
Branch coverage
0%
Covered branches: 0
Total branches: 18
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%11100%
GetChapterImagesPath(...)100%210%
IsEligibleForChapterImageExtraction(...)0%110100%
GetAverageDurationBetweenChapters(...)0%2040%
GetChapterImagePath(...)100%210%
GetSavedChapterImages(...)0%620%
DeleteDeadImages(...)0%620%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using Jellyfin.Extensions;
 13using MediaBrowser.Controller.Chapters;
 14using MediaBrowser.Controller.Entities;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.MediaEncoding;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Model.Configuration;
 19using MediaBrowser.Model.Dto;
 20using MediaBrowser.Model.Entities;
 21using MediaBrowser.Model.IO;
 22using Microsoft.Extensions.Logging;
 23
 24namespace Emby.Server.Implementations.MediaEncoder
 25{
 26    public class EncodingManager : IEncodingManager
 27    {
 28        private readonly IFileSystem _fileSystem;
 29        private readonly ILogger<EncodingManager> _logger;
 30        private readonly IMediaEncoder _encoder;
 31        private readonly IChapterManager _chapterManager;
 32        private readonly ILibraryManager _libraryManager;
 33
 34        /// <summary>
 35        /// The first chapter ticks.
 36        /// </summary>
 037        private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
 38
 39        public EncodingManager(
 40            ILogger<EncodingManager> logger,
 41            IFileSystem fileSystem,
 42            IMediaEncoder encoder,
 43            IChapterManager chapterManager,
 44            ILibraryManager libraryManager)
 45        {
 2246            _logger = logger;
 2247            _fileSystem = fileSystem;
 2248            _encoder = encoder;
 2249            _chapterManager = chapterManager;
 2250            _libraryManager = libraryManager;
 2251        }
 52
 53        /// <summary>
 54        /// Gets the chapter images data path.
 55        /// </summary>
 56        /// <value>The chapter images data path.</value>
 57        private static string GetChapterImagesPath(BaseItem item)
 58        {
 059            return Path.Combine(item.GetInternalMetadataPath(), "chapters");
 60        }
 61
 62        /// <summary>
 63        /// Determines whether [is eligible for chapter image extraction] [the specified video].
 64        /// </summary>
 65        /// <param name="video">The video.</param>
 66        /// <param name="libraryOptions">The library options for the video.</param>
 67        /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>fals
 68        private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
 69        {
 070            if (video.IsPlaceHolder)
 71            {
 072                return false;
 73            }
 74
 075            if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
 76            {
 077                return false;
 78            }
 79
 080            if (video.IsShortcut)
 81            {
 082                return false;
 83            }
 84
 085            if (!video.IsCompleteMedia)
 86            {
 087                return false;
 88            }
 89
 90            // Can't extract images if there are no video streams
 091            return video.DefaultVideoStreamIndex.HasValue;
 92        }
 93
 94        private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
 95        {
 096            if (chapters.Count < 2)
 97            {
 098                return 0;
 99            }
 100
 0101            long sum = 0;
 0102            for (int i = 1; i < chapters.Count; i++)
 103            {
 0104                sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
 105            }
 106
 0107            return sum / chapters.Count;
 108        }
 109
 110        public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<Chap
 111        {
 112            if (chapters.Count == 0)
 113            {
 114                return true;
 115            }
 116
 117            var libraryOptions = _libraryManager.GetLibraryOptions(video);
 118
 119            if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
 120            {
 121                extractImages = false;
 122            }
 123
 124            var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
 125            var threshold = TimeSpan.FromSeconds(1).Ticks;
 126            if (averageChapterDuration < threshold)
 127            {
 128                _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {A
 129                extractImages = false;
 130            }
 131
 132            var success = true;
 133            var changesMade = false;
 134
 135            var runtimeTicks = video.RunTimeTicks ?? 0;
 136
 137            var currentImages = GetSavedChapterImages(video, directoryService);
 138
 139            foreach (var chapter in chapters)
 140            {
 141                if (chapter.StartPositionTicks >= runtimeTicks)
 142                {
 143                    _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a posit
 144                    break;
 145                }
 146
 147                var path = GetChapterImagePath(video, chapter.StartPositionTicks);
 148
 149                if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
 150                {
 151                    if (extractImages)
 152                    {
 153                        cancellationToken.ThrowIfCancellationRequested();
 154
 155                        try
 156                        {
 157                            // Add some time for the first chapter to make sure we don't end up with a black image
 158                            var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks,
 159
 160                            var inputPath = video.Path;
 161
 162                            Directory.CreateDirectory(Path.GetDirectoryName(path));
 163
 164                            var container = video.Container;
 165                            var mediaSource = new MediaSourceInfo
 166                            {
 167                                VideoType = video.VideoType,
 168                                IsoType = video.IsoType,
 169                                Protocol = video.PathProtocol.Value,
 170                            };
 171
 172                            var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.Get
 173                            File.Copy(tempFile, path, true);
 174
 175                            try
 176                            {
 177                                _fileSystem.DeleteFile(tempFile);
 178                            }
 179                            catch (IOException ex)
 180                            {
 181                                _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", temp
 182                            }
 183
 184                            chapter.ImagePath = path;
 185                            chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
 186                            changesMade = true;
 187                        }
 188                        catch (Exception ex)
 189                        {
 190                            _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path)
 191                            success = false;
 192                            break;
 193                        }
 194                    }
 195                    else if (!string.IsNullOrEmpty(chapter.ImagePath))
 196                    {
 197                        chapter.ImagePath = null;
 198                        changesMade = true;
 199                    }
 200                }
 201                else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
 202                {
 203                    chapter.ImagePath = path;
 204                    chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
 205                    changesMade = true;
 206                }
 207                else if (libraryOptions?.EnableChapterImageExtraction != true)
 208                {
 209                    // We have an image for the current chapter but the user has disabled chapter image extraction -> de
 210                    chapter.ImagePath = null;
 211                    changesMade = true;
 212                }
 213            }
 214
 215            if (saveChapters && changesMade)
 216            {
 217                _chapterManager.SaveChapters(video.Id, chapters);
 218            }
 219
 220            DeleteDeadImages(currentImages, chapters);
 221
 222            return success;
 223        }
 224
 225        private string GetChapterImagePath(Video video, long chapterPositionTicks)
 226        {
 0227            var filename = video.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.
 228
 0229            return Path.Combine(GetChapterImagesPath(video), filename);
 230        }
 231
 232        private static IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
 233        {
 0234            var path = GetChapterImagesPath(video);
 0235            if (!Directory.Exists(path))
 236            {
 0237                return Array.Empty<string>();
 238            }
 239
 240            try
 241            {
 0242                return directoryService.GetFilePaths(path);
 243            }
 0244            catch (IOException)
 245            {
 0246                return Array.Empty<string>();
 247            }
 0248        }
 249
 250        private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
 251        {
 0252            var deadImages = images
 0253                .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIg
 0254                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.O
 0255                .ToList();
 256
 0257            foreach (var image in deadImages)
 258            {
 0259                _logger.LogDebug("Deleting dead chapter image {Path}", image);
 260
 261                try
 262                {
 0263                    _fileSystem.DeleteFile(image);
 0264                }
 0265                catch (IOException ex)
 266                {
 0267                    _logger.LogError(ex, "Error deleting {Path}.", image);
 0268                }
 269            }
 0270        }
 271    }
 272}