2014-02-09 21:11:11 +00:00
using MediaBrowser.Common.Configuration ;
2015-01-17 20:12:02 +00:00
using MediaBrowser.Model.Extensions ;
2013-02-21 01:33:05 +00:00
using MediaBrowser.Controller.Entities ;
using MediaBrowser.Controller.Entities.Audio ;
2014-02-06 04:39:16 +00:00
using MediaBrowser.Controller.Library ;
2014-02-20 16:37:41 +00:00
using MediaBrowser.Controller.MediaEncoding ;
2013-12-06 03:39:44 +00:00
using MediaBrowser.Controller.Persistence ;
2013-02-21 01:33:05 +00:00
using MediaBrowser.Model.Entities ;
2014-06-17 01:56:23 +00:00
using MediaBrowser.Model.MediaInfo ;
2014-02-09 21:11:11 +00:00
using MediaBrowser.Model.Serialization ;
2013-02-21 01:33:05 +00:00
using System ;
using System.Collections.Generic ;
2014-02-06 04:39:16 +00:00
using System.Globalization ;
2014-02-09 21:11:11 +00:00
using System.IO ;
2013-02-21 01:33:05 +00:00
using System.Linq ;
using System.Threading ;
2013-08-15 15:25:51 +00:00
using System.Threading.Tasks ;
2013-02-21 01:33:05 +00:00
2013-06-09 16:47:28 +00:00
namespace MediaBrowser.Providers.MediaInfo
2013-02-21 01:33:05 +00:00
{
2014-02-06 04:39:16 +00:00
class FFProbeAudioInfo
2013-02-21 01:33:05 +00:00
{
2014-02-06 04:39:16 +00:00
private readonly IMediaEncoder _mediaEncoder ;
2013-12-06 03:39:44 +00:00
private readonly IItemRepository _itemRepo ;
2014-02-09 21:11:11 +00:00
private readonly IApplicationPaths _appPaths ;
private readonly IJsonSerializer _json ;
2013-12-06 03:39:44 +00:00
2014-02-06 04:39:16 +00:00
private readonly CultureInfo _usCulture = new CultureInfo ( "en-US" ) ;
2014-02-09 04:12:04 +00:00
2014-02-09 21:11:11 +00:00
public FFProbeAudioInfo ( IMediaEncoder mediaEncoder , IItemRepository itemRepo , IApplicationPaths appPaths , IJsonSerializer json )
2013-03-02 17:59:15 +00:00
{
2014-02-06 04:39:16 +00:00
_mediaEncoder = mediaEncoder ;
2013-12-06 03:39:44 +00:00
_itemRepo = itemRepo ;
2014-02-09 21:11:11 +00:00
_appPaths = appPaths ;
_json = json ;
2013-03-02 17:59:15 +00:00
}
2014-02-06 04:39:16 +00:00
public async Task < ItemUpdateType > Probe < T > ( T item , CancellationToken cancellationToken )
where T : Audio
2013-06-18 19:16:27 +00:00
{
2014-12-29 20:18:48 +00:00
if ( item . IsArchive )
{
var ext = Path . GetExtension ( item . Path ) ? ? string . Empty ;
item . Container = ext . TrimStart ( '.' ) ;
return ItemUpdateType . MetadataImport ;
}
2014-02-06 04:39:16 +00:00
var result = await GetMediaInfo ( item , cancellationToken ) . ConfigureAwait ( false ) ;
2013-06-18 19:16:27 +00:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-02-04 20:19:29 +00:00
FFProbeHelpers . NormalizeFFProbeResult ( result ) ;
2013-06-18 19:16:27 +00:00
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-02-06 04:39:16 +00:00
await Fetch ( item , cancellationToken , result ) . ConfigureAwait ( false ) ;
2013-06-18 19:16:27 +00:00
2014-02-06 04:39:16 +00:00
return ItemUpdateType . MetadataImport ;
}
2013-06-18 19:16:27 +00:00
2014-03-26 21:05:31 +00:00
private const string SchemaVersion = "1" ;
2014-02-06 04:39:16 +00:00
private async Task < InternalMediaInfoResult > GetMediaInfo ( BaseItem item , CancellationToken cancellationToken )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
2014-02-09 21:11:11 +00:00
var idString = item . Id . ToString ( "N" ) ;
2014-12-29 20:18:48 +00:00
var cachePath = Path . Combine ( _appPaths . CachePath ,
"ffprobe-audio" ,
2014-03-26 21:05:31 +00:00
idString . Substring ( 0 , 2 ) , idString , "v" + SchemaVersion + _mediaEncoder . Version + item . DateModified . Ticks . ToString ( _usCulture ) + ".json" ) ;
2014-02-09 21:11:11 +00:00
try
{
return _json . DeserializeFromFile < InternalMediaInfoResult > ( cachePath ) ;
}
catch ( FileNotFoundException )
{
}
catch ( DirectoryNotFoundException )
{
}
2014-02-06 04:39:16 +00:00
var inputPath = new [ ] { item . Path } ;
2014-06-17 01:56:23 +00:00
var result = await _mediaEncoder . GetMediaInfo ( inputPath , MediaProtocol . File , false , cancellationToken ) . ConfigureAwait ( false ) ;
2014-02-09 21:11:11 +00:00
Directory . CreateDirectory ( Path . GetDirectoryName ( cachePath ) ) ;
_json . SerializeToFile ( result , cachePath ) ;
return result ;
2013-06-18 19:16:27 +00:00
}
2013-02-21 01:33:05 +00:00
/// <summary>
/// Fetches the specified audio.
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="data">The data.</param>
/// <returns>Task.</returns>
2014-01-12 06:31:21 +00:00
protected Task Fetch ( Audio audio , CancellationToken cancellationToken , InternalMediaInfoResult data )
2013-02-21 01:33:05 +00:00
{
2014-04-22 17:25:54 +00:00
var mediaInfo = MediaEncoderHelpers . GetMediaInfo ( data ) ;
var mediaStreams = mediaInfo . MediaStreams ;
2013-02-21 01:33:05 +00:00
2014-04-22 17:25:54 +00:00
audio . FormatName = mediaInfo . Format ;
audio . TotalBitrate = mediaInfo . TotalBitrate ;
2014-06-30 17:40:46 +00:00
audio . HasEmbeddedImage = mediaStreams . Any ( i = > i . Type = = MediaStreamType . EmbeddedImage ) ;
2013-12-06 03:39:44 +00:00
2014-01-11 23:07:56 +00:00
if ( data . streams ! = null )
2013-04-06 01:03:38 +00:00
{
2014-01-11 23:07:56 +00:00
// Get the first audio stream
var stream = data . streams . FirstOrDefault ( s = > string . Equals ( s . codec_type , "audio" , StringComparison . OrdinalIgnoreCase ) ) ;
2013-05-15 04:05:52 +00:00
2014-01-11 23:07:56 +00:00
if ( stream ! = null )
2013-05-15 04:05:52 +00:00
{
2014-01-11 23:07:56 +00:00
// Get duration from stream properties
var duration = stream . duration ;
// If it's not there go into format properties
if ( string . IsNullOrEmpty ( duration ) )
{
duration = data . format . duration ;
}
// If we got something, parse it
if ( ! string . IsNullOrEmpty ( duration ) )
{
2014-02-06 04:39:16 +00:00
audio . RunTimeTicks = TimeSpan . FromSeconds ( double . Parse ( duration , _usCulture ) ) . Ticks ;
2014-01-11 23:07:56 +00:00
}
2013-05-15 04:05:52 +00:00
}
2013-04-06 01:03:38 +00:00
}
2013-02-21 01:33:05 +00:00
2014-04-18 05:03:01 +00:00
if ( data . format ! = null )
2013-04-06 01:03:38 +00:00
{
2014-04-18 05:03:01 +00:00
var extension = ( Path . GetExtension ( audio . Path ) ? ? string . Empty ) . TrimStart ( '.' ) ;
audio . Container = extension ;
if ( ! string . IsNullOrEmpty ( data . format . size ) )
{
2014-12-29 20:18:48 +00:00
audio . Size = long . Parse ( data . format . size , _usCulture ) ;
2014-04-18 05:03:01 +00:00
}
else
{
audio . Size = null ;
}
if ( data . format . tags ! = null )
{
FetchDataFromTags ( audio , data . format . tags ) ;
}
2013-04-06 01:03:38 +00:00
}
2013-12-06 03:39:44 +00:00
return _itemRepo . SaveMediaStreams ( audio . Id , mediaStreams , cancellationToken ) ;
2013-02-21 01:33:05 +00:00
}
/// <summary>
/// Fetches data from the tags dictionary
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
private void FetchDataFromTags ( Audio audio , Dictionary < string , string > tags )
{
2014-02-04 20:19:29 +00:00
var title = FFProbeHelpers . GetDictionaryValue ( tags , "title" ) ;
2013-02-21 01:33:05 +00:00
// Only set Name if title was found in the dictionary
if ( ! string . IsNullOrEmpty ( title ) )
{
audio . Name = title ;
}
2013-08-04 00:59:23 +00:00
if ( ! audio . LockedFields . Contains ( MetadataFields . Cast ) )
2013-02-21 01:33:05 +00:00
{
2013-08-29 21:00:27 +00:00
audio . People . Clear ( ) ;
2014-02-04 20:19:29 +00:00
var composer = FFProbeHelpers . GetDictionaryValue ( tags , "composer" ) ;
2013-02-21 01:33:05 +00:00
2013-08-04 00:59:23 +00:00
if ( ! string . IsNullOrWhiteSpace ( composer ) )
{
2014-03-25 21:13:55 +00:00
foreach ( var person in Split ( composer , false ) )
2013-02-21 01:33:05 +00:00
{
2013-09-05 22:59:07 +00:00
audio . AddPerson ( new PersonInfo { Name = person , Type = PersonType . Composer } ) ;
2013-02-21 01:33:05 +00:00
}
}
}
2014-02-04 20:19:29 +00:00
audio . Album = FFProbeHelpers . GetDictionaryValue ( tags , "album" ) ;
2013-02-21 01:33:05 +00:00
2015-01-27 22:45:59 +00:00
var artists = FFProbeHelpers . GetDictionaryValue ( tags , "artists" ) ;
2013-09-05 19:00:50 +00:00
2015-01-27 22:45:59 +00:00
if ( ! string . IsNullOrWhiteSpace ( artists ) )
2013-09-05 19:00:50 +00:00
{
2015-01-27 22:45:59 +00:00
audio . Artists = artists . Split ( new [ ] { '/' } , StringSplitOptions . RemoveEmptyEntries )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToList ( ) ;
2013-09-05 19:00:50 +00:00
}
else
{
2015-01-27 22:45:59 +00:00
var artist = FFProbeHelpers . GetDictionaryValue ( tags , "artist" ) ;
if ( string . IsNullOrWhiteSpace ( artist ) )
{
audio . Artists . Clear ( ) ;
}
else
{
audio . Artists = SplitArtists ( artist )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToList ( ) ;
}
2013-09-05 19:00:50 +00:00
}
2013-02-21 01:33:05 +00:00
2014-06-23 16:05:19 +00:00
var albumArtist = FFProbeHelpers . GetDictionaryValue ( tags , "albumartist" ) ? ? FFProbeHelpers . GetDictionaryValue ( tags , "album artist" ) ? ? FFProbeHelpers . GetDictionaryValue ( tags , "album_artist" ) ;
if ( string . IsNullOrWhiteSpace ( albumArtist ) )
{
audio . AlbumArtists = new List < string > ( ) ;
}
else
{
audio . AlbumArtists = SplitArtists ( albumArtist )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToList ( ) ;
}
2013-02-21 01:33:05 +00:00
// Track number
2013-09-08 16:49:05 +00:00
audio . IndexNumber = GetDictionaryDiscValue ( tags , "track" ) ;
2013-02-21 01:33:05 +00:00
// Disc number
2013-09-08 16:49:05 +00:00
audio . ParentIndexNumber = GetDictionaryDiscValue ( tags , "disc" ) ;
2013-02-21 01:33:05 +00:00
2014-02-04 20:19:29 +00:00
audio . ProductionYear = FFProbeHelpers . GetDictionaryNumericValue ( tags , "date" ) ;
2013-02-21 01:33:05 +00:00
// Several different forms of retaildate
2014-12-29 20:18:48 +00:00
audio . PremiereDate = FFProbeHelpers . GetDictionaryDateTime ( tags , "retaildate" ) ? ?
FFProbeHelpers . GetDictionaryDateTime ( tags , "retail date" ) ? ?
FFProbeHelpers . GetDictionaryDateTime ( tags , "retail_date" ) ? ?
2014-02-22 20:20:22 +00:00
FFProbeHelpers . GetDictionaryDateTime ( tags , "date" ) ;
2013-02-21 01:33:05 +00:00
// If we don't have a ProductionYear try and get it from PremiereDate
if ( audio . PremiereDate . HasValue & & ! audio . ProductionYear . HasValue )
{
2013-04-12 18:22:40 +00:00
audio . ProductionYear = audio . PremiereDate . Value . ToLocalTime ( ) . Year ;
2013-02-21 01:33:05 +00:00
}
2013-08-04 00:59:23 +00:00
if ( ! audio . LockedFields . Contains ( MetadataFields . Genres ) )
{
FetchGenres ( audio , tags ) ;
}
2013-02-21 01:33:05 +00:00
2013-08-04 00:59:23 +00:00
if ( ! audio . LockedFields . Contains ( MetadataFields . Studios ) )
{
2013-09-05 19:00:50 +00:00
audio . Studios . Clear ( ) ;
2013-08-04 00:59:23 +00:00
// There's several values in tags may or may not be present
FetchStudios ( audio , tags , "organization" ) ;
FetchStudios ( audio , tags , "ensemble" ) ;
FetchStudios ( audio , tags , "publisher" ) ;
}
2014-02-07 22:40:03 +00:00
2015-01-27 22:45:59 +00:00
// These support mulitple values, but for now we only store the first.
audio . SetProviderId ( MetadataProviders . MusicBrainzAlbumArtist , GetMultipleMusicBrainzId ( FFProbeHelpers . GetDictionaryValue ( tags , "MusicBrainz Album Artist Id" ) ) ) ;
audio . SetProviderId ( MetadataProviders . MusicBrainzArtist , GetMultipleMusicBrainzId ( FFProbeHelpers . GetDictionaryValue ( tags , "MusicBrainz Artist Id" ) ) ) ;
2015-02-07 05:28:38 +00:00
audio . SetProviderId ( MetadataProviders . MusicBrainzAlbum , GetMultipleMusicBrainzId ( FFProbeHelpers . GetDictionaryValue ( tags , "MusicBrainz Album Id" ) ) ) ;
audio . SetProviderId ( MetadataProviders . MusicBrainzReleaseGroup , GetMultipleMusicBrainzId ( FFProbeHelpers . GetDictionaryValue ( tags , "MusicBrainz Release Group Id" ) ) ) ;
audio . SetProviderId ( MetadataProviders . MusicBrainzTrack , GetMultipleMusicBrainzId ( FFProbeHelpers . GetDictionaryValue ( tags , "MusicBrainz Release Track Id" ) ) ) ;
2015-01-27 22:45:59 +00:00
}
private string GetMultipleMusicBrainzId ( string value )
{
if ( string . IsNullOrWhiteSpace ( value ) )
{
return null ;
}
2015-02-07 05:28:38 +00:00
return value . Split ( new [ ] { '/' } , StringSplitOptions . RemoveEmptyEntries )
. Select ( i = > i . Trim ( ) )
. Where ( i = > ! string . IsNullOrWhiteSpace ( i ) )
. FirstOrDefault ( ) ;
2013-02-21 01:33:05 +00:00
}
2014-02-09 04:12:04 +00:00
private readonly char [ ] _nameDelimiters = { '/' , '|' , ';' , '\\' } ;
2013-09-05 19:00:50 +00:00
2013-04-21 01:02:16 +00:00
/// <summary>
/// Splits the specified val.
/// </summary>
/// <param name="val">The val.</param>
2014-03-25 21:13:55 +00:00
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
2013-04-21 01:02:16 +00:00
/// <returns>System.String[][].</returns>
2014-03-25 21:13:55 +00:00
private IEnumerable < string > Split ( string val , bool allowCommaDelimiter )
2013-04-21 01:02:16 +00:00
{
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
2014-12-29 20:18:48 +00:00
var delimeter = ! allowCommaDelimiter | | _nameDelimiters . Any ( i = > val . IndexOf ( i ) ! = - 1 ) ?
_nameDelimiters :
2014-03-25 21:13:55 +00:00
new [ ] { ',' } ;
2013-09-17 14:32:31 +00:00
return val . Split ( delimeter , StringSplitOptions . RemoveEmptyEntries )
. Where ( i = > ! string . IsNullOrWhiteSpace ( i ) )
. Select ( i = > i . Trim ( ) ) ;
}
private const string ArtistReplaceValue = " | " ;
private IEnumerable < string > SplitArtists ( string val )
{
val = val . Replace ( " featuring " , ArtistReplaceValue , StringComparison . OrdinalIgnoreCase )
. Replace ( " feat. " , ArtistReplaceValue , StringComparison . OrdinalIgnoreCase ) ;
2014-02-09 04:12:04 +00:00
var artistsFound = new List < string > ( ) ;
foreach ( var whitelistArtist in GetSplitWhitelist ( ) )
{
2014-02-09 04:52:52 +00:00
var originalVal = val ;
val = val . Replace ( whitelistArtist , "|" , StringComparison . OrdinalIgnoreCase ) ;
2014-02-09 04:12:04 +00:00
2014-02-09 04:52:52 +00:00
if ( ! string . Equals ( originalVal , val , StringComparison . OrdinalIgnoreCase ) )
{
2014-02-09 04:12:04 +00:00
artistsFound . Add ( whitelistArtist ) ;
}
}
2013-09-17 14:32:31 +00:00
// Only use the comma as a delimeter if there are no slashes or pipes.
// We want to be careful not to split names that have commas in them
var delimeter = _nameDelimiters ;
2013-04-21 01:02:16 +00:00
2014-02-09 04:12:04 +00:00
var artists = val . Split ( delimeter , StringSplitOptions . RemoveEmptyEntries )
2013-09-05 22:59:07 +00:00
. Where ( i = > ! string . IsNullOrWhiteSpace ( i ) )
. Select ( i = > i . Trim ( ) ) ;
2014-02-09 04:12:04 +00:00
artistsFound . AddRange ( artists ) ;
return artistsFound ;
}
private List < string > _splitWhiteList = null ;
private IEnumerable < string > GetSplitWhitelist ( )
{
if ( _splitWhiteList = = null )
{
var file = GetType ( ) . Namespace + ".whitelist.txt" ;
using ( var stream = GetType ( ) . Assembly . GetManifestResourceStream ( file ) )
{
using ( var reader = new StreamReader ( stream ) )
{
var list = new List < string > ( ) ;
while ( ! reader . EndOfStream )
{
var val = reader . ReadLine ( ) ;
if ( ! string . IsNullOrWhiteSpace ( val ) )
{
list . Add ( val ) ;
}
}
_splitWhiteList = list ;
}
}
}
return _splitWhiteList ;
2013-04-21 01:02:16 +00:00
}
2013-02-21 01:33:05 +00:00
/// <summary>
/// Gets the studios from the tags collection
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
/// <param name="tagName">Name of the tag.</param>
private void FetchStudios ( Audio audio , Dictionary < string , string > tags , string tagName )
{
2014-02-04 20:19:29 +00:00
var val = FFProbeHelpers . GetDictionaryValue ( tags , tagName ) ;
2013-02-21 01:33:05 +00:00
if ( ! string . IsNullOrEmpty ( val ) )
{
2013-06-11 16:10:41 +00:00
// Sometimes the artist name is listed here, account for that
2014-03-25 21:13:55 +00:00
var studios = Split ( val , true ) . Where ( i = > ! audio . HasArtist ( i ) ) ;
2013-04-28 05:44:45 +00:00
foreach ( var studio in studios )
{
2013-09-05 22:59:07 +00:00
audio . AddStudio ( studio ) ;
2013-04-28 05:44:45 +00:00
}
2013-02-21 01:33:05 +00:00
}
}
/// <summary>
/// Gets the genres from the tags collection
/// </summary>
/// <param name="audio">The audio.</param>
/// <param name="tags">The tags.</param>
private void FetchGenres ( Audio audio , Dictionary < string , string > tags )
{
2014-02-04 20:19:29 +00:00
var val = FFProbeHelpers . GetDictionaryValue ( tags , "genre" ) ;
2013-02-21 01:33:05 +00:00
if ( ! string . IsNullOrEmpty ( val ) )
{
2013-04-28 05:44:45 +00:00
audio . Genres . Clear ( ) ;
2014-03-25 21:13:55 +00:00
foreach ( var genre in Split ( val , true ) )
2013-04-28 05:44:45 +00:00
{
2013-09-05 22:59:07 +00:00
audio . AddGenre ( genre ) ;
2013-04-28 05:44:45 +00:00
}
2013-02-21 01:33:05 +00:00
}
}
/// <summary>
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
/// </summary>
/// <param name="tags">The tags.</param>
2013-09-08 16:49:05 +00:00
/// <param name="tagName">Name of the tag.</param>
2013-02-21 01:33:05 +00:00
/// <returns>System.Nullable{System.Int32}.</returns>
2013-09-08 16:49:05 +00:00
private int? GetDictionaryDiscValue ( Dictionary < string , string > tags , string tagName )
2013-02-21 01:33:05 +00:00
{
2014-02-04 20:19:29 +00:00
var disc = FFProbeHelpers . GetDictionaryValue ( tags , tagName ) ;
2013-02-21 01:33:05 +00:00
if ( ! string . IsNullOrEmpty ( disc ) )
{
disc = disc . Split ( '/' ) [ 0 ] ;
int num ;
if ( int . TryParse ( disc , out num ) )
{
return num ;
}
}
return null ;
}
2014-02-06 04:39:16 +00:00
}
2013-02-21 01:33:05 +00:00
}