2014-02-04 15:19:29 -05:00
using DvdLib.Ifo ;
2014-02-09 16:11:11 -05:00
using MediaBrowser.Common.Configuration ;
2015-01-17 15:12:02 -05:00
using MediaBrowser.Model.Extensions ;
2014-04-24 22:00:19 -04:00
using MediaBrowser.Common.IO ;
2014-06-09 15:16:14 -04:00
using MediaBrowser.Controller.Chapters ;
2014-05-06 22:28:19 -04:00
using MediaBrowser.Controller.Configuration ;
2014-02-04 15:19:29 -05:00
using MediaBrowser.Controller.Entities ;
2014-05-06 22:28:19 -04:00
using MediaBrowser.Controller.Entities.Movies ;
using MediaBrowser.Controller.Entities.TV ;
2014-02-04 15:19:29 -05:00
using MediaBrowser.Controller.Library ;
using MediaBrowser.Controller.Localization ;
2014-02-20 11:37:41 -05:00
using MediaBrowser.Controller.MediaEncoding ;
2014-02-04 15:19:29 -05:00
using MediaBrowser.Controller.Persistence ;
2014-02-10 13:39:41 -05:00
using MediaBrowser.Controller.Providers ;
2014-05-06 22:28:19 -04:00
using MediaBrowser.Controller.Subtitles ;
2014-09-20 11:48:23 -04:00
using MediaBrowser.MediaInfo ;
2014-08-10 18:13:17 -04:00
using MediaBrowser.Model.Configuration ;
2014-02-04 15:19:29 -05:00
using MediaBrowser.Model.Entities ;
using MediaBrowser.Model.IO ;
using MediaBrowser.Model.Logging ;
using MediaBrowser.Model.MediaInfo ;
2014-08-10 18:13:17 -04:00
using MediaBrowser.Model.Providers ;
2014-02-09 16:11:11 -05:00
using MediaBrowser.Model.Serialization ;
2014-02-04 15:19:29 -05:00
using System ;
using System.Collections.Generic ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
namespace MediaBrowser.Providers.MediaInfo
{
public class FFProbeVideoInfo
{
private readonly ILogger _logger ;
private readonly IIsoManager _isoManager ;
private readonly IMediaEncoder _mediaEncoder ;
private readonly IItemRepository _itemRepo ;
private readonly IBlurayExaminer _blurayExaminer ;
private readonly ILocalizationManager _localization ;
2014-02-09 16:11:11 -05:00
private readonly IApplicationPaths _appPaths ;
private readonly IJsonSerializer _json ;
2014-02-20 11:37:41 -05:00
private readonly IEncodingManager _encodingManager ;
2014-04-24 22:00:19 -04:00
private readonly IFileSystem _fileSystem ;
2014-05-06 22:28:19 -04:00
private readonly IServerConfigurationManager _config ;
private readonly ISubtitleManager _subtitleManager ;
2014-06-09 15:16:14 -04:00
private readonly IChapterManager _chapterManager ;
2014-02-04 15:19:29 -05:00
private readonly CultureInfo _usCulture = new CultureInfo ( "en-US" ) ;
2014-06-09 15:16:14 -04:00
public FFProbeVideoInfo ( ILogger logger , IIsoManager isoManager , IMediaEncoder mediaEncoder , IItemRepository itemRepo , IBlurayExaminer blurayExaminer , ILocalizationManager localization , IApplicationPaths appPaths , IJsonSerializer json , IEncodingManager encodingManager , IFileSystem fileSystem , IServerConfigurationManager config , ISubtitleManager subtitleManager , IChapterManager chapterManager )
2014-02-04 15:19:29 -05:00
{
_logger = logger ;
_isoManager = isoManager ;
_mediaEncoder = mediaEncoder ;
_itemRepo = itemRepo ;
_blurayExaminer = blurayExaminer ;
_localization = localization ;
2014-02-09 16:11:11 -05:00
_appPaths = appPaths ;
_json = json ;
2014-02-20 11:37:41 -05:00
_encodingManager = encodingManager ;
2014-04-24 22:45:06 -04:00
_fileSystem = fileSystem ;
2014-05-06 22:28:19 -04:00
_config = config ;
_subtitleManager = subtitleManager ;
2014-06-09 15:16:14 -04:00
_chapterManager = chapterManager ;
2014-02-04 15:19:29 -05:00
}
2014-06-09 15:16:14 -04:00
public async Task < ItemUpdateType > ProbeVideo < T > ( T item ,
MetadataRefreshOptions options ,
CancellationToken cancellationToken )
2014-02-04 15:19:29 -05:00
where T : Video
{
2014-12-29 15:18:48 -05:00
if ( item . IsArchive )
{
var ext = Path . GetExtension ( item . Path ) ? ? string . Empty ;
item . Container = ext . TrimStart ( '.' ) ;
return ItemUpdateType . MetadataImport ;
}
2014-02-04 15:19:29 -05:00
var isoMount = await MountIsoIfNeeded ( item , cancellationToken ) . ConfigureAwait ( false ) ;
2014-03-04 21:59:59 -05:00
BlurayDiscInfo blurayDiscInfo = null ;
2014-02-04 15:19:29 -05:00
try
{
2014-03-04 21:59:59 -05:00
if ( item . VideoType = = VideoType . BluRay | | ( item . IsoType . HasValue & & item . IsoType = = IsoType . BluRay ) )
{
var inputPath = isoMount ! = null ? isoMount . MountedPath : item . Path ;
blurayDiscInfo = GetBDInfo ( inputPath ) ;
}
OnPreFetch ( item , isoMount , blurayDiscInfo ) ;
2014-02-04 15:19:29 -05:00
// If we didn't find any satisfying the min length, just take them all
if ( item . VideoType = = VideoType . Dvd | | ( item . IsoType . HasValue & & item . IsoType = = IsoType . Dvd ) )
{
if ( item . PlayableStreamFileNames . Count = = 0 )
{
_logger . Error ( "No playable vobs found in dvd structure, skipping ffprobe." ) ;
return ItemUpdateType . MetadataImport ;
}
}
2014-03-04 21:59:59 -05:00
if ( item . VideoType = = VideoType . BluRay | | ( item . IsoType . HasValue & & item . IsoType = = IsoType . BluRay ) )
{
if ( item . PlayableStreamFileNames . Count = = 0 )
{
_logger . Error ( "No playable vobs found in bluray structure, skipping ffprobe." ) ;
return ItemUpdateType . MetadataImport ;
}
}
2014-02-04 15:19:29 -05:00
var result = await GetMediaInfo ( item , isoMount , cancellationToken ) . ConfigureAwait ( false ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
FFProbeHelpers . NormalizeFFProbeResult ( result ) ;
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-06-09 15:16:14 -04:00
await Fetch ( item , cancellationToken , result , isoMount , blurayDiscInfo , options ) . ConfigureAwait ( false ) ;
2014-02-04 15:19:29 -05:00
}
finally
{
if ( isoMount ! = null )
{
isoMount . Dispose ( ) ;
}
}
return ItemUpdateType . MetadataImport ;
}
2014-03-26 17:05:31 -04:00
private const string SchemaVersion = "1" ;
2014-06-16 21:56:23 -04:00
private async Task < InternalMediaInfoResult > GetMediaInfo ( Video item ,
2014-06-09 15:16:14 -04:00
IIsoMount isoMount ,
CancellationToken cancellationToken )
2014-02-04 15:19:29 -05:00
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-02-09 16:11:11 -05:00
var idString = item . Id . ToString ( "N" ) ;
2014-05-06 22:28:19 -04:00
var cachePath = Path . Combine ( _appPaths . CachePath ,
2014-03-26 17:05:31 -04:00
"ffprobe-video" ,
idString . Substring ( 0 , 2 ) , idString , "v" + SchemaVersion + _mediaEncoder . Version + item . DateModified . Ticks . ToString ( _usCulture ) + ".json" ) ;
2014-02-09 16:11:11 -05:00
try
{
return _json . DeserializeFromFile < InternalMediaInfoResult > ( cachePath ) ;
}
catch ( FileNotFoundException )
{
}
catch ( DirectoryNotFoundException )
{
}
2014-06-16 21:56:23 -04:00
var protocol = item . LocationType = = LocationType . Remote
? MediaProtocol . Http
: MediaProtocol . File ;
2014-02-04 15:19:29 -05:00
2014-06-16 21:56:23 -04:00
var inputPath = MediaEncoderHelpers . GetInputArgument ( item . Path , protocol , isoMount , item . PlayableStreamFileNames ) ;
2014-02-04 15:19:29 -05:00
2014-06-16 21:56:23 -04:00
var result = await _mediaEncoder . GetMediaInfo ( inputPath , protocol , false , cancellationToken ) . ConfigureAwait ( false ) ;
2014-02-09 16:11:11 -05:00
Directory . CreateDirectory ( Path . GetDirectoryName ( cachePath ) ) ;
_json . SerializeToFile ( result , cachePath ) ;
return result ;
2014-02-04 15:19:29 -05:00
}
2014-06-09 15:16:14 -04:00
protected async Task Fetch ( Video video ,
CancellationToken cancellationToken ,
InternalMediaInfoResult data ,
IIsoMount isoMount ,
BlurayDiscInfo blurayInfo ,
MetadataRefreshOptions options )
2014-02-04 15:19:29 -05:00
{
2014-04-22 13:25:54 -04:00
var mediaInfo = MediaEncoderHelpers . GetMediaInfo ( data ) ;
var mediaStreams = mediaInfo . MediaStreams ;
video . TotalBitrate = mediaInfo . TotalBitrate ;
video . FormatName = ( mediaInfo . Format ? ? string . Empty )
. Replace ( "matroska" , "mkv" , StringComparison . OrdinalIgnoreCase ) ;
2014-02-04 15:19:29 -05:00
if ( data . format ! = null )
{
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
var needToSetRuntime = video . VideoType ! = VideoType . Dvd | | video . RunTimeTicks = = null | | video . RunTimeTicks . Value = = 0 ;
if ( needToSetRuntime & & ! string . IsNullOrEmpty ( data . format . duration ) )
{
video . RunTimeTicks = TimeSpan . FromSeconds ( double . Parse ( data . format . duration , _usCulture ) ) . Ticks ;
}
2014-04-18 01:03:01 -04:00
if ( video . VideoType = = VideoType . VideoFile )
{
var extension = ( Path . GetExtension ( video . Path ) ? ? string . Empty ) . TrimStart ( '.' ) ;
video . Container = extension ;
}
else
{
video . Container = null ;
}
if ( ! string . IsNullOrEmpty ( data . format . size ) )
{
video . Size = long . Parse ( data . format . size , _usCulture ) ;
}
else
{
video . Size = null ;
}
2014-02-04 15:19:29 -05:00
}
2014-03-26 17:05:31 -04:00
var mediaChapters = ( data . Chapters ? ? new MediaChapter [ ] { } ) . ToList ( ) ;
var chapters = mediaChapters . Select ( GetChapterInfo ) . ToList ( ) ;
2014-02-04 15:19:29 -05:00
if ( video . VideoType = = VideoType . BluRay | | ( video . IsoType . HasValue & & video . IsoType . Value = = IsoType . BluRay ) )
{
2014-03-04 21:59:59 -05:00
FetchBdInfo ( video , chapters , mediaStreams , blurayInfo ) ;
2014-02-04 15:19:29 -05:00
}
2014-06-09 15:16:14 -04:00
await AddExternalSubtitles ( video , mediaStreams , options , cancellationToken ) . ConfigureAwait ( false ) ;
2014-02-04 15:19:29 -05:00
FetchWtvInfo ( video , data ) ;
video . IsHD = mediaStreams . Any ( i = > i . Type = = MediaStreamType . Video & & i . Width . HasValue & & i . Width . Value > = 1270 ) ;
var videoStream = mediaStreams . FirstOrDefault ( i = > i . Type = = MediaStreamType . Video ) ;
video . VideoBitRate = videoStream = = null ? null : videoStream . BitRate ;
video . DefaultVideoStreamIndex = videoStream = = null ? ( int? ) null : videoStream . Index ;
video . HasSubtitles = mediaStreams . Any ( i = > i . Type = = MediaStreamType . Subtitle ) ;
2014-04-24 22:00:19 -04:00
ExtractTimestamp ( video ) ;
2014-09-20 11:48:23 -04:00
UpdateFromMediaInfo ( video , videoStream ) ;
2014-04-24 22:00:19 -04:00
2014-06-09 15:16:14 -04:00
await _itemRepo . SaveMediaStreams ( video . Id , mediaStreams , cancellationToken ) . ConfigureAwait ( false ) ;
if ( options . MetadataRefreshMode = = MetadataRefreshMode . FullRefresh | |
2014-07-03 22:22:57 -04:00
options . MetadataRefreshMode = = MetadataRefreshMode . Default )
2014-02-20 11:37:41 -05:00
{
2014-09-08 21:15:31 -04:00
var chapterOptions = _chapterManager . GetConfiguration ( ) ;
2014-06-13 10:24:14 -04:00
try
{
2014-09-08 21:15:31 -04:00
var remoteChapters = await DownloadChapters ( video , chapters , chapterOptions , cancellationToken ) . ConfigureAwait ( false ) ;
2014-06-09 15:16:14 -04:00
2014-06-13 10:24:14 -04:00
if ( remoteChapters . Count > 0 )
{
chapters = remoteChapters ;
}
}
catch ( Exception ex )
2014-06-09 15:16:14 -04:00
{
2014-06-13 10:24:14 -04:00
_logger . ErrorException ( "Error downloading chapters" , ex ) ;
2014-06-09 15:16:14 -04:00
}
2014-02-20 11:37:41 -05:00
2014-06-09 15:16:14 -04:00
if ( chapters . Count = = 0 & & mediaStreams . Any ( i = > i . Type = = MediaStreamType . Video ) )
{
AddDummyChapters ( video , chapters ) ;
}
2014-02-04 15:19:29 -05:00
2014-07-11 22:31:08 -04:00
NormalizeChapterNames ( chapters ) ;
2014-06-09 15:16:14 -04:00
await _encodingManager . RefreshChapterImages ( new ChapterImageRefreshOptions
{
Chapters = chapters ,
Video = video ,
2014-09-08 21:15:31 -04:00
ExtractImages = chapterOptions . ExtractDuringLibraryScan ,
2014-06-09 15:16:14 -04:00
SaveChapters = false
} , cancellationToken ) . ConfigureAwait ( false ) ;
2014-02-04 15:19:29 -05:00
2014-06-10 13:36:06 -04:00
await _chapterManager . SaveChapters ( video . Id . ToString ( ) , chapters , cancellationToken ) . ConfigureAwait ( false ) ;
2014-06-09 15:16:14 -04:00
}
2014-02-04 15:19:29 -05:00
}
2014-09-20 11:48:23 -04:00
private void UpdateFromMediaInfo ( Video video , MediaStream videoStream )
{
if ( video . VideoType = = VideoType . VideoFile & & video . LocationType ! = LocationType . Remote & & video . LocationType ! = LocationType . Virtual )
{
if ( videoStream ! = null )
{
try
{
var result = new MediaInfoLib ( ) . GetVideoInfo ( video . Path ) ;
2014-10-23 00:26:01 -04:00
videoStream . IsCabac = result . IsCabac ? ? videoStream . IsCabac ;
2014-09-20 11:48:23 -04:00
videoStream . IsInterlaced = result . IsInterlaced ? ? videoStream . IsInterlaced ;
videoStream . BitDepth = result . BitDepth ? ? videoStream . BitDepth ;
videoStream . RefFrames = result . RefFrames ;
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error running MediaInfo on {0}" , ex , video . Path ) ;
}
}
}
}
2014-07-11 22:31:08 -04:00
private void NormalizeChapterNames ( List < ChapterInfo > chapters )
{
var index = 1 ;
foreach ( var chapter in chapters )
{
TimeSpan time ;
// Check if the name is empty and/or if the name is a time
// Some ripping programs do that.
if ( string . IsNullOrWhiteSpace ( chapter . Name ) | |
TimeSpan . TryParse ( chapter . Name , out time ) )
{
chapter . Name = string . Format ( _localization . GetLocalizedString ( "LabelChapterName" ) , index . ToString ( CultureInfo . InvariantCulture ) ) ;
}
index + + ;
}
}
2014-03-26 17:05:31 -04:00
private ChapterInfo GetChapterInfo ( MediaChapter chapter )
{
var info = new ChapterInfo ( ) ;
if ( chapter . tags ! = null )
{
string name ;
if ( chapter . tags . TryGetValue ( "title" , out name ) )
{
info . Name = name ;
}
}
2014-05-14 16:43:31 -04:00
// Limit accuracy to milliseconds to match xml saving
2014-06-10 13:36:06 -04:00
var secondsString = chapter . start_time ;
double seconds ;
if ( double . TryParse ( secondsString , NumberStyles . Any , CultureInfo . InvariantCulture , out seconds ) )
{
var ms = Math . Round ( TimeSpan . FromSeconds ( seconds ) . TotalMilliseconds ) ;
info . StartPositionTicks = TimeSpan . FromMilliseconds ( ms ) . Ticks ;
}
2014-03-26 17:05:31 -04:00
return info ;
}
2014-03-04 21:59:59 -05:00
private void FetchBdInfo ( BaseItem item , List < ChapterInfo > chapters , List < MediaStream > mediaStreams , BlurayDiscInfo blurayInfo )
2014-02-04 15:19:29 -05:00
{
var video = ( Video ) item ;
int? currentHeight = null ;
int? currentWidth = null ;
int? currentBitRate = null ;
var videoStream = mediaStreams . FirstOrDefault ( s = > s . Type = = MediaStreamType . Video ) ;
// Grab the values that ffprobe recorded
if ( videoStream ! = null )
{
currentBitRate = videoStream . BitRate ;
currentWidth = videoStream . Width ;
currentHeight = videoStream . Height ;
}
// Fill video properties from the BDInfo result
mediaStreams . Clear ( ) ;
2014-03-04 21:59:59 -05:00
mediaStreams . AddRange ( blurayInfo . MediaStreams ) ;
2014-02-04 15:19:29 -05:00
2014-03-04 21:59:59 -05:00
video . MainFeaturePlaylistName = blurayInfo . PlaylistName ;
2014-02-04 15:19:29 -05:00
2014-03-04 21:59:59 -05:00
if ( blurayInfo . RunTimeTicks . HasValue & & blurayInfo . RunTimeTicks . Value > 0 )
2014-02-04 15:19:29 -05:00
{
2014-03-04 21:59:59 -05:00
video . RunTimeTicks = blurayInfo . RunTimeTicks ;
2014-02-04 15:19:29 -05:00
}
2014-03-04 21:59:59 -05:00
video . PlayableStreamFileNames = blurayInfo . Files . ToList ( ) ;
2014-02-04 15:19:29 -05:00
2014-03-04 21:59:59 -05:00
if ( blurayInfo . Chapters ! = null )
2014-02-04 15:19:29 -05:00
{
chapters . Clear ( ) ;
2014-03-04 21:59:59 -05:00
chapters . AddRange ( blurayInfo . Chapters . Select ( c = > new ChapterInfo
2014-02-04 15:19:29 -05:00
{
StartPositionTicks = TimeSpan . FromSeconds ( c ) . Ticks
} ) ) ;
}
2014-03-04 21:59:59 -05:00
videoStream = mediaStreams . FirstOrDefault ( s = > s . Type = = MediaStreamType . Video ) ;
// Use the ffprobe values if these are empty
if ( videoStream ! = null )
{
videoStream . BitRate = IsEmpty ( videoStream . BitRate ) ? currentBitRate : videoStream . BitRate ;
videoStream . Width = IsEmpty ( videoStream . Width ) ? currentWidth : videoStream . Width ;
videoStream . Height = IsEmpty ( videoStream . Height ) ? currentHeight : videoStream . Height ;
}
}
private bool IsEmpty ( int? num )
{
return ! num . HasValue | | num . Value = = 0 ;
2014-02-04 15:19:29 -05:00
}
/// <summary>
/// Gets information about the longest playlist on a bdrom
/// </summary>
/// <param name="path">The path.</param>
/// <returns>VideoStream.</returns>
private BlurayDiscInfo GetBDInfo ( string path )
{
return _blurayExaminer . GetDiscInfo ( path ) ;
}
2015-03-05 01:34:36 -05:00
public const int MaxSubtitleDescriptionExtractionLength = 100 ; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
2014-02-04 15:19:29 -05:00
private void FetchWtvInfo ( Video video , InternalMediaInfoResult data )
{
if ( data . format = = null | | data . format . tags = = null )
{
return ;
}
2015-03-05 01:34:36 -05:00
if ( ! video . LockedFields . Contains ( MetadataFields . Genres ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
var genres = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/Genre" ) ;
2014-02-04 15:19:29 -05:00
2015-03-05 01:34:36 -05:00
if ( ! string . IsNullOrWhiteSpace ( genres ) )
{
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
2014-02-04 15:19:29 -05:00
}
2015-03-05 01:34:36 -05:00
if ( ! string . IsNullOrWhiteSpace ( genres ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
video . Genres = genres . Split ( new [ ] { ';' , '/' , ',' } , StringSplitOptions . RemoveEmptyEntries )
. Where ( i = > ! string . IsNullOrWhiteSpace ( i ) )
. Select ( i = > i . Trim ( ) )
. ToList ( ) ;
2014-02-04 15:19:29 -05:00
}
}
2015-03-05 01:34:36 -05:00
if ( ! video . LockedFields . Contains ( MetadataFields . OfficialRating ) )
2014-02-04 15:19:29 -05:00
{
var officialRating = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/ParentalRating" ) ;
if ( ! string . IsNullOrWhiteSpace ( officialRating ) )
{
2015-03-05 01:34:36 -05:00
video . OfficialRating = officialRating ;
2014-02-04 15:19:29 -05:00
}
}
2015-03-05 01:34:36 -05:00
if ( ! video . LockedFields . Contains ( MetadataFields . Cast ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
var people = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/MediaCredits" ) ;
if ( ! string . IsNullOrEmpty ( people ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
video . People = people . Split ( new [ ] { ';' , '/' } , StringSplitOptions . RemoveEmptyEntries )
. Where ( i = > ! string . IsNullOrWhiteSpace ( i ) )
. Select ( i = > new PersonInfo { Name = i . Trim ( ) , Type = PersonType . Actor } )
. ToList ( ) ;
}
}
2014-02-04 15:19:29 -05:00
2015-03-05 01:34:36 -05:00
var year = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/OriginalReleaseTime" ) ;
if ( ! string . IsNullOrWhiteSpace ( year ) )
{
int val ;
if ( int . TryParse ( year , NumberStyles . Integer , _usCulture , out val ) )
{
video . ProductionYear = val ;
2014-02-04 15:19:29 -05:00
}
}
2015-03-05 01:34:36 -05:00
var premiereDateString = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/MediaOriginalBroadcastDateTime" ) ;
if ( ! string . IsNullOrWhiteSpace ( premiereDateString ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
DateTime val ;
2014-02-04 15:19:29 -05:00
2015-03-05 01:34:36 -05:00
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
if ( DateTime . TryParse ( year , null , DateTimeStyles . None , out val ) )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
video . PremiereDate = val . ToUniversalTime ( ) ;
}
}
var description = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/SubTitleDescription" ) ;
var episode = video as Episode ;
if ( episode ! = null )
{
var subTitle = FFProbeHelpers . GetDictionaryValue ( data . format . tags , "WM/SubTitle" ) ;
2014-02-04 15:19:29 -05:00
2015-03-05 01:34:36 -05:00
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if ( String . IsNullOrWhiteSpace ( subTitle ) & & ! String . IsNullOrWhiteSpace ( description ) & & description . Substring ( 0 , Math . Min ( description . Length , MaxSubtitleDescriptionExtractionLength ) ) . Contains ( ":" ) ) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string [ ] parts = description . Split ( ':' ) ;
if ( parts . Length > 0 )
2014-02-04 15:19:29 -05:00
{
2015-03-05 01:34:36 -05:00
string subtitle = parts [ 0 ] ;
try
{
if ( subtitle . Contains ( "/" ) ) // It contains a episode number and season number
{
string [ ] numbers = subtitle . Split ( ' ' ) ;
episode . IndexNumber = int . Parse ( numbers [ 0 ] . Replace ( "." , "" ) . Split ( '/' ) [ 0 ] ) ;
int totalEpisodesInSeason = int . Parse ( numbers [ 0 ] . Replace ( "." , "" ) . Split ( '/' ) [ 1 ] ) ;
description = String . Join ( " " , numbers , 1 , numbers . Length - 1 ) . Trim ( ) ; // Skip the first, concatenate the rest, clean up spaces and save it
}
else
throw new Exception ( ) ; // Switch to default parsing
}
catch // Default parsing
{
if ( subtitle . Contains ( "." ) ) // skip the comment, keep the subtitle
description = String . Join ( "." , subtitle . Split ( '.' ) , 1 , subtitle . Split ( '.' ) . Length - 1 ) . Trim ( ) ; // skip the first
else
description = subtitle . Trim ( ) ; // Clean up whitespaces and save it
}
2014-02-04 15:19:29 -05:00
}
}
}
2015-03-05 01:34:36 -05:00
if ( ! video . LockedFields . Contains ( MetadataFields . Overview ) )
{
if ( ! string . IsNullOrWhiteSpace ( description ) )
{
video . Overview = description ;
}
}
2014-02-04 15:19:29 -05:00
}
2014-08-10 18:13:17 -04:00
private SubtitleOptions GetOptions ( )
{
return _config . GetConfiguration < SubtitleOptions > ( "subtitles" ) ;
}
2014-02-04 15:19:29 -05:00
/// <summary>
/// Adds the external subtitles.
/// </summary>
/// <param name="video">The video.</param>
/// <param name="currentStreams">The current streams.</param>
2014-06-09 15:16:14 -04:00
/// <param name="options">The options.</param>
2014-05-17 00:24:10 -04:00
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
2014-06-09 15:16:14 -04:00
private async Task AddExternalSubtitles ( Video video ,
List < MediaStream > currentStreams ,
MetadataRefreshOptions options ,
CancellationToken cancellationToken )
2014-05-06 22:28:19 -04:00
{
2014-07-26 13:30:15 -04:00
var subtitleResolver = new SubtitleResolver ( _localization , _fileSystem ) ;
2014-05-16 15:16:29 -04:00
2014-06-09 15:16:14 -04:00
var externalSubtitleStreams = subtitleResolver . GetExternalSubtitleStreams ( video , currentStreams . Count , options . DirectoryService , false ) . ToList ( ) ;
2014-07-03 22:22:57 -04:00
var enableSubtitleDownloading = options . MetadataRefreshMode = = MetadataRefreshMode . Default | |
2014-06-09 15:16:14 -04:00
options . MetadataRefreshMode = = MetadataRefreshMode . FullRefresh ;
2014-05-06 22:28:19 -04:00
2014-08-10 18:13:17 -04:00
var subtitleOptions = GetOptions ( ) ;
if ( enableSubtitleDownloading & & ( subtitleOptions . DownloadEpisodeSubtitles & &
2014-05-06 22:28:19 -04:00
video is Episode ) | |
2014-08-10 18:13:17 -04:00
( subtitleOptions . DownloadMovieSubtitles & &
2014-05-06 22:28:19 -04:00
video is Movie ) )
{
var downloadedLanguages = await new SubtitleDownloader ( _logger ,
_subtitleManager )
. DownloadSubtitles ( video ,
2014-09-06 00:21:23 -04:00
currentStreams . Concat ( externalSubtitleStreams ) . ToList ( ) ,
2014-08-10 18:13:17 -04:00
subtitleOptions . SkipIfGraphicalSubtitlesPresent ,
subtitleOptions . SkipIfAudioTrackMatches ,
subtitleOptions . DownloadLanguages ,
2014-05-06 22:28:19 -04:00
cancellationToken ) . ConfigureAwait ( false ) ;
// Rescan
if ( downloadedLanguages . Count > 0 )
{
2014-06-09 15:16:14 -04:00
externalSubtitleStreams = subtitleResolver . GetExternalSubtitleStreams ( video , currentStreams . Count , options . DirectoryService , true ) . ToList ( ) ;
2014-05-06 22:28:19 -04:00
}
}
video . SubtitleFiles = externalSubtitleStreams . Select ( i = > i . Path ) . OrderBy ( i = > i ) . ToList ( ) ;
currentStreams . AddRange ( externalSubtitleStreams ) ;
}
2014-09-08 21:15:31 -04:00
private async Task < List < ChapterInfo > > DownloadChapters ( Video video , List < ChapterInfo > currentChapters , ChapterOptions options , CancellationToken cancellationToken )
2014-06-09 15:16:14 -04:00
{
2014-06-29 13:35:05 -04:00
if ( ( options . DownloadEpisodeChapters & &
2014-06-09 15:16:14 -04:00
video is Episode ) | |
2014-06-29 13:35:05 -04:00
( options . DownloadMovieChapters & &
2014-06-09 15:16:14 -04:00
video is Movie ) )
{
var results = await _chapterManager . Search ( video , cancellationToken ) . ConfigureAwait ( false ) ;
var result = results . FirstOrDefault ( ) ;
if ( result ! = null )
{
var chapters = await _chapterManager . GetChapters ( result . Id , cancellationToken ) . ConfigureAwait ( false ) ;
2014-06-11 22:38:40 -04:00
var chapterInfos = chapters . Chapters . Select ( i = > new ChapterInfo
2014-06-09 15:16:14 -04:00
{
Name = i . Name ,
StartPositionTicks = i . StartPositionTicks
} ) . ToList ( ) ;
2014-06-11 22:38:40 -04:00
if ( chapterInfos . All ( i = > i . StartPositionTicks = = 0 ) )
{
if ( currentChapters . Count > = chapterInfos . Count )
{
var index = 0 ;
foreach ( var info in chapterInfos )
{
info . StartPositionTicks = currentChapters [ index ] . StartPositionTicks ;
index + + ;
}
}
else
{
chapterInfos . Clear ( ) ;
}
}
return chapterInfos ;
2014-06-09 15:16:14 -04:00
}
}
return new List < ChapterInfo > ( ) ;
}
2014-02-04 15:19:29 -05:00
/// <summary>
/// The dummy chapter duration
/// </summary>
private readonly long _dummyChapterDuration = TimeSpan . FromMinutes ( 5 ) . Ticks ;
/// <summary>
/// Adds the dummy chapters.
/// </summary>
/// <param name="video">The video.</param>
/// <param name="chapters">The chapters.</param>
private void AddDummyChapters ( Video video , List < ChapterInfo > chapters )
{
var runtime = video . RunTimeTicks ? ? 0 ;
if ( runtime < 0 )
{
throw new ArgumentException ( string . Format ( "{0} has invalid runtime of {1}" , video . Name , runtime ) ) ;
}
if ( runtime < _dummyChapterDuration )
{
return ;
}
long currentChapterTicks = 0 ;
var index = 1 ;
// Limit to 100 chapters just in case there's some incorrect metadata here
while ( currentChapterTicks < runtime & & index < 100 )
{
chapters . Add ( new ChapterInfo
{
StartPositionTicks = currentChapterTicks
} ) ;
index + + ;
currentChapterTicks + = _dummyChapterDuration ;
}
}
/// <summary>
/// Called when [pre fetch].
/// </summary>
/// <param name="item">The item.</param>
/// <param name="mount">The mount.</param>
2014-06-09 15:16:14 -04:00
/// <param name="blurayDiscInfo">The bluray disc information.</param>
2014-03-04 21:59:59 -05:00
private void OnPreFetch ( Video item , IIsoMount mount , BlurayDiscInfo blurayDiscInfo )
2014-02-04 15:19:29 -05:00
{
if ( item . VideoType = = VideoType . Iso )
{
item . IsoType = DetermineIsoType ( mount ) ;
}
if ( item . VideoType = = VideoType . Dvd | | ( item . IsoType . HasValue & & item . IsoType = = IsoType . Dvd ) )
{
FetchFromDvdLib ( item , mount ) ;
}
2014-03-04 21:59:59 -05:00
if ( item . VideoType = = VideoType . BluRay | | ( item . IsoType . HasValue & & item . IsoType . Value = = IsoType . BluRay ) )
{
item . PlayableStreamFileNames = blurayDiscInfo . Files . ToList ( ) ;
}
2014-02-04 15:19:29 -05:00
}
2014-04-24 22:00:19 -04:00
private void ExtractTimestamp ( Video video )
{
if ( video . VideoType = = VideoType . VideoFile )
{
if ( string . Equals ( video . Container , "mpeg2ts" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( video . Container , "m2ts" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( video . Container , "ts" , StringComparison . OrdinalIgnoreCase ) )
{
try
{
video . Timestamp = GetMpegTimestamp ( video . Path ) ;
2014-04-24 22:45:06 -04:00
_logger . Debug ( "Video has {0} timestamp" , video . Timestamp ) ;
2014-04-24 22:00:19 -04:00
}
catch ( Exception ex )
{
_logger . ErrorException ( "Error extracting timestamp info from {0}" , ex , video . Path ) ;
2014-04-24 22:45:06 -04:00
video . Timestamp = null ;
2014-04-24 22:00:19 -04:00
}
}
}
}
private TransportStreamTimestamp GetMpegTimestamp ( string path )
{
var packetBuffer = new byte [ 'Å' ] ;
using ( var fs = _fileSystem . GetFileStream ( path , FileMode . Open , FileAccess . Read , FileShare . Read ) )
{
fs . Read ( packetBuffer , 0 , packetBuffer . Length ) ;
}
if ( packetBuffer [ 0 ] = = 71 )
{
2014-04-24 22:45:06 -04:00
return TransportStreamTimestamp . None ;
2014-04-24 22:00:19 -04:00
}
if ( ( packetBuffer [ 4 ] = = 71 ) & & ( packetBuffer [ 'Ä' ] = = 71 ) )
{
if ( ( packetBuffer [ 0 ] = = 0 ) & & ( packetBuffer [ 1 ] = = 0 ) & & ( packetBuffer [ 2 ] = = 0 ) & & ( packetBuffer [ 3 ] = = 0 ) )
{
2014-04-24 22:45:06 -04:00
return TransportStreamTimestamp . Zero ;
2014-04-24 22:00:19 -04:00
}
2014-04-24 22:45:06 -04:00
return TransportStreamTimestamp . Valid ;
2014-04-24 22:00:19 -04:00
}
2014-04-24 22:45:06 -04:00
return TransportStreamTimestamp . None ;
2014-04-24 22:00:19 -04:00
}
2014-02-04 15:19:29 -05:00
private void FetchFromDvdLib ( Video item , IIsoMount mount )
{
var path = mount = = null ? item . Path : mount . MountedPath ;
var dvd = new Dvd ( path ) ;
2014-05-06 22:28:19 -04:00
2014-02-04 15:19:29 -05:00
var primaryTitle = dvd . Titles . OrderByDescending ( GetRuntime ) . FirstOrDefault ( ) ;
2014-02-22 17:40:44 -05:00
byte? titleNumber = null ;
2014-02-04 15:19:29 -05:00
if ( primaryTitle ! = null )
{
2014-02-22 17:40:44 -05:00
titleNumber = primaryTitle . VideoTitleSetNumber ;
2014-02-22 15:20:22 -05:00
item . RunTimeTicks = GetRuntime ( primaryTitle ) ;
2014-02-04 15:19:29 -05:00
}
item . PlayableStreamFileNames = GetPrimaryPlaylistVobFiles ( item , mount , titleNumber )
. Select ( Path . GetFileName )
. ToList ( ) ;
}
private long GetRuntime ( Title title )
{
return title . ProgramChains
. Select ( i = > ( TimeSpan ) i . PlaybackTime )
. Select ( i = > i . Ticks )
. Sum ( ) ;
}
/// <summary>
/// Mounts the iso if needed.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>IsoMount.</returns>
protected Task < IIsoMount > MountIsoIfNeeded ( Video item , CancellationToken cancellationToken )
{
if ( item . VideoType = = VideoType . Iso )
{
return _isoManager . Mount ( item . Path , cancellationToken ) ;
}
return Task . FromResult < IIsoMount > ( null ) ;
}
/// <summary>
/// Determines the type of the iso.
/// </summary>
/// <param name="isoMount">The iso mount.</param>
/// <returns>System.Nullable{IsoType}.</returns>
private IsoType ? DetermineIsoType ( IIsoMount isoMount )
{
2014-12-08 23:57:18 -05:00
var fileSystemEntries = Directory . EnumerateFileSystemEntries ( isoMount . MountedPath ) . Select ( Path . GetFileName ) . ToList ( ) ;
2014-02-04 15:19:29 -05:00
2014-12-08 23:57:18 -05:00
if ( fileSystemEntries . Contains ( "video_ts" , StringComparer . OrdinalIgnoreCase ) | |
fileSystemEntries . Contains ( "VIDEO_TS.IFO" , StringComparer . OrdinalIgnoreCase ) )
2014-02-04 15:19:29 -05:00
{
return IsoType . Dvd ;
}
2014-12-08 23:57:18 -05:00
if ( fileSystemEntries . Contains ( "bdmv" , StringComparer . OrdinalIgnoreCase ) )
2014-02-04 15:19:29 -05:00
{
return IsoType . BluRay ;
}
return null ;
}
private IEnumerable < string > GetPrimaryPlaylistVobFiles ( Video video , IIsoMount isoMount , uint? titleNumber )
{
// min size 300 mb
const long minPlayableSize = 314572800 ;
var root = isoMount ! = null ? isoMount . MountedPath : video . Path ;
// Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
// Once we reach a file that is at least the minimum, return all subsequent ones
2014-02-23 22:27:13 -05:00
var allVobs = new DirectoryInfo ( root ) . EnumerateFiles ( "*" , SearchOption . AllDirectories )
. Where ( file = > string . Equals ( file . Extension , ".vob" , StringComparison . OrdinalIgnoreCase ) )
. OrderBy ( i = > i . FullName )
2014-02-04 15:19:29 -05:00
. ToList ( ) ;
// If we didn't find any satisfying the min length, just take them all
if ( allVobs . Count = = 0 )
{
_logger . Error ( "No vobs found in dvd structure." ) ;
return new List < string > ( ) ;
}
if ( titleNumber . HasValue )
{
var prefix = string . Format ( "VTS_0{0}_" , titleNumber . Value . ToString ( _usCulture ) ) ;
2014-02-23 22:27:13 -05:00
var vobs = allVobs . Where ( i = > i . Name . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) ) . ToList ( ) ;
2014-02-04 15:19:29 -05:00
if ( vobs . Count > 0 )
{
2014-02-23 22:48:27 -05:00
var minSizeVobs = vobs
2014-02-23 22:27:13 -05:00
. SkipWhile ( f = > f . Length < minPlayableSize )
. ToList ( ) ;
return minSizeVobs . Count = = 0 ? vobs . Select ( i = > i . FullName ) : minSizeVobs . Select ( i = > i . FullName ) ;
2014-02-04 15:19:29 -05:00
}
_logger . Debug ( "Could not determine vob file list for {0} using DvdLib. Will scan using file sizes." , video . Path ) ;
}
var files = allVobs
2014-02-23 22:27:13 -05:00
. SkipWhile ( f = > f . Length < minPlayableSize )
2014-02-04 15:19:29 -05:00
. ToList ( ) ;
// If we didn't find any satisfying the min length, just take them all
if ( files . Count = = 0 )
{
_logger . Warn ( "Vob size filter resulted in zero matches. Taking all vobs." ) ;
files = allVobs ;
}
// Assuming they're named "vts_05_01", take all files whose second part matches that of the first file
if ( files . Count > 0 )
{
2014-07-26 13:30:15 -04:00
var parts = _fileSystem . GetFileNameWithoutExtension ( files [ 0 ] ) . Split ( '_' ) ;
2014-02-04 15:19:29 -05:00
if ( parts . Length = = 3 )
{
var title = parts [ 1 ] ;
files = files . TakeWhile ( f = >
{
2014-07-26 13:30:15 -04:00
var fileParts = _fileSystem . GetFileNameWithoutExtension ( f ) . Split ( '_' ) ;
2014-02-04 15:19:29 -05:00
return fileParts . Length = = 3 & & string . Equals ( title , fileParts [ 1 ] , StringComparison . OrdinalIgnoreCase ) ;
} ) . ToList ( ) ;
// If this resulted in not getting any vobs, just take them all
if ( files . Count = = 0 )
{
_logger . Warn ( "Vob filename filter resulted in zero matches. Taking all vobs." ) ;
files = allVobs ;
}
}
}
2014-02-23 22:27:13 -05:00
return files . Select ( i = > i . FullName ) ;
2014-02-04 15:19:29 -05:00
}
}
}