2021-05-20 21:28:18 +02:00
#nullable disable
2020-05-29 11:28:19 +02:00
#pragma warning disable CS1591
2013-02-20 20:33:05 -05:00
using System ;
2013-09-04 13:02:19 -04:00
using System.Collections.Generic ;
2013-12-15 13:29:34 -05:00
using System.Globalization ;
2013-02-20 20:33:05 -05:00
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2021-12-20 13:31:07 +01:00
using Jellyfin.Extensions ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Controller.Chapters ;
using MediaBrowser.Controller.Entities ;
2016-08-29 17:06:24 -04:00
using MediaBrowser.Controller.Library ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Controller.MediaEncoding ;
2017-11-05 16:51:23 -05:00
using MediaBrowser.Controller.Providers ;
2023-01-16 18:34:15 +00:00
using MediaBrowser.Model.Configuration ;
2020-09-24 08:41:42 +02:00
using MediaBrowser.Model.Dto ;
2019-01-13 20:22:00 +01:00
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.IO ;
using Microsoft.Extensions.Logging ;
2013-02-20 20:33:05 -05:00
2016-11-03 18:06:00 -04:00
namespace Emby.Server.Implementations.MediaEncoder
2013-02-20 20:33:05 -05:00
{
2014-02-20 11:37:41 -05:00
public class EncodingManager : IEncodingManager
2013-02-20 20:33:05 -05:00
{
2014-02-20 11:37:41 -05:00
private readonly IFileSystem _fileSystem ;
2020-06-05 18:15:56 -06:00
private readonly ILogger < EncodingManager > _logger ;
2014-02-20 11:37:41 -05:00
private readonly IMediaEncoder _encoder ;
2024-10-09 09:53:39 +00:00
private readonly IChapterRepository _chapterManager ;
2016-08-29 17:06:24 -04:00
private readonly ILibraryManager _libraryManager ;
2013-04-27 18:52:41 -04:00
2020-02-23 10:53:51 +01:00
/// <summary>
/// The first chapter ticks.
/// </summary>
private static readonly long _firstChapterTicks = TimeSpan . FromSeconds ( 15 ) . Ticks ;
2019-01-17 22:55:05 +00:00
public EncodingManager (
2020-02-23 10:53:51 +01:00
ILogger < EncodingManager > logger ,
2019-01-17 22:55:05 +00:00
IFileSystem fileSystem ,
2017-08-05 15:02:33 -04:00
IMediaEncoder encoder ,
2024-10-09 09:53:39 +00:00
IChapterRepository chapterManager ,
2020-02-23 10:53:51 +01:00
ILibraryManager libraryManager )
2013-02-20 20:33:05 -05:00
{
2020-02-23 10:53:51 +01:00
_logger = logger ;
2014-02-20 11:37:41 -05:00
_fileSystem = fileSystem ;
_encoder = encoder ;
2014-06-10 13:36:06 -04:00
_chapterManager = chapterManager ;
2016-08-30 02:06:24 -04:00
_libraryManager = libraryManager ;
2014-02-20 11:37:41 -05:00
}
2013-02-20 20:33:05 -05:00
/// <summary>
2014-02-20 11:37:41 -05:00
/// Gets the chapter images data path.
2013-02-20 20:33:05 -05:00
/// </summary>
2014-02-20 11:37:41 -05:00
/// <value>The chapter images data path.</value>
2019-01-06 21:50:43 +01:00
private static string GetChapterImagesPath ( BaseItem item )
2013-02-20 20:33:05 -05:00
{
2014-09-28 11:27:26 -04:00
return Path . Combine ( item . GetInternalMetadataPath ( ) , "chapters" ) ;
2013-02-20 20:33:05 -05:00
}
2013-12-05 22:39:44 -05:00
2013-12-18 00:44:46 -05:00
/// <summary>
/// Determines whether [is eligible for chapter image extraction] [the specified video].
/// </summary>
/// <param name="video">The video.</param>
2023-01-16 20:06:56 +00:00
/// <param name="libraryOptions">The library options for the video.</param>
2013-12-18 00:44:46 -05:00
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
2023-01-16 18:34:15 +00:00
private bool IsEligibleForChapterImageExtraction ( Video video , LibraryOptions libraryOptions )
2013-12-18 00:44:46 -05:00
{
2014-03-03 00:11:03 -05:00
if ( video . IsPlaceHolder )
{
return false ;
}
2023-01-16 18:34:15 +00:00
if ( libraryOptions is null | | ! libraryOptions . EnableChapterImageExtraction )
2013-12-18 00:44:46 -05:00
{
2016-10-02 00:31:47 -04:00
return false ;
2013-12-18 00:44:46 -05:00
}
2017-12-03 17:12:46 -05:00
if ( video . IsShortcut )
{
return false ;
}
2017-08-26 15:50:02 -04:00
if ( ! video . IsCompleteMedia )
{
return false ;
}
2013-12-18 00:44:46 -05:00
// Can't extract images if there are no video streams
return video . DefaultVideoStreamIndex . HasValue ;
}
2024-09-12 21:44:57 +02:00
private long GetAverageDurationBetweenChapters ( IReadOnlyList < ChapterInfo > chapters )
{
if ( chapters . Count < 2 )
{
return 0 ;
}
long sum = 0 ;
for ( int i = 1 ; i < chapters . Count ; i + + )
{
sum + = chapters [ i ] . StartPositionTicks - chapters [ i - 1 ] . StartPositionTicks ;
}
return sum / chapters . Count ;
}
2020-02-23 10:53:51 +01:00
public async Task < bool > RefreshChapterImages ( Video video , IDirectoryService directoryService , IReadOnlyList < ChapterInfo > chapters , bool extractImages , bool saveChapters , CancellationToken cancellationToken )
2013-02-20 20:33:05 -05:00
{
2024-09-12 21:44:57 +02:00
if ( chapters . Count = = 0 )
{
return true ;
}
2023-01-16 18:34:15 +00:00
var libraryOptions = _libraryManager . GetLibraryOptions ( video ) ;
if ( ! IsEligibleForChapterImageExtraction ( video , libraryOptions ) )
2013-03-13 21:24:43 -04:00
{
2014-01-30 23:50:09 -05:00
extractImages = false ;
2013-03-13 21:24:43 -04:00
}
2024-09-12 21:44:57 +02:00
var averageChapterDuration = GetAverageDurationBetweenChapters ( chapters ) ;
var threshold = TimeSpan . FromSeconds ( 1 ) . Ticks ;
if ( averageChapterDuration < threshold )
{
_logger . LogInformation ( "Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}" , video . Name , averageChapterDuration , threshold ) ;
extractImages = false ;
}
2013-05-25 01:17:32 -04:00
var success = true ;
2013-02-20 20:33:05 -05:00
var changesMade = false ;
2013-06-01 09:56:48 -04:00
var runtimeTicks = video . RunTimeTicks ? ? 0 ;
2017-11-05 16:51:23 -05:00
var currentImages = GetSavedChapterImages ( video , directoryService ) ;
2013-12-15 11:53:32 -05:00
2013-06-18 15:16:27 -04:00
foreach ( var chapter in chapters )
2013-02-20 20:33:05 -05:00
{
2013-06-01 09:56:48 -04:00
if ( chapter . StartPositionTicks > = runtimeTicks )
{
2018-12-13 14:18:25 +01:00
_logger . LogInformation ( "Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime." , video . Name ) ;
2013-06-01 09:56:48 -04:00
break ;
}
2013-12-15 11:53:32 -05:00
var path = GetChapterImagePath ( video , chapter . StartPositionTicks ) ;
2013-02-20 20:33:05 -05:00
2021-12-20 13:31:07 +01:00
if ( ! currentImages . Contains ( path , StringComparison . OrdinalIgnoreCase ) )
2013-02-20 20:33:05 -05:00
{
if ( extractImages )
{
2018-09-12 19:26:21 +02:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2016-09-05 16:07:36 -04:00
try
{
// Add some time for the first chapter to make sure we don't end up with a black image
2020-02-23 10:53:51 +01:00
var time = chapter . StartPositionTicks = = 0 ? TimeSpan . FromTicks ( Math . Min ( _firstChapterTicks , video . RunTimeTicks ? ? 0 ) ) : TimeSpan . FromTicks ( chapter . StartPositionTicks ) ;
2013-02-20 20:33:05 -05:00
2020-10-01 22:20:28 +02:00
var inputPath = video . Path ;
2013-04-07 16:55:05 -04:00
2019-01-26 22:08:04 +01:00
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ) ;
2013-12-05 22:39:44 -05:00
2016-09-30 14:43:59 -04:00
var container = video . Container ;
2020-09-24 08:41:42 +02:00
var mediaSource = new MediaSourceInfo
{
VideoType = video . VideoType ,
IsoType = video . IsoType ,
Protocol = video . PathProtocol . Value ,
} ;
2016-09-30 14:43:59 -04:00
2020-09-24 08:41:42 +02:00
var tempFile = await _encoder . ExtractVideoImage ( inputPath , container , mediaSource , video . GetDefaultVideoStream ( ) , video . Video3DFormat , time , cancellationToken ) . ConfigureAwait ( false ) ;
2019-01-26 22:31:59 +01:00
File . Copy ( tempFile , path , true ) ;
2016-06-30 22:35:18 -04:00
try
{
2016-11-03 18:06:00 -04:00
_fileSystem . DeleteFile ( tempFile ) ;
2016-06-30 22:35:18 -04:00
}
2020-02-23 10:53:51 +01:00
catch ( IOException ex )
2014-02-06 18:57:21 -05:00
{
2020-04-01 19:05:41 +02:00
_logger . LogError ( ex , "Error deleting temporary chapter image encoding file {Path}" , tempFile ) ;
2014-02-06 18:57:21 -05:00
}
2013-02-20 20:33:05 -05:00
chapter . ImagePath = path ;
2016-07-06 13:44:44 -04:00
chapter . ImageDateModified = _fileSystem . GetLastWriteTimeUtc ( path ) ;
2013-02-20 20:33:05 -05:00
changesMade = true ;
}
2015-04-11 13:59:55 -04:00
catch ( Exception ex )
2013-04-06 20:27:33 -04:00
{
2021-02-13 00:39:18 +01:00
_logger . LogError ( ex , "Error extracting chapter images for {0}" , string . Join ( ',' , video . Path ) ) ;
2013-05-25 01:17:32 -04:00
success = false ;
2013-04-06 20:27:33 -04:00
break ;
}
2013-02-20 20:33:05 -05:00
}
2014-01-30 23:50:09 -05:00
else if ( ! string . IsNullOrEmpty ( chapter . ImagePath ) )
{
chapter . ImagePath = null ;
changesMade = true ;
}
2013-02-20 20:33:05 -05:00
}
else if ( ! string . Equals ( path , chapter . ImagePath , StringComparison . OrdinalIgnoreCase ) )
{
chapter . ImagePath = path ;
2016-07-06 13:44:44 -04:00
chapter . ImageDateModified = _fileSystem . GetLastWriteTimeUtc ( path ) ;
2013-02-20 20:33:05 -05:00
changesMade = true ;
}
2023-01-16 18:34:15 +00:00
else if ( libraryOptions ? . EnableChapterImageExtraction ! = true )
{
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
chapter . ImagePath = null ;
changesMade = true ;
}
2013-02-20 20:33:05 -05:00
}
2013-06-18 15:16:27 -04:00
if ( saveChapters & & changesMade )
2013-02-20 20:33:05 -05:00
{
2020-02-23 10:53:51 +01:00
_chapterManager . SaveChapters ( video . Id , chapters ) ;
2013-02-20 20:33:05 -05:00
}
2013-05-25 01:17:32 -04:00
2013-12-15 11:53:32 -05:00
DeleteDeadImages ( currentImages , chapters ) ;
2013-05-25 01:17:32 -04:00
return success ;
2013-02-20 20:33:05 -05:00
}
2014-02-20 11:37:41 -05:00
private string GetChapterImagePath ( Video video , long chapterPositionTicks )
{
2021-09-26 08:14:36 -06:00
var filename = video . DateModified . Ticks . ToString ( CultureInfo . InvariantCulture ) + "_" + chapterPositionTicks . ToString ( CultureInfo . InvariantCulture ) + ".jpg" ;
2014-02-20 11:37:41 -05:00
2014-09-28 11:27:26 -04:00
return Path . Combine ( GetChapterImagesPath ( video ) , filename ) ;
2014-02-20 11:37:41 -05:00
}
2020-02-23 10:53:51 +01:00
private static IReadOnlyList < string > GetSavedChapterImages ( Video video , IDirectoryService directoryService )
2014-02-20 11:37:41 -05:00
{
2014-09-28 11:27:26 -04:00
var path = GetChapterImagesPath ( video ) ;
2019-02-24 15:47:59 +01:00
if ( ! Directory . Exists ( path ) )
{
2020-02-23 10:53:51 +01:00
return Array . Empty < string > ( ) ;
2019-02-24 15:47:59 +01:00
}
2014-02-20 11:37:41 -05:00
try
{
2020-02-23 10:53:51 +01:00
return directoryService . GetFilePaths ( path ) ;
2014-02-20 11:37:41 -05:00
}
2016-11-03 18:06:00 -04:00
catch ( IOException )
2014-02-20 11:37:41 -05:00
{
2020-02-23 10:53:51 +01:00
return Array . Empty < string > ( ) ;
2014-02-20 11:37:41 -05:00
}
}
2013-12-15 11:53:32 -05:00
private void DeleteDeadImages ( IEnumerable < string > images , IEnumerable < ChapterInfo > chapters )
{
var deadImages = images
2013-12-15 12:01:56 -05:00
. Except ( chapters . Select ( i = > i . ImagePath ) . Where ( i = > ! string . IsNullOrEmpty ( i ) ) , StringComparer . OrdinalIgnoreCase )
2023-10-06 00:40:09 +02:00
. Where ( i = > BaseItem . SupportedImageExtensions . Contains ( Path . GetExtension ( i . AsSpan ( ) ) , StringComparison . OrdinalIgnoreCase ) )
2013-12-15 11:53:32 -05:00
. ToList ( ) ;
foreach ( var image in deadImages )
{
2020-02-23 10:53:51 +01:00
_logger . LogDebug ( "Deleting dead chapter image {Path}" , image ) ;
2013-12-15 11:53:32 -05:00
try
{
2015-01-12 22:46:44 -05:00
_fileSystem . DeleteFile ( image ) ;
2013-12-15 11:53:32 -05:00
}
catch ( IOException ex )
{
2020-02-23 10:53:51 +01:00
_logger . LogError ( ex , "Error deleting {Path}." , image ) ;
2013-12-15 11:53:32 -05:00
}
}
}
2013-02-20 20:33:05 -05:00
}
}