2018-10-30 20:44:59 +00:00
using System ;
using System.Globalization ;
using System.Linq ;
using System.Text.RegularExpressions ;
using NLog ;
using NLog.Fluent ;
using NzbDrone.Common.Extensions ;
using NzbDrone.Common.Instrumentation ;
using NzbDrone.Common.Instrumentation.Extensions ;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public static class MediaInfoFormatter
{
2019-12-22 22:08:53 +00:00
private const string ValidHdrColourPrimaries = "BT.2020" ;
private static readonly string [ ] ValidHdrTransferFunctions = { "PQ" , "HLG" } ;
2018-10-30 20:44:59 +00:00
private static readonly Logger Logger = NzbDroneLogger . GetLogger ( typeof ( MediaInfoFormatter ) ) ;
public static decimal FormatAudioChannels ( MediaInfoModel mediaInfo )
{
var audioChannels = FormatAudioChannelsFromAudioChannelPositions ( mediaInfo ) ;
if ( audioChannels = = null )
{
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText ( mediaInfo ) ;
}
if ( audioChannels = = null )
{
audioChannels = FormatAudioChannelsFromAudioChannels ( mediaInfo ) ;
}
return audioChannels ? ? 0 ;
}
public static string FormatAudioCodec ( MediaInfoModel mediaInfo , string sceneName )
{
if ( mediaInfo . AudioCodecID = = null )
{
return FormatAudioCodecLegacy ( mediaInfo , sceneName ) ;
}
2019-09-04 21:24:29 +00:00
var audioFormat = mediaInfo . AudioFormat . Trim ( ) . Split ( new [ ] { " / " } , StringSplitOptions . RemoveEmptyEntries ) ;
2018-10-30 20:44:59 +00:00
var audioCodecID = mediaInfo . AudioCodecID ? ? string . Empty ;
var audioProfile = mediaInfo . AudioProfile ? ? string . Empty ;
var audioCodecLibrary = mediaInfo . AudioCodecLibrary ? ? string . Empty ;
2019-12-22 22:08:53 +00:00
var splitAdditionalFeatures = ( mediaInfo . AudioAdditionalFeatures ? ? string . Empty ) . Split ( new [ ] { ' ' } , StringSplitOptions . RemoveEmptyEntries ) ;
2018-10-30 20:44:59 +00:00
2019-09-04 21:24:29 +00:00
if ( audioFormat . Empty ( ) )
2018-10-30 20:44:59 +00:00
{
return string . Empty ;
}
2019-12-22 21:24:11 +00:00
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "Atmos" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
return "TrueHD Atmos" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "MLP FBA" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "16-ch" ) )
{
return "TrueHD Atmos" ;
}
return "TrueHD" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "TrueHD" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
return "TrueHD" ;
}
2018-10-30 20:44:59 +00:00
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "FLAC" ) )
{
return "FLAC" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "DTS" ) )
2018-10-30 20:44:59 +00:00
{
2019-07-01 01:50:01 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "XLL" ) )
2018-10-30 20:44:59 +00:00
{
2019-07-01 01:50:01 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "X" ) )
2018-10-30 20:44:59 +00:00
{
return "DTS-X" ;
}
2019-12-22 22:08:53 +00:00
2018-10-30 20:44:59 +00:00
return "DTS-HD MA" ;
}
2019-07-01 01:50:01 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "ES" ) )
2018-10-30 20:44:59 +00:00
{
return "DTS-ES" ;
}
2019-07-01 01:50:01 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "XBR" ) )
2018-10-30 20:44:59 +00:00
{
return "DTS-HD HRA" ;
}
return "DTS" ;
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "E-AC-3" ) )
2018-10-30 20:44:59 +00:00
{
2019-12-10 03:52:46 +00:00
if ( splitAdditionalFeatures . ContainsIgnoreCase ( "JOC" ) )
{
return "EAC3 Atmos" ;
}
2019-09-04 21:24:29 +00:00
return "EAC3" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "AC-3" ) )
{
return "AC3" ;
}
if ( audioFormat . ContainsIgnoreCase ( "AAC" ) )
{
if ( audioCodecID = = "A_AAC/MPEG4/LC/SBR" )
{
return "HE-AAC" ;
}
return "AAC" ;
}
if ( audioFormat . ContainsIgnoreCase ( "mp3" ) )
2018-10-30 20:44:59 +00:00
{
return "MP3" ;
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "MPEG Audio" ) )
2018-10-30 20:44:59 +00:00
{
if ( mediaInfo . AudioCodecID = = "55" | | mediaInfo . AudioCodecID = = "A_MPEG/L3" | | mediaInfo . AudioProfile = = "Layer 3" )
{
return "MP3" ;
}
if ( mediaInfo . AudioCodecID = = "A_MPEG/L2" | | mediaInfo . AudioProfile = = "Layer 2" )
{
return "MP2" ;
}
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "Opus" ) )
2018-10-30 20:44:59 +00:00
{
return "Opus" ;
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "PCM" ) )
2018-10-30 20:44:59 +00:00
{
return "PCM" ;
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "ADPCM" ) )
2019-07-01 01:50:01 +00:00
{
2019-09-04 21:24:29 +00:00
return "PCM" ;
2019-07-01 01:50:01 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "Vorbis" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
return "Vorbis" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "WMA" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
return "WMA" ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 21:24:29 +00:00
if ( audioFormat . ContainsIgnoreCase ( "A_QUICKTIME" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 21:24:29 +00:00
return "" ;
2018-10-30 20:44:59 +00:00
}
2019-07-01 01:50:01 +00:00
Logger . Debug ( )
2019-09-04 21:24:29 +00:00
. Message ( "Unknown audio format: '{0}' in '{1}'." , string . Join ( ", " , mediaInfo . AudioFormat , audioCodecID , audioProfile , audioCodecLibrary , mediaInfo . AudioAdditionalFeatures ) , sceneName )
. WriteSentryWarn ( "UnknownAudioFormat" , mediaInfo . ContainerFormat , mediaInfo . AudioFormat , audioCodecID )
2019-07-01 01:50:01 +00:00
. Write ( ) ;
2018-10-30 20:44:59 +00:00
2019-09-04 21:24:29 +00:00
return mediaInfo . AudioFormat ;
2018-10-30 20:44:59 +00:00
}
public static string FormatAudioCodecLegacy ( MediaInfoModel mediaInfo , string sceneName )
{
var audioFormat = mediaInfo . AudioFormat ;
if ( audioFormat . IsNullOrWhiteSpace ( ) )
{
return audioFormat ;
}
if ( audioFormat . EqualsIgnoreCase ( "AC-3" ) )
{
return "AC3" ;
}
if ( audioFormat . EqualsIgnoreCase ( "E-AC-3" ) )
{
return "EAC3" ;
}
if ( audioFormat . EqualsIgnoreCase ( "AAC" ) )
{
return "AAC" ;
}
if ( audioFormat . EqualsIgnoreCase ( "MPEG Audio" ) & & mediaInfo . AudioProfile = = "Layer 3" )
{
return "MP3" ;
}
if ( audioFormat . EqualsIgnoreCase ( "DTS" ) )
{
return "DTS" ;
}
if ( audioFormat . EqualsIgnoreCase ( "TrueHD" ) )
{
return "TrueHD" ;
}
if ( audioFormat . EqualsIgnoreCase ( "FLAC" ) )
{
return "FLAC" ;
}
if ( audioFormat . EqualsIgnoreCase ( "Vorbis" ) )
{
return "Vorbis" ;
}
if ( audioFormat . EqualsIgnoreCase ( "Opus" ) )
{
return "Opus" ;
}
return audioFormat ;
}
public static string FormatVideoCodec ( MediaInfoModel mediaInfo , string sceneName )
{
if ( mediaInfo . VideoFormat = = null )
{
return FormatVideoCodecLegacy ( mediaInfo , sceneName ) ;
}
2019-09-04 22:32:18 +00:00
var videoFormat = mediaInfo . VideoFormat . Trim ( ) . Split ( new [ ] { " / " } , StringSplitOptions . RemoveEmptyEntries ) ;
2018-10-30 20:44:59 +00:00
var videoCodecID = mediaInfo . VideoCodecID ? ? string . Empty ;
var videoProfile = mediaInfo . VideoProfile ? ? string . Empty ;
var videoCodecLibrary = mediaInfo . VideoCodecLibrary ? ? string . Empty ;
2019-09-04 22:32:18 +00:00
var result = mediaInfo . VideoFormat . Trim ( ) ;
2018-10-30 20:44:59 +00:00
2019-09-04 22:32:18 +00:00
if ( videoFormat . Empty ( ) )
2018-10-30 20:44:59 +00:00
{
return result ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "x264" ) )
2018-10-30 20:44:59 +00:00
{
return "x264" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "AVC" ) | | videoFormat . ContainsIgnoreCase ( "V.MPEG4/ISO/AVC" ) )
2018-10-30 20:44:59 +00:00
{
if ( videoCodecLibrary . StartsWithIgnoreCase ( "x264" ) )
{
return "x264" ;
}
return GetSceneNameMatch ( sceneName , "AVC" , "x264" , "h264" ) ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "HEVC" ) | | videoFormat . ContainsIgnoreCase ( "V_MPEGH/ISO/HEVC" ) )
2018-10-30 20:44:59 +00:00
{
if ( videoCodecLibrary . StartsWithIgnoreCase ( "x265" ) )
{
return "x265" ;
}
return GetSceneNameMatch ( sceneName , "HEVC" , "x265" , "h265" ) ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "MPEG Video" ) )
2018-10-30 20:44:59 +00:00
{
if ( videoCodecID = = "2" | | videoCodecID = = "V_MPEG2" )
{
return "MPEG2" ;
}
if ( videoCodecID . IsNullOrWhiteSpace ( ) )
{
return "MPEG" ;
}
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "MPEG-2 Video" ) )
2018-10-30 20:44:59 +00:00
{
return "MPEG2" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "MPEG-4 Visual" ) )
2018-10-30 20:44:59 +00:00
{
if ( videoCodecID . ContainsIgnoreCase ( "XVID" ) | |
videoCodecLibrary . StartsWithIgnoreCase ( "XviD" ) )
{
return "XviD" ;
}
if ( videoCodecID . ContainsIgnoreCase ( "DIV3" ) | |
videoCodecID . ContainsIgnoreCase ( "DIVX" ) | |
videoCodecID . ContainsIgnoreCase ( "DX50" ) | |
videoCodecLibrary . StartsWithIgnoreCase ( "DivX" ) )
{
return "DivX" ;
}
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "MPEG-4 Visual" ) | | videoFormat . ContainsIgnoreCase ( "mp4v" ) )
2018-10-30 20:44:59 +00:00
{
result = GetSceneNameMatch ( sceneName , "XviD" , "DivX" , "" ) ;
if ( result . IsNotNullOrWhiteSpace ( ) )
{
return result ;
}
2019-09-04 22:32:18 +00:00
if ( videoCodecLibrary . Contains ( "Lavc" ) )
{
return "" ; // libavcodec mpeg-4
}
if ( videoCodecLibrary . Contains ( "em4v" ) )
{
return "" ; // NeroDigital
}
if ( videoCodecLibrary . Contains ( "Intel(R) IPP" ) )
{
return "" ; // Intel(R) IPP
}
if ( videoCodecLibrary = = "" )
{
return "" ; // Unknown mp4v
}
2018-10-30 20:44:59 +00:00
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "VC-1" ) )
2018-10-30 20:44:59 +00:00
{
return "VC1" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "VP6" ) | | videoFormat . ContainsIgnoreCase ( "VP7" ) | |
videoFormat . ContainsIgnoreCase ( "VP8" ) | | videoFormat . ContainsIgnoreCase ( "VP9" ) )
2018-10-30 20:44:59 +00:00
{
2019-09-04 22:32:18 +00:00
return videoFormat . First ( ) . ToUpperInvariant ( ) ;
2018-10-30 20:44:59 +00:00
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "WMV1" ) | | videoFormat . ContainsIgnoreCase ( "WMV2" ) )
2018-10-30 20:44:59 +00:00
{
return "WMV" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "DivX" ) | | videoFormat . ContainsIgnoreCase ( "div3" ) )
2018-10-30 20:44:59 +00:00
{
return "DivX" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "XviD" ) )
2018-10-30 20:44:59 +00:00
{
return "XviD" ;
}
2019-09-04 22:32:18 +00:00
if ( videoFormat . ContainsIgnoreCase ( "V_QUICKTIME" ) | |
videoFormat . ContainsIgnoreCase ( "RealVideo 4" ) )
{
return "" ;
}
if ( videoFormat . ContainsIgnoreCase ( "mp42" ) | |
videoFormat . ContainsIgnoreCase ( "mp43" ) )
2019-09-04 21:24:29 +00:00
{
2019-09-04 22:32:18 +00:00
// MS old DivX competitor
2019-09-04 21:24:29 +00:00
return "" ;
}
2019-08-07 02:20:47 +00:00
Logger . Debug ( )
2019-12-28 07:42:56 +00:00
. Message ( "Unknown video format: '{0}' in '{1}'." , string . Join ( ", " , mediaInfo . VideoFormat , videoCodecID , videoProfile , videoCodecLibrary ) , sceneName )
2019-09-04 22:32:18 +00:00
. WriteSentryWarn ( "UnknownVideoFormat" , mediaInfo . ContainerFormat , mediaInfo . VideoFormat , videoCodecID )
2019-08-07 02:20:47 +00:00
. Write ( ) ;
2018-10-30 20:44:59 +00:00
return result ;
}
public static string FormatVideoCodecLegacy ( MediaInfoModel mediaInfo , string sceneName )
{
2018-11-07 19:57:49 +00:00
var videoCodec = mediaInfo . VideoCodec ;
2018-10-30 20:44:59 +00:00
if ( videoCodec . IsNullOrWhiteSpace ( ) )
{
return videoCodec ;
}
if ( videoCodec = = "AVC" )
{
return GetSceneNameMatch ( sceneName , "AVC" , "h264" , "x264" ) ;
}
if ( videoCodec = = "V_MPEGH/ISO/HEVC" | | videoCodec = = "HEVC" )
{
return GetSceneNameMatch ( sceneName , "HEVC" , "h265" , "x265" ) ;
}
if ( videoCodec = = "MPEG-2 Video" )
{
return "MPEG2" ;
}
if ( videoCodec = = "MPEG-4 Visual" )
{
return GetSceneNameMatch ( sceneName , "DivX" , "XviD" ) ;
}
if ( videoCodec . StartsWithIgnoreCase ( "XviD" ) )
{
return "XviD" ;
}
if ( videoCodec . StartsWithIgnoreCase ( "DivX" ) )
{
return "DivX" ;
}
if ( videoCodec . EqualsIgnoreCase ( "VC-1" ) )
{
return "VC1" ;
}
return videoCodec ;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositions ( MediaInfoModel mediaInfo )
{
var audioChannelPositions = mediaInfo . AudioChannelPositions ;
if ( audioChannelPositions . IsNullOrWhiteSpace ( ) )
{
return null ;
}
try
{
if ( audioChannelPositions . Contains ( "+" ) )
{
return audioChannelPositions . Split ( '+' )
. Sum ( s = > decimal . Parse ( s . Trim ( ) , CultureInfo . InvariantCulture ) ) ;
}
if ( audioChannelPositions . Contains ( "/" ) )
{
2019-12-24 06:24:47 +00:00
var channelStringList = Regex . Replace ( audioChannelPositions ,
2019-12-22 22:08:53 +00:00
@"^\d+\sobjects" ,
"" ,
2019-07-01 01:50:01 +00:00
RegexOptions . Compiled | RegexOptions . IgnoreCase )
. Replace ( "Object Based / " , "" )
2019-12-22 22:08:53 +00:00
. Split ( new string [ ] { " / " } , StringSplitOptions . RemoveEmptyEntries )
2019-07-01 01:50:01 +00:00
. FirstOrDefault ( )
2019-12-24 06:24:47 +00:00
? . Split ( '/' ) ;
var positions = default ( decimal ) ;
if ( channelStringList = = null )
{
return 0 ;
}
foreach ( var channel in channelStringList )
{
var channelSplit = channel . Split ( new string [ ] { "." } , StringSplitOptions . None ) ;
if ( channelSplit . Count ( ) = = 3 )
{
positions + = decimal . Parse ( string . Format ( "{0}.{1}" , channelSplit [ 1 ] , channelSplit [ 2 ] ) ) ;
}
else
{
positions + = decimal . Parse ( channel ) ;
}
}
return positions ;
2018-10-30 20:44:59 +00:00
}
}
2020-05-03 20:30:49 +00:00
catch ( Exception ex )
2018-10-30 20:44:59 +00:00
{
2019-09-27 01:31:46 +00:00
Logger . Warn ( )
2020-05-03 20:30:49 +00:00
. Message ( "Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}' and '{1}'. Error {2}" , audioChannelPositions , mediaInfo . AudioChannelPositionsText , ex . Message )
2019-10-01 00:46:22 +00:00
. WriteSentryWarn ( "UnknownAudioChannelFormat" , audioChannelPositions , mediaInfo . AudioChannelPositionsText )
2019-09-27 01:31:46 +00:00
. Write ( ) ;
2018-10-30 20:44:59 +00:00
}
return null ;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositionsText ( MediaInfoModel mediaInfo )
{
var audioChannelPositionsText = mediaInfo . AudioChannelPositionsText ;
var audioChannels = mediaInfo . AudioChannels ;
if ( audioChannelPositionsText . IsNullOrWhiteSpace ( ) )
{
return null ;
}
try
{
return audioChannelPositionsText . ContainsIgnoreCase ( "LFE" ) ? audioChannels - 1 + 0.1 m : audioChannels ;
}
catch ( Exception e )
{
2019-07-01 01:50:01 +00:00
Logger . Warn ( e , "Unable to format audio channels using 'AudioChannelPositionsText', with a value of: '{0}'" , audioChannelPositionsText ) ;
2018-10-30 20:44:59 +00:00
}
return null ;
}
private static decimal? FormatAudioChannelsFromAudioChannels ( MediaInfoModel mediaInfo )
{
var audioChannels = mediaInfo . AudioChannels ;
if ( mediaInfo . SchemaRevision > = 3 )
{
return audioChannels ;
}
return null ;
}
private static string GetSceneNameMatch ( string sceneName , params string [ ] tokens )
{
sceneName = sceneName . IsNotNullOrWhiteSpace ( ) ? Parser . Parser . RemoveFileExtension ( sceneName ) : string . Empty ;
foreach ( var token in tokens )
{
if ( sceneName . ContainsIgnoreCase ( token ) )
{
return token ;
}
}
// Last token is the default.
return tokens . Last ( ) ;
}
2019-07-01 01:50:01 +00:00
public static string FormatVideoDynamicRange ( MediaInfoModel mediaInfo )
{
// assume SDR by default
var videoDynamicRange = "" ;
if ( mediaInfo . VideoBitDepth > = 10 & &
2019-07-27 02:41:32 +00:00
mediaInfo . VideoColourPrimaries . IsNotNullOrWhiteSpace ( ) & &
mediaInfo . VideoTransferCharacteristics . IsNotNullOrWhiteSpace ( ) )
2019-07-01 01:50:01 +00:00
{
if ( mediaInfo . VideoColourPrimaries . EqualsIgnoreCase ( ValidHdrColourPrimaries ) & &
ValidHdrTransferFunctions . Any ( mediaInfo . VideoTransferCharacteristics . Contains ) )
{
videoDynamicRange = "HDR" ;
}
}
return videoDynamicRange ;
}
2018-10-30 20:44:59 +00:00
}
}