convert episode providers to new system

pull/702/head
Luke Pulverenti 11 years ago
parent 351cfef7a7
commit 04d62d3420

@ -11,13 +11,16 @@ using System.Threading.Tasks;
namespace MediaBrowser.Api namespace MediaBrowser.Api
{ {
[Route("/Items/{Id}/Refresh", "POST")] public class BaseRefreshRequest : IReturnVoid
[Api(Description = "Refreshes metadata for an item")]
public class RefreshItem : IReturnVoid
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] [ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; } public bool Forced { get; set; }
}
[Route("/Items/{Id}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for an item")]
public class RefreshItem : BaseRefreshRequest
{
[ApiMember(Name = "Recursive", Description = "Indicates if the refresh should occur recursively.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")] [ApiMember(Name = "Recursive", Description = "Indicates if the refresh should occur recursively.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Recursive { get; set; } public bool Recursive { get; set; }
@ -27,66 +30,48 @@ namespace MediaBrowser.Api
[Route("/Artists/{Name}/Refresh", "POST")] [Route("/Artists/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for an artist")] [Api(Description = "Refreshes metadata for an artist")]
public class RefreshArtist : IReturnVoid public class RefreshArtist : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
[Route("/Genres/{Name}/Refresh", "POST")] [Route("/Genres/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for a genre")] [Api(Description = "Refreshes metadata for a genre")]
public class RefreshGenre : IReturnVoid public class RefreshGenre : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
[Route("/MusicGenres/{Name}/Refresh", "POST")] [Route("/MusicGenres/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for a music genre")] [Api(Description = "Refreshes metadata for a music genre")]
public class RefreshMusicGenre : IReturnVoid public class RefreshMusicGenre : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
[Route("/GameGenres/{Name}/Refresh", "POST")] [Route("/GameGenres/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for a game genre")] [Api(Description = "Refreshes metadata for a game genre")]
public class RefreshGameGenre : IReturnVoid public class RefreshGameGenre : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
[Route("/Persons/{Name}/Refresh", "POST")] [Route("/Persons/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for a person")] [Api(Description = "Refreshes metadata for a person")]
public class RefreshPerson : IReturnVoid public class RefreshPerson : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
[Route("/Studios/{Name}/Refresh", "POST")] [Route("/Studios/{Name}/Refresh", "POST")]
[Api(Description = "Refreshes metadata for a studio")] [Api(Description = "Refreshes metadata for a studio")]
public class RefreshStudio : IReturnVoid public class RefreshStudio : BaseRefreshRequest
{ {
[ApiMember(Name = "Forced", Description = "Indicates if a normal or forced refresh should occur.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "POST")]
public bool Forced { get; set; }
[ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public string Name { get; set; } public string Name { get; set; }
} }
@ -132,11 +117,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -157,11 +138,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -182,11 +159,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -207,11 +180,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -232,11 +201,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -257,11 +222,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -291,11 +252,7 @@ namespace MediaBrowser.Api
try try
{ {
await item.RefreshMetadata(new MetadataRefreshOptions await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
if (item.IsFolder) if (item.IsFolder)
{ {
@ -328,13 +285,11 @@ namespace MediaBrowser.Api
/// <returns>Task.</returns> /// <returns>Task.</returns>
private async Task RefreshCollectionFolderChildren(RefreshItem request, CollectionFolder collectionFolder) private async Task RefreshCollectionFolderChildren(RefreshItem request, CollectionFolder collectionFolder)
{ {
var options = GetRefreshOptions(request);
foreach (var child in collectionFolder.Children.ToList()) foreach (var child in collectionFolder.Children.ToList())
{ {
await child.RefreshMetadata(new MetadataRefreshOptions await child.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
{
ReplaceAllMetadata = request.Forced,
}, CancellationToken.None).ConfigureAwait(false);
if (child.IsFolder) if (child.IsFolder)
{ {
@ -344,5 +299,15 @@ namespace MediaBrowser.Api
} }
} }
} }
private MetadataRefreshOptions GetRefreshOptions(BaseRefreshRequest request)
{
return new MetadataRefreshOptions
{
MetadataRefreshMode = request.Forced ? MetadataRefreshMode.FullRefresh : MetadataRefreshMode.EnsureMetadata,
ImageRefreshMode = request.Forced ? ImageRefreshMode.FullRefresh : ImageRefreshMode.Default,
ReplaceAllMetadata = request.Forced
};
}
} }
} }

@ -1,8 +1,8 @@
using System; using MediaBrowser.Model.Configuration;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Entities.TV namespace MediaBrowser.Controller.Entities.TV
{ {
@ -57,6 +57,12 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary> /// </summary>
/// <value>The absolute episode number.</value> /// <value>The absolute episode number.</value>
public int? AbsoluteEpisodeNumber { get; set; } public int? AbsoluteEpisodeNumber { get; set; }
/// <summary>
/// This is the ending episode number for double episodes.
/// </summary>
/// <value>The index number.</value>
public int? IndexNumberEnd { get; set; }
/// <summary> /// <summary>
/// We want to group into series not show individually in an index /// We want to group into series not show individually in an index
@ -89,7 +95,7 @@ namespace MediaBrowser.Controller.Entities.TV
return value; return value;
} }
var season = Parent as Season; var season = Season;
return season != null ? season.IndexNumber : null; return season != null ? season.IndexNumber : null;
} }
@ -140,10 +146,6 @@ namespace MediaBrowser.Controller.Entities.TV
get { return Series != null ? Series.CustomRatingForComparison : base.CustomRatingForComparison; } get { return Series != null ? Series.CustomRatingForComparison : base.CustomRatingForComparison; }
} }
/// <summary>
/// The _series
/// </summary>
private Series _series;
/// <summary> /// <summary>
/// This Episode's Series Instance /// This Episode's Series Instance
/// </summary> /// </summary>
@ -151,14 +153,14 @@ namespace MediaBrowser.Controller.Entities.TV
[IgnoreDataMember] [IgnoreDataMember]
public Series Series public Series Series
{ {
get { return _series ?? (_series = FindParent<Series>()); } get { return FindParent<Series>(); }
} }
/// <summary> [IgnoreDataMember]
/// This is the ending episode number for double episodes. public Season Season
/// </summary> {
/// <value>The index number.</value> get { return FindParent<Season>(); }
public int? IndexNumberEnd { get; set; } }
/// <summary> /// <summary>
/// Creates the name of the sort. /// Creates the name of the sort.
@ -217,7 +219,7 @@ namespace MediaBrowser.Controller.Entities.TV
get get
{ {
// First see if the parent is a Season // First see if the parent is a Season
var season = Parent as Season; var season = Season;
if (season != null) if (season != null)
{ {
@ -229,7 +231,7 @@ namespace MediaBrowser.Controller.Entities.TV
// Parent is a Series // Parent is a Series
if (seasonNumber.HasValue) if (seasonNumber.HasValue)
{ {
var series = Parent as Series; var series = Series;
if (series != null) if (series != null)
{ {

@ -144,7 +144,6 @@
<Compile Include="Persistence\IFileOrganizationRepository.cs" /> <Compile Include="Persistence\IFileOrganizationRepository.cs" />
<Compile Include="Persistence\MediaStreamQuery.cs" /> <Compile Include="Persistence\MediaStreamQuery.cs" />
<Compile Include="Providers\ICustomMetadataProvider.cs" /> <Compile Include="Providers\ICustomMetadataProvider.cs" />
<Compile Include="Providers\IDynamicInfoProvider.cs" />
<Compile Include="Providers\IHasChangeMonitor.cs" /> <Compile Include="Providers\IHasChangeMonitor.cs" />
<Compile Include="Providers\IHasMetadata.cs" /> <Compile Include="Providers\IHasMetadata.cs" />
<Compile Include="Providers\IImageProvider.cs" /> <Compile Include="Providers\IImageProvider.cs" />

@ -1,7 +1,8 @@
using System; using MediaBrowser.Model.Entities;
using System;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.MediaInfo namespace MediaBrowser.Controller.MediaInfo
{ {
@ -35,6 +36,18 @@ namespace MediaBrowser.Controller.MediaInfo
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task ExtractImage(string[] inputFiles, InputType type, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken); Task ExtractImage(string[] inputFiles, InputType type, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, string outputPath, CancellationToken cancellationToken);
/// <summary>
/// Extracts the image.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="type">The type.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="threedFormat">The threed format.</param>
/// <param name="offset">The offset.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{Stream}.</returns>
Task<Stream> ExtractImage(string[] inputFiles, InputType type, bool isAudio, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Extracts the text subtitle. /// Extracts the text subtitle.
/// </summary> /// </summary>

@ -1,4 +1,5 @@
using System.Threading; using MediaBrowser.Controller.Library;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MediaBrowser.Controller.Providers namespace MediaBrowser.Controller.Providers
@ -10,6 +11,6 @@ namespace MediaBrowser.Controller.Providers
public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider
where TItemType : IHasMetadata where TItemType : IHasMetadata
{ {
Task FetchAsync(TItemType item, CancellationToken cancellationToken); Task<ItemUpdateType> FetchAsync(TItemType item, CancellationToken cancellationToken);
} }
} }

@ -1,10 +0,0 @@

namespace MediaBrowser.Controller.Providers
{
/// <summary>
/// Marker interface for a provider that always runs
/// </summary>
public interface IDynamicInfoProvider
{
}
}

@ -16,6 +16,12 @@ namespace MediaBrowser.Controller.Providers
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
string GetPreferredMetadataCountryCode(); string GetPreferredMetadataCountryCode();
/// <summary>
/// Gets the date modified.
/// </summary>
/// <value>The date modified.</value>
DateTime DateModified { get; }
/// <summary> /// <summary>
/// Gets the locked fields. /// Gets the locked fields.
/// </summary> /// </summary>

@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Providers
public ImageType Type { get; set; } public ImageType Type { get; set; }
} }
public interface IDynamicImageProvider : ILocalImageProvider public interface IDynamicImageProvider : IImageProvider
{ {
/// <summary> /// <summary>
/// Gets the supported images. /// Gets the supported images.

@ -31,6 +31,8 @@ namespace MediaBrowser.Controller.Providers
/// </summary> /// </summary>
/// <value>The year.</value> /// <value>The year.</value>
public int? Year { get; set; } public int? Year { get; set; }
public int? IndexNumber { get; set; }
public int? ParentIndexNumber { get; set; }
public ItemId() public ItemId()
{ {
@ -45,11 +47,17 @@ namespace MediaBrowser.Controller.Providers
/// </summary> /// </summary>
/// <value>The album artist.</value> /// <value>The album artist.</value>
public string AlbumArtist { get; set; } public string AlbumArtist { get; set; }
/// <summary> /// <summary>
/// Gets or sets the artist music brainz identifier. /// Gets or sets the artist provider ids.
/// </summary> /// </summary>
/// <value>The artist music brainz identifier.</value> /// <value>The artist provider ids.</value>
public string ArtistMusicBrainzId { get; set; } public Dictionary<string, string> ArtistProviderIds { get; set; }
public AlbumId()
{
ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
} }
public class GameId : ItemId public class GameId : ItemId
@ -69,4 +77,16 @@ namespace MediaBrowser.Controller.Providers
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
} }
public class EpisodeId : ItemId
{
public Dictionary<string, string> SeriesProviderIds { get; set; }
public int? IndexNumberEnd { get; set; }
public EpisodeId()
{
SeriesProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
} }

@ -0,0 +1,43 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.AdultVideos
{
class AdultVideoMetadataService : MetadataService<AdultVideo, ItemId>
{
private readonly ILibraryManager _libraryManager;
public AdultVideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Merges the specified source.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="target">The target.</param>
/// <param name="lockedFields">The locked fields.</param>
/// <param name="replaceData">if set to <c>true</c> [replace data].</param>
/// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
protected override void MergeData(AdultVideo source, AdultVideo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
{
ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
}
protected override Task SaveItem(AdultVideo item, ItemUpdateType reason, CancellationToken cancellationToken)
{
return _libraryManager.UpdateItem(item, reason, cancellationToken);
}
}
}

@ -0,0 +1,59 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Movies;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.AdultVideos
{
class AdultVideoXmlProvider : BaseXmlProvider, ILocalMetadataProvider<AdultVideo>
{
private readonly ILogger _logger;
public AdultVideoXmlProvider(IFileSystem fileSystem, ILogger logger)
: base(fileSystem)
{
_logger = logger;
}
public async Task<MetadataResult<AdultVideo>> GetMetadata(string path, CancellationToken cancellationToken)
{
path = GetXmlFile(path).FullName;
var result = new MetadataResult<AdultVideo>();
await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
result.Item = new AdultVideo();
new MovieXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
result.HasMetadata = true;
}
catch (FileNotFoundException)
{
result.HasMetadata = false;
}
finally
{
XmlParsingResourcePool.Release();
}
return result;
}
public string Name
{
get { return "Media Browser Xml"; }
}
protected override FileInfo GetXmlFile(string path)
{
return MovieXmlProvider.GetXmlFileInfo(path, FileSystem);
}
}
}

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,6 +1,7 @@
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
@ -230,13 +231,23 @@ namespace MediaBrowser.Providers.Manager
protected virtual TIdType GetId(TItemType item) protected virtual TIdType GetId(TItemType item)
{ {
return new TIdType var id = new TIdType
{ {
MetadataCountryCode = item.GetPreferredMetadataCountryCode(), MetadataCountryCode = item.GetPreferredMetadataCountryCode(),
MetadataLanguage = item.GetPreferredMetadataLanguage(), MetadataLanguage = item.GetPreferredMetadataLanguage(),
Name = item.Name, Name = item.Name,
ProviderIds = item.ProviderIds ProviderIds = item.ProviderIds
}; };
var baseItem = item as BaseItem;
if (baseItem != null)
{
id.IndexNumber = baseItem.IndexNumber;
id.ParentIndexNumber = baseItem.ParentIndexNumber;
}
return id;
} }
public bool CanRefresh(IHasMetadata item) public bool CanRefresh(IHasMetadata item)
@ -253,6 +264,7 @@ namespace MediaBrowser.Providers.Manager
}; };
var temp = CreateNew(); var temp = CreateNew();
temp.Path = item.Path;
// If replacing all metadata, run internet providers first // If replacing all metadata, run internet providers first
if (options.ReplaceAllMetadata) if (options.ReplaceAllMetadata)
@ -313,29 +325,32 @@ namespace MediaBrowser.Providers.Manager
foreach (var provider in providers.OfType<ICustomMetadataProvider<TItemType>>()) foreach (var provider in providers.OfType<ICustomMetadataProvider<TItemType>>())
{ {
Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name); await RunCustomProvider(provider, item, refreshResult, cancellationToken).ConfigureAwait(false);
try
{
await provider.FetchAsync(item, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
refreshResult.Status = ProviderRefreshStatus.CompletedWithErrors;
refreshResult.ErrorMessage = ex.Message;
Logger.ErrorException("Error in {0}", ex, provider.Name);
}
} }
return refreshResult; return refreshResult;
} }
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, RefreshResult refreshResult, CancellationToken cancellationToken)
{
Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
try
{
refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
refreshResult.Status = ProviderRefreshStatus.CompletedWithErrors;
refreshResult.ErrorMessage = ex.Message;
Logger.ErrorException("Error in {0}", ex, provider.Name);
}
}
protected virtual TItemType CreateNew() protected virtual TItemType CreateNew()
{ {
return new TItemType(); return new TItemType();

@ -246,11 +246,7 @@ namespace MediaBrowser.Providers.Manager
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Don't clog up the log with these providers _logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
if (!(provider is IDynamicInfoProvider))
{
_logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
}
try try
{ {
@ -637,7 +633,7 @@ namespace MediaBrowser.Providers.Manager
})); }));
// Fetchers // Fetchers
list.AddRange(providers.Where(i => !(i is ILocalMetadataProvider)).Select(i => new MetadataPlugin list.AddRange(providers.Where(i => (i is IRemoteMetadataProvider)).Select(i => new MetadataPlugin
{ {
Name = i.Name, Name = i.Name,
Type = MetadataPluginType.MetadataFetcher Type = MetadataPluginType.MetadataFetcher

@ -64,6 +64,7 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AdultVideos\AdultVideoMetadataService.cs" />
<Compile Include="All\LocalImageProvider.cs" /> <Compile Include="All\LocalImageProvider.cs" />
<Compile Include="Books\BookMetadataService.cs" /> <Compile Include="Books\BookMetadataService.cs" />
<Compile Include="BoxSets\BoxSetMetadataService.cs" /> <Compile Include="BoxSets\BoxSetMetadataService.cs" />
@ -89,7 +90,11 @@
<Compile Include="Games\GameSystemXmlProvider.cs" /> <Compile Include="Games\GameSystemXmlProvider.cs" />
<Compile Include="ImageFromMediaLocationProvider.cs" /> <Compile Include="ImageFromMediaLocationProvider.cs" />
<Compile Include="ImagesByNameProvider.cs" /> <Compile Include="ImagesByNameProvider.cs" />
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
<Compile Include="MediaInfo\FFProbeProvider.cs" />
<Compile Include="MediaInfo\FFProbeVideoInfo.cs" />
<Compile Include="Movies\MovieDbSearch.cs" /> <Compile Include="Movies\MovieDbSearch.cs" />
<Compile Include="Movies\MovieXmlProvider.cs" />
<Compile Include="MusicGenres\MusicGenreImageProvider.cs" /> <Compile Include="MusicGenres\MusicGenreImageProvider.cs" />
<Compile Include="GameGenres\GameGenreImageProvider.cs" /> <Compile Include="GameGenres\GameGenreImageProvider.cs" />
<Compile Include="Genres\GenreImageProvider.cs" /> <Compile Include="Genres\GenreImageProvider.cs" />
@ -107,6 +112,8 @@
<Compile Include="Music\ArtistMetadataService.cs" /> <Compile Include="Music\ArtistMetadataService.cs" />
<Compile Include="Music\LastfmArtistProvider.cs" /> <Compile Include="Music\LastfmArtistProvider.cs" />
<Compile Include="Music\MusicBrainzArtistProvider.cs" /> <Compile Include="Music\MusicBrainzArtistProvider.cs" />
<Compile Include="Music\MusicVideoMetadataService.cs" />
<Compile Include="Music\MusicVideoXmlProvider.cs" />
<Compile Include="Omdb\OmdbProvider.cs" /> <Compile Include="Omdb\OmdbProvider.cs" />
<Compile Include="Omdb\OmdbSeriesProvider.cs" /> <Compile Include="Omdb\OmdbSeriesProvider.cs" />
<Compile Include="People\MovieDbPersonImageProvider.cs" /> <Compile Include="People\MovieDbPersonImageProvider.cs" />
@ -150,9 +157,9 @@
<Compile Include="Savers\XmlSaverHelpers.cs" /> <Compile Include="Savers\XmlSaverHelpers.cs" />
<Compile Include="Studios\StudiosImageProvider.cs" /> <Compile Include="Studios\StudiosImageProvider.cs" />
<Compile Include="Studios\StudioMetadataService.cs" /> <Compile Include="Studios\StudioMetadataService.cs" />
<Compile Include="TV\EpisodeImageFromMediaLocationProvider.cs" /> <Compile Include="TV\EpisodeLocalImageProvider.cs" />
<Compile Include="TV\EpisodeIndexNumberProvider.cs" /> <Compile Include="TV\EpisodeMetadataService.cs" />
<Compile Include="TV\EpisodeProviderFromXml.cs" /> <Compile Include="TV\EpisodeXmlProvider.cs" />
<Compile Include="TV\EpisodeXmlParser.cs" /> <Compile Include="TV\EpisodeXmlParser.cs" />
<Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" /> <Compile Include="TV\FanArtTvUpdatesPrescanTask.cs" />
<Compile Include="TV\FanartSeasonProvider.cs" /> <Compile Include="TV\FanartSeasonProvider.cs" />

@ -141,109 +141,5 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
} }
/// <summary>
/// Normalizes the FF probe result.
/// </summary>
/// <param name="result">The result.</param>
protected void NormalizeFFProbeResult(InternalMediaInfoResult result)
{
if (result.format != null && result.format.tags != null)
{
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
}
if (result.streams != null)
{
// Convert all dictionaries to case insensitive
foreach (var stream in result.streams)
{
if (stream.tags != null)
{
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
}
if (stream.disposition != null)
{
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
}
}
}
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
/// <summary>
/// Gets an int from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
int i;
if (int.TryParse(val, out i))
{
return i;
}
}
return null;
}
/// <summary>
/// Gets a DateTime from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{DateTime}.</returns>
protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
DateTime i;
if (DateTime.TryParse(val, out i))
{
return i.ToUniversalTime();
}
}
return null;
}
/// <summary>
/// Converts a dictionary to case insensitive
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
{
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
}
} }
} }

@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
NormalizeFFProbeResult(result); FFProbeHelpers.NormalizeFFProbeResult(result);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -102,7 +102,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tags">The tags.</param> /// <param name="tags">The tags.</param>
private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags) private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
{ {
var title = GetDictionaryValue(tags, "title"); var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
// Only set Name if title was found in the dictionary // Only set Name if title was found in the dictionary
if (!string.IsNullOrEmpty(title)) if (!string.IsNullOrEmpty(title))
@ -114,7 +114,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
audio.People.Clear(); audio.People.Clear();
var composer = GetDictionaryValue(tags, "composer"); var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
if (!string.IsNullOrWhiteSpace(composer)) if (!string.IsNullOrWhiteSpace(composer))
{ {
@ -125,9 +125,9 @@ namespace MediaBrowser.Providers.MediaInfo
} }
} }
audio.Album = GetDictionaryValue(tags, "album"); audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
var artist = GetDictionaryValue(tags, "artist"); var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
if (string.IsNullOrWhiteSpace(artist)) if (string.IsNullOrWhiteSpace(artist))
{ {
@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.MediaInfo
} }
// Several different forms of albumartist // Several different forms of albumartist
audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist"); audio.AlbumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album artist") ?? FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
// Track number // Track number
audio.IndexNumber = GetDictionaryDiscValue(tags, "track"); audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
@ -150,10 +150,10 @@ namespace MediaBrowser.Providers.MediaInfo
// Disc number // Disc number
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc"); audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
audio.ProductionYear = GetDictionaryNumericValue(tags, "date"); audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
// Several different forms of retaildate // Several different forms of retaildate
audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date"); audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ?? FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date");
// If we don't have a ProductionYear try and get it from PremiereDate // If we don't have a ProductionYear try and get it from PremiereDate
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue) if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
@ -219,7 +219,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tagName">Name of the tag.</param> /// <param name="tagName">Name of the tag.</param>
private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName) private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
{ {
var val = GetDictionaryValue(tags, tagName); var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(val)) if (!string.IsNullOrEmpty(val))
{ {
@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tags">The tags.</param> /// <param name="tags">The tags.</param>
private void FetchGenres(Audio audio, Dictionary<string, string> tags) private void FetchGenres(Audio audio, Dictionary<string, string> tags)
{ {
var val = GetDictionaryValue(tags, "genre"); var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
if (!string.IsNullOrEmpty(val)) if (!string.IsNullOrEmpty(val))
{ {
@ -261,7 +261,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// <returns>System.Nullable{System.Int32}.</returns> /// <returns>System.Nullable{System.Int32}.</returns>
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName) private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
{ {
var disc = GetDictionaryValue(tags, tagName); var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
if (!string.IsNullOrEmpty(disc)) if (!string.IsNullOrEmpty(disc))
{ {

@ -0,0 +1,113 @@
using MediaBrowser.Controller.MediaInfo;
using System;
using System.Collections.Generic;
namespace MediaBrowser.Providers.MediaInfo
{
public static class FFProbeHelpers
{
/// <summary>
/// Normalizes the FF probe result.
/// </summary>
/// <param name="result">The result.</param>
public static void NormalizeFFProbeResult(InternalMediaInfoResult result)
{
if (result.format != null && result.format.tags != null)
{
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
}
if (result.streams != null)
{
// Convert all dictionaries to case insensitive
foreach (var stream in result.streams)
{
if (stream.tags != null)
{
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
}
if (stream.disposition != null)
{
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
}
}
}
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
public static string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
/// <summary>
/// Gets an int from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{System.Int32}.</returns>
public static int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
int i;
if (int.TryParse(val, out i))
{
return i;
}
}
return null;
}
/// <summary>
/// Gets a DateTime from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.Nullable{DateTime}.</returns>
public static DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
{
var val = GetDictionaryValue(tags, key);
if (!string.IsNullOrEmpty(val))
{
DateTime i;
if (DateTime.TryParse(val, out i))
{
return i.ToUniversalTime();
}
}
return null;
}
/// <summary>
/// Converts a dictionary to case insensitive
/// </summary>
/// <param name="dict">The dict.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
{
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
}
}
}

@ -0,0 +1,103 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.MediaInfo;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.MediaInfo
{
public class FFProbeProvider : ICustomMetadataProvider<Episode>,
ICustomMetadataProvider<MusicVideo>,
ICustomMetadataProvider<Movie>,
ICustomMetadataProvider<AdultVideo>,
ICustomMetadataProvider<LiveTvVideoRecording>,
IHasChangeMonitor
{
private readonly ILogger _logger;
private readonly IIsoManager _isoManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly IBlurayExaminer _blurayExaminer;
private readonly ILocalizationManager _localization;
public string Name
{
get { return "ffprobe"; }
}
public Task<ItemUpdateType> FetchAsync(Episode item, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, cancellationToken);
}
public Task<ItemUpdateType> FetchAsync(MusicVideo item, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, cancellationToken);
}
public Task<ItemUpdateType> FetchAsync(Movie item, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, cancellationToken);
}
public Task<ItemUpdateType> FetchAsync(AdultVideo item, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, cancellationToken);
}
public Task<ItemUpdateType> FetchAsync(LiveTvVideoRecording item, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, cancellationToken);
}
public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization)
{
_logger = logger;
_isoManager = isoManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
_localization = localization;
}
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.Unspecified);
public Task<ItemUpdateType> FetchVideoInfo<T>(T item, CancellationToken cancellationToken)
where T : Video
{
if (item.LocationType != LocationType.FileSystem)
{
return _cachedTask;
}
if (item.VideoType == VideoType.Iso && !_isoManager.CanMount(item.Path))
{
return _cachedTask;
}
if (item.VideoType == VideoType.HdDvd)
{
return _cachedTask;
}
var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization);
return prober.ProbeVideo(item, cancellationToken);
}
public bool HasChanged(IHasMetadata item, DateTime date)
{
return item.DateModified > date;
}
}
}

@ -0,0 +1,587 @@
using DvdLib.Ifo;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Controller.MediaInfo;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
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;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization)
{
_logger = logger;
_isoManager = isoManager;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_blurayExaminer = blurayExaminer;
_localization = localization;
}
public async Task<ItemUpdateType> ProbeVideo<T>(T item, CancellationToken cancellationToken)
where T : Video
{
var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false);
try
{
OnPreFetch(item, isoMount);
// 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;
}
}
var result = await GetMediaInfo(item, isoMount, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
FFProbeHelpers.NormalizeFFProbeResult(result);
cancellationToken.ThrowIfCancellationRequested();
await Fetch(item, cancellationToken, result, isoMount).ConfigureAwait(false);
}
finally
{
if (isoMount != null)
{
isoMount.Dispose();
}
}
return ItemUpdateType.MetadataImport;
}
private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var type = InputType.File;
var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath };
var video = item as Video;
if (video != null)
{
inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type);
}
return await _mediaEncoder.GetMediaInfo(inputPath, type, false, cancellationToken).ConfigureAwait(false);
}
protected async Task Fetch(Video video, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount)
{
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;
}
}
var mediaStreams = MediaEncoderHelpers.GetMediaInfo(data).MediaStreams;
var chapters = data.Chapters ?? new List<ChapterInfo>();
if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
{
var inputPath = isoMount != null ? isoMount.MountedPath : video.Path;
FetchBdInfo(video, chapters, mediaStreams, inputPath, cancellationToken);
}
AddExternalSubtitles(video, mediaStreams);
FetchWtvInfo(video, data);
video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270);
if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
{
AddDummyChapters(video, chapters);
}
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);
await FFMpegManager.Instance.PopulateChapterImages(video, chapters, false, false, cancellationToken).ConfigureAwait(false);
await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false);
await _itemRepo.SaveChapters(video.Id, chapters, cancellationToken).ConfigureAwait(false);
}
private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, string inputPath, CancellationToken cancellationToken)
{
var video = (Video)item;
var result = GetBDInfo(inputPath);
cancellationToken.ThrowIfCancellationRequested();
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
Fetch(video, mediaStreams, result, chapters);
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;
}
/// <param name="chapters">The chapters.</param>
private void Fetch(Video video, List<MediaStream> mediaStreams, BlurayDiscInfo stream, List<ChapterInfo> chapters)
{
// Check all input for null/empty/zero
mediaStreams.Clear();
mediaStreams.AddRange(stream.MediaStreams);
video.MainFeaturePlaylistName = stream.PlaylistName;
if (stream.RunTimeTicks.HasValue && stream.RunTimeTicks.Value > 0)
{
video.RunTimeTicks = stream.RunTimeTicks;
}
video.PlayableStreamFileNames = stream.Files.ToList();
if (stream.Chapters != null)
{
chapters.Clear();
chapters.AddRange(stream.Chapters.Select(c => new ChapterInfo
{
StartPositionTicks = TimeSpan.FromSeconds(c).Ticks
}));
}
}
/// <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);
}
private void FetchWtvInfo(Video video, InternalMediaInfoResult data)
{
if (data.format == null || data.format.tags == null)
{
return;
}
if (video.Genres.Count == 0)
{
if (!video.LockedFields.Contains(MetadataFields.Genres))
{
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
if (!string.IsNullOrEmpty(genres))
{
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Trim())
.ToList();
}
}
}
if (string.IsNullOrEmpty(video.Overview))
{
if (!video.LockedFields.Contains(MetadataFields.Overview))
{
var overview = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
if (!string.IsNullOrWhiteSpace(overview))
{
video.Overview = overview;
}
}
}
if (string.IsNullOrEmpty(video.OfficialRating))
{
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
if (!string.IsNullOrWhiteSpace(officialRating))
{
if (!video.LockedFields.Contains(MetadataFields.OfficialRating))
{
video.OfficialRating = officialRating;
}
}
}
if (video.People.Count == 0)
{
if (!video.LockedFields.Contains(MetadataFields.Cast))
{
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
if (!string.IsNullOrEmpty(people))
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new PersonInfo { Name = i.Trim(), Type = PersonType.Actor })
.ToList();
}
}
}
if (!video.ProductionYear.HasValue)
{
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;
}
}
}
}
private IEnumerable<string> SubtitleExtensions
{
get
{
return new[] { ".srt", ".ssa", ".ass" };
}
}
/// <summary>
/// Adds the external subtitles.
/// </summary>
/// <param name="video">The video.</param>
/// <param name="currentStreams">The current streams.</param>
private void AddExternalSubtitles(Video video, List<MediaStream> currentStreams)
{
var useParent = !video.ResolveArgs.IsDirectory;
if (useParent && video.Parent == null)
{
return;
}
var fileSystemChildren = useParent
? video.Parent.ResolveArgs.FileSystemChildren
: video.ResolveArgs.FileSystemChildren;
var startIndex = currentStreams.Count;
var streams = new List<MediaStream>();
var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
foreach (var file in fileSystemChildren
.Where(f => !f.Attributes.HasFlag(FileAttributes.Directory) && SubtitleExtensions.Contains(Path.GetExtension(f.FullName), StringComparer.OrdinalIgnoreCase)))
{
var fullName = file.FullName;
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fullName);
// If the subtitle file matches the video file name
if (string.Equals(videoFileNameWithoutExtension, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
streams.Add(new MediaStream
{
Index = startIndex++,
Type = MediaStreamType.Subtitle,
IsExternal = true,
Path = fullName,
Codec = Path.GetExtension(fullName).ToLower().TrimStart('.')
});
}
else if (fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension + ".", StringComparison.OrdinalIgnoreCase))
{
// Support xbmc naming conventions - 300.spanish.srt
var language = fileNameWithoutExtension.Split('.').LastOrDefault();
// Try to translate to three character code
// Be flexible and check against both the full and three character versions
var culture = _localization.GetCultures()
.FirstOrDefault(i => string.Equals(i.DisplayName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.ThreeLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase) || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase));
if (culture != null)
{
language = culture.ThreeLetterISOLanguageName;
}
streams.Add(new MediaStream
{
Index = startIndex++,
Type = MediaStreamType.Subtitle,
IsExternal = true,
Path = fullName,
Codec = Path.GetExtension(fullName).ToLower().TrimStart('.'),
Language = language
});
}
}
currentStreams.AddRange(streams);
}
/// <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
{
Name = "Chapter " + index,
StartPositionTicks = currentChapterTicks
});
index++;
currentChapterTicks += _dummyChapterDuration;
}
}
/// <summary>
/// Called when [pre fetch].
/// </summary>
/// <param name="item">The item.</param>
/// <param name="mount">The mount.</param>
private void OnPreFetch(Video item, IIsoMount mount)
{
if (item.VideoType == VideoType.Iso)
{
item.IsoType = DetermineIsoType(mount);
}
if (item.VideoType == VideoType.Dvd || (item.IsoType.HasValue && item.IsoType == IsoType.Dvd))
{
FetchFromDvdLib(item, mount);
}
}
private void FetchFromDvdLib(Video item, IIsoMount mount)
{
var path = mount == null ? item.Path : mount.MountedPath;
var dvd = new Dvd(path);
item.RunTimeTicks = dvd.Titles.Select(GetRuntime).Max();
var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
uint? titleNumber = null;
if (primaryTitle != null)
{
titleNumber = primaryTitle.TitleNumber;
}
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)
{
var folders = Directory.EnumerateDirectories(isoMount.MountedPath).Select(Path.GetFileName).ToList();
if (folders.Contains("video_ts", StringComparer.OrdinalIgnoreCase))
{
return IsoType.Dvd;
}
if (folders.Contains("bdmv", StringComparer.OrdinalIgnoreCase))
{
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
var allVobs = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
.Where(file => string.Equals(Path.GetExtension(file), ".vob", StringComparison.OrdinalIgnoreCase))
.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));
var vobs = allVobs.Where(i => Path.GetFileName(i).StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
if (vobs.Count > 0)
{
return vobs;
}
_logger.Debug("Could not determine vob file list for {0} using DvdLib. Will scan using file sizes.", video.Path);
}
var files = allVobs
.SkipWhile(f => new FileInfo(f).Length < minPlayableSize)
.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)
{
var parts = Path.GetFileNameWithoutExtension(files[0]).Split('_');
if (parts.Length == 3)
{
var title = parts[1];
files = files.TakeWhile(f =>
{
var fileParts = Path.GetFileNameWithoutExtension(f).Split('_');
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;
}
}
}
return files;
}
}
}

@ -192,7 +192,7 @@ namespace MediaBrowser.Providers.MediaInfo
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
NormalizeFFProbeResult(result); FFProbeHelpers.NormalizeFFProbeResult(result);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
if (!video.LockedFields.Contains(MetadataFields.Genres)) if (!video.LockedFields.Contains(MetadataFields.Genres))
{ {
var genres = GetDictionaryValue(data.format.tags, "genre"); var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
if (!string.IsNullOrEmpty(genres)) if (!string.IsNullOrEmpty(genres))
{ {
@ -417,7 +417,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
if (!video.LockedFields.Contains(MetadataFields.Overview)) if (!video.LockedFields.Contains(MetadataFields.Overview))
{ {
var overview = GetDictionaryValue(data.format.tags, "WM/SubTitleDescription"); var overview = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
if (!string.IsNullOrWhiteSpace(overview)) if (!string.IsNullOrWhiteSpace(overview))
{ {
@ -428,7 +428,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (force || string.IsNullOrEmpty(video.OfficialRating)) if (force || string.IsNullOrEmpty(video.OfficialRating))
{ {
var officialRating = GetDictionaryValue(data.format.tags, "WM/ParentalRating"); var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
if (!string.IsNullOrWhiteSpace(officialRating)) if (!string.IsNullOrWhiteSpace(officialRating))
{ {
@ -443,7 +443,7 @@ namespace MediaBrowser.Providers.MediaInfo
{ {
if (!video.LockedFields.Contains(MetadataFields.Cast)) if (!video.LockedFields.Contains(MetadataFields.Cast))
{ {
var people = GetDictionaryValue(data.format.tags, "WM/MediaCredits"); var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
if (!string.IsNullOrEmpty(people)) if (!string.IsNullOrEmpty(people))
{ {
@ -457,7 +457,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (force || !video.ProductionYear.HasValue) if (force || !video.ProductionYear.HasValue)
{ {
var year = GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime"); var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
if (!string.IsNullOrWhiteSpace(year)) if (!string.IsNullOrWhiteSpace(year))
{ {

@ -1,90 +1,28 @@
using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaInfo; using MediaBrowser.Controller.MediaInfo;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Generic;
using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MediaBrowser.Providers.MediaInfo namespace MediaBrowser.Providers.MediaInfo
{ {
class VideoImageProvider : BaseMetadataProvider public class VideoImageProvider : IDynamicImageProvider
{ {
/// <summary>
/// The _locks
/// </summary>
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new ConcurrentDictionary<string, SemaphoreSlim>();
/// <summary>
/// The _media encoder
/// </summary>
private readonly IMediaEncoder _mediaEncoder;
private readonly IIsoManager _isoManager; private readonly IIsoManager _isoManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerConfigurationManager _config;
public VideoImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IIsoManager isoManager) public VideoImageProvider(IIsoManager isoManager, IMediaEncoder mediaEncoder, IServerConfigurationManager config)
: base(logManager, configurationManager)
{ {
_mediaEncoder = mediaEncoder;
_isoManager = isoManager; _isoManager = isoManager;
} _mediaEncoder = mediaEncoder;
_config = config;
/// <summary>
/// Gets a value indicating whether [refresh on version change].
/// </summary>
/// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
protected override bool RefreshOnVersionChange
{
get
{
return true;
}
}
/// <summary>
/// Gets the provider version.
/// </summary>
/// <value>The provider version.</value>
protected override string ProviderVersion
{
get
{
return "1";
}
}
/// <summary>
/// Supportses the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
public override bool Supports(BaseItem item)
{
return item.LocationType == LocationType.FileSystem && item is Video;
}
/// <summary>
/// Needses the refresh internal.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="providerInfo">The provider info.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
{
var video = (Video)item;
if (!QualifiesForExtraction(video))
{
return false;
}
return base.NeedsRefreshInternal(item, providerInfo);
} }
/// <summary> /// <summary>
@ -94,16 +32,6 @@ namespace MediaBrowser.Providers.MediaInfo
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private bool QualifiesForExtraction(Video item) private bool QualifiesForExtraction(Video item)
{ {
if (!ConfigurationManager.Configuration.EnableVideoImageExtraction)
{
return false;
}
if (!string.IsNullOrEmpty(item.PrimaryImagePath))
{
return false;
}
// No support for this // No support for this
if (item.VideoType == VideoType.HdDvd) if (item.VideoType == VideoType.HdDvd)
{ {
@ -126,137 +54,61 @@ namespace MediaBrowser.Providers.MediaInfo
} }
/// <summary> /// <summary>
/// Override this to return the date that should be compared to the last refresh date /// The null mount task result
/// to determine if this provider should be re-fetched.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>DateTime.</returns>
protected override DateTime CompareDate(BaseItem item)
{
return item.DateModified;
}
/// <summary>
/// Gets the priority.
/// </summary> /// </summary>
/// <value>The priority.</value> protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
public override MetadataProviderPriority Priority
{
get { return MetadataProviderPriority.Last; }
}
public override ItemUpdateType ItemUpdateType
{
get
{
return ItemUpdateType.ImageUpdate;
}
}
/// <summary> /// <summary>
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done /// Mounts the iso if needed.
/// </summary> /// </summary>
/// <param name="item">The item.</param> /// <param name="item">The item.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns> /// <returns>Task{IIsoMount}.</returns>
public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken) protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
{ {
item.ValidateImages(); if (item.VideoType == VideoType.Iso)
var video = (Video)item;
// Double check this here in case force was used
if (QualifiesForExtraction(video))
{ {
try return _isoManager.Mount(item.Path, cancellationToken);
{
await ExtractImage(video, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
// Swallow this so that we don't keep on trying over and over again
Logger.ErrorException("Error extracting image for {0}", ex, item.Name);
}
} }
SetLastRefreshed(item, DateTime.UtcNow, providerInfo); return NullMountTaskResult;
return true;
} }
/// <summary> public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
/// Extracts the image.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task ExtractImage(Video item, CancellationToken cancellationToken)
{ {
cancellationToken.ThrowIfCancellationRequested(); return new List<ImageType> { ImageType.Primary };
}
var path = GetVideoImagePath(item);
if (!File.Exists(path))
{
var semaphore = GetLock(path);
// Acquire a lock
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
// Check again
if (!File.Exists(path))
{
try
{
var parentPath = Path.GetDirectoryName(path);
Directory.CreateDirectory(parentPath);
await ExtractImageInternal(item, path, cancellationToken).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
else
{
semaphore.Release();
}
}
// Image is already in the cache public Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken)
item.SetImagePath(ImageType.Primary, path); {
return GetVideoImage((Video)item, cancellationToken);
} }
/// <summary> public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
/// Extracts the image.
/// </summary>
/// <param name="video">The video.</param>
/// <param name="path">The path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task ExtractImageInternal(Video video, string path, CancellationToken cancellationToken)
{ {
var isoMount = await MountIsoIfNeeded(video, cancellationToken).ConfigureAwait(false); var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false);
try try
{ {
// If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in. // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
// Always use 10 seconds for dvd because our duration could be out of whack // Always use 10 seconds for dvd because our duration could be out of whack
var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue && var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
video.RunTimeTicks.Value > 0 item.RunTimeTicks.Value > 0
? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1)) ? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1))
: TimeSpan.FromSeconds(10); : TimeSpan.FromSeconds(10);
InputType type; InputType type;
var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type); var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, item.LocationType == LocationType.Remote, item.VideoType, item.IsoType, isoMount, item.PlayableStreamFileNames, out type);
await _mediaEncoder.ExtractImage(inputPath, type, false, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false); var stream = await _mediaEncoder.ExtractImage(inputPath, type, false, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
video.SetImagePath(ImageType.Primary, path); return new DynamicImageResponse
{
Format = ImageFormat.Jpg,
HasImage = true,
Stream = stream
};
} }
finally finally
{ {
@ -267,63 +119,19 @@ namespace MediaBrowser.Providers.MediaInfo
} }
} }
/// <summary> public string Name
/// The null mount task result
/// </summary>
protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
/// <summary>
/// Mounts the iso if needed.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IIsoMount}.</returns>
protected Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
{
if (item.VideoType == VideoType.Iso)
{
return _isoManager.Mount(item.Path, cancellationToken);
}
return NullMountTaskResult;
}
/// <summary>
/// Gets the lock.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>SemaphoreSlim.</returns>
private SemaphoreSlim GetLock(string filename)
{ {
return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); get { return "Embedded Image"; }
} }
/// <summary> public bool Supports(IHasImages item)
/// Gets the video images data path.
/// </summary>
/// <value>The video images data path.</value>
public string VideoImagesPath
{ {
get if (!_config.Configuration.EnableVideoImageExtraction)
{ {
return Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "extracted-video-images"); return false;
} }
}
/// <summary> return item.LocationType == LocationType.FileSystem && item is Video;
/// Gets the audio image path.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
private string GetVideoImagePath(Video item)
{
var filename = item.Path + "_" + item.DateModified.Ticks + "_primary";
filename = filename.GetMD5() + ".jpg";
var prefix = filename.Substring(0, 1);
return Path.Combine(VideoImagesPath, prefix, filename);
} }
} }
} }

@ -371,6 +371,11 @@ namespace MediaBrowser.Providers.Movies
{ {
var path = GetDataFilePath(item); var path = GetDataFilePath(item);
if (string.IsNullOrEmpty(path))
{
return _cachedTask;
}
var fileInfo = _fileSystem.GetFileSystemInfo(path); var fileInfo = _fileSystem.GetFileSystemInfo(path);
if (fileInfo.Exists) if (fileInfo.Exists)

@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.Movies
try try
{ {
await new MovieXmlParser(Logger, _itemRepo).FetchAsync(video, path, cancellationToken).ConfigureAwait(false); new MovieXmlParser(Logger).FetchAsync(video, path, cancellationToken);
} }
finally finally
{ {

@ -1,10 +1,8 @@
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
namespace MediaBrowser.Providers.Movies namespace MediaBrowser.Providers.Movies
@ -14,28 +12,14 @@ namespace MediaBrowser.Providers.Movies
/// </summary> /// </summary>
public class MovieXmlParser : BaseItemXmlParser<Video> public class MovieXmlParser : BaseItemXmlParser<Video>
{ {
private readonly IItemRepository _itemRepo; public MovieXmlParser(ILogger logger)
private Task _chaptersTask = null;
public MovieXmlParser(ILogger logger, IItemRepository itemRepo)
: base(logger) : base(logger)
{ {
_itemRepo = itemRepo;
} }
public async Task FetchAsync(Video item, string metadataFile, CancellationToken cancellationToken) public void FetchAsync(Video item, string metadataFile, CancellationToken cancellationToken)
{ {
_chaptersTask = null;
Fetch(item, metadataFile, cancellationToken); Fetch(item, metadataFile, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
if (_chaptersTask != null)
{
await _chaptersTask.ConfigureAwait(false);
}
} }
/// <summary> /// <summary>

@ -0,0 +1,78 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Logging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.Movies
{
public class MovieXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Movie>
{
private readonly ILogger _logger;
public MovieXmlProvider(IFileSystem fileSystem, ILogger logger)
: base(fileSystem)
{
_logger = logger;
}
public async Task<MetadataResult<Movie>> GetMetadata(string path, CancellationToken cancellationToken)
{
path = GetXmlFile(path).FullName;
var result = new MetadataResult<Movie>();
await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
result.Item = new Movie();
new MovieXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
result.HasMetadata = true;
}
catch (FileNotFoundException)
{
result.HasMetadata = false;
}
finally
{
XmlParsingResourcePool.Release();
}
return result;
}
public string Name
{
get { return "Media Browser Xml"; }
}
protected override FileInfo GetXmlFile(string path)
{
return GetXmlFileInfo(path, FileSystem);
}
public static FileInfo GetXmlFileInfo(string path, IFileSystem _fileSystem)
{
var fileInfo = _fileSystem.GetFileSystemInfo(path);
var directoryInfo = fileInfo as DirectoryInfo;
if (directoryInfo == null)
{
directoryInfo = new DirectoryInfo(Path.GetDirectoryName(path));
}
var directoryPath = directoryInfo.FullName;
var specificFile = Path.Combine(directoryPath, Path.GetFileNameWithoutExtension(path) + ".xml");
var file = new FileInfo(specificFile);
return file.Exists ? file : new FileInfo(Path.Combine(directoryPath, "movie.xml"));
}
}
}

@ -112,7 +112,7 @@ namespace MediaBrowser.Providers.Music
if (artist != null) if (artist != null)
{ {
id.ArtistMusicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz); id.ArtistProviderIds = artist.ProviderIds;
id.AlbumArtist = id.AlbumArtist ?? artist.Name; id.AlbumArtist = id.AlbumArtist ?? artist.Name;
} }

@ -37,7 +37,10 @@ namespace MediaBrowser.Providers.Music
if (string.IsNullOrEmpty(releaseId)) if (string.IsNullOrEmpty(releaseId))
{ {
var releaseResult = await GetReleaseResult(albumId.ArtistMusicBrainzId, albumId.AlbumArtist, albumId.Name, cancellationToken).ConfigureAwait(false); string artistMusicBrainzId;
albumId.ArtistProviderIds.TryGetValue(MetadataProviders.Musicbrainz.ToString(), out artistMusicBrainzId);
var releaseResult = await GetReleaseResult(artistMusicBrainzId, albumId.AlbumArtist, albumId.Name, cancellationToken).ConfigureAwait(false);
result.Item = new MusicAlbum(); result.Item = new MusicAlbum();

@ -0,0 +1,53 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.Music
{
class MusicVideoMetadataService : MetadataService<MusicVideo, ItemId>
{
private readonly ILibraryManager _libraryManager;
public MusicVideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Merges the specified source.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="target">The target.</param>
/// <param name="lockedFields">The locked fields.</param>
/// <param name="replaceData">if set to <c>true</c> [replace data].</param>
/// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
protected override void MergeData(MusicVideo source, MusicVideo target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
{
ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
if (replaceData || string.IsNullOrEmpty(target.Album))
{
target.Album = source.Album;
}
if (replaceData || string.IsNullOrEmpty(target.Artist))
{
target.Artist = source.Artist;
}
}
protected override Task SaveItem(MusicVideo item, ItemUpdateType reason, CancellationToken cancellationToken)
{
return _libraryManager.UpdateItem(item, reason, cancellationToken);
}
}
}

@ -0,0 +1,60 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Movies;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.Music
{
class MusicVideoXmlProvider : BaseXmlProvider, ILocalMetadataProvider<MusicVideo>
{
private readonly ILogger _logger;
public MusicVideoXmlProvider(IFileSystem fileSystem, ILogger logger)
: base(fileSystem)
{
_logger = logger;
}
public async Task<MetadataResult<MusicVideo>> GetMetadata(string path, CancellationToken cancellationToken)
{
path = GetXmlFile(path).FullName;
var result = new MetadataResult<MusicVideo>();
await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var item = new MusicVideo();
new MusicVideoXmlParser(_logger).Fetch(item, path, cancellationToken);
result.HasMetadata = true;
result.Item = item;
}
catch (FileNotFoundException)
{
result.HasMetadata = false;
}
finally
{
XmlParsingResourcePool.Release();
}
return result;
}
public string Name
{
get { return "Media Browser Xml"; }
}
protected override FileInfo GetXmlFile(string path)
{
return MovieXmlProvider.GetXmlFileInfo(path, FileSystem);
}
}
}

@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -2,6 +2,7 @@
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using System; using System;
@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Omdb
_httpClient = httpClient; _httpClient = httpClient;
} }
public async Task Fetch(BaseItem item, CancellationToken cancellationToken) public async Task<ItemUpdateType> Fetch(BaseItem item, CancellationToken cancellationToken)
{ {
var imdbId = item.GetProviderId(MetadataProviders.Imdb); var imdbId = item.GetProviderId(MetadataProviders.Imdb);
if (string.IsNullOrEmpty(imdbId)) if (string.IsNullOrEmpty(imdbId))
{ {
return; return ItemUpdateType.Unspecified;
} }
var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId; var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
@ -97,6 +98,8 @@ namespace MediaBrowser.Providers.Omdb
ParseAdditionalMetadata(item, result); ParseAdditionalMetadata(item, result);
} }
return ItemUpdateType.MetadataDownload;
} }
private void ParseAdditionalMetadata(BaseItem item, RootObject result) private void ParseAdditionalMetadata(BaseItem item, RootObject result)

@ -1,5 +1,8 @@
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using System.Threading; using System.Threading;
@ -7,7 +10,8 @@ using System.Threading.Tasks;
namespace MediaBrowser.Providers.Omdb namespace MediaBrowser.Providers.Omdb
{ {
public class OmdbSeriesProvider : ICustomMetadataProvider<Series> public class OmdbSeriesProvider : ICustomMetadataProvider<Series>,
ICustomMetadataProvider<Movie>, ICustomMetadataProvider<Trailer>
{ {
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
@ -18,14 +22,30 @@ namespace MediaBrowser.Providers.Omdb
_httpClient = httpClient; _httpClient = httpClient;
} }
public Task FetchAsync(Series item, CancellationToken cancellationToken) public string Name
{
get { return "OMDb"; }
}
public Task<ItemUpdateType> FetchAsync(Series item, CancellationToken cancellationToken)
{ {
return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken); return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
} }
public string Name public Task<ItemUpdateType> FetchAsync(Movie item, CancellationToken cancellationToken)
{ {
get { return "OMDb"; } return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
}
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.Unspecified);
public Task<ItemUpdateType> FetchAsync(Trailer item, CancellationToken cancellationToken)
{
if (item.IsLocalTrailer)
{
return _cachedTask;
}
return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
} }
} }
} }

@ -158,6 +158,7 @@ namespace MediaBrowser.Providers
} }
MergeAlbumArtist(source, target, lockedFields, replaceData); MergeAlbumArtist(source, target, lockedFields, replaceData);
MergeBudget(source, target, lockedFields, replaceData);
if (mergeMetadataSettings) if (mergeMetadataSettings)
{ {
@ -198,5 +199,24 @@ namespace MediaBrowser.Providers
} }
} }
} }
private static void MergeBudget(BaseItem source, BaseItem target, List<MetadataFields> lockedFields, bool replaceData)
{
var sourceHasBudget = source as IHasBudget;
var targetHasBudget = target as IHasBudget;
if (sourceHasBudget != null && targetHasBudget != null)
{
if (replaceData || !targetHasBudget.Budget.HasValue)
{
targetHasBudget.Budget = sourceHasBudget.Budget;
}
if (replaceData || !targetHasBudget.Revenue.HasValue)
{
targetHasBudget.Revenue = sourceHasBudget.Revenue;
}
}
}
} }
} }

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -1,163 +0,0 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.TV
{
/// <summary>
/// Class EpisodeImageFromMediaLocationProvider
/// </summary>
public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
{
public EpisodeImageFromMediaLocationProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
: base(logManager, configurationManager)
{
}
public override ItemUpdateType ItemUpdateType
{
get
{
return ItemUpdateType.ImageUpdate;
}
}
/// <summary>
/// Supportses the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
public override bool Supports(BaseItem item)
{
return item is Episode && item.LocationType == LocationType.FileSystem;
}
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override MetadataProviderPriority Priority
{
get { return MetadataProviderPriority.First; }
}
/// <summary>
/// Returns true or false indicating if the provider should refresh when the contents of it's directory changes
/// </summary>
/// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
protected override bool RefreshOnFileSystemStampChange
{
get
{
return true;
}
}
/// <summary>
/// Gets the filestamp extensions.
/// </summary>
/// <value>The filestamp extensions.</value>
protected override string[] FilestampExtensions
{
get
{
return BaseItem.SupportedImageExtensions;
}
}
/// <summary>
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
/// </summary>
/// <param name="item">The item.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns>
public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var episode = (Episode)item;
var episodeFileName = Path.GetFileName(episode.Path);
var parent = item.ResolveArgs.Parent;
ValidateImage(episode);
cancellationToken.ThrowIfCancellationRequested();
SetPrimaryImagePath(episode, parent, item.MetaLocation, episodeFileName);
SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
return TrueTaskResult;
}
/// <summary>
/// Validates the primary image path still exists
/// </summary>
/// <param name="episode">The episode.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private void ValidateImage(Episode episode)
{
var path = episode.PrimaryImagePath;
if (string.IsNullOrEmpty(path))
{
return;
}
if (!File.Exists(path))
{
episode.SetImagePath(ImageType.Primary, null);
}
}
/// <summary>
/// Sets the primary image path.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="parent">The parent.</param>
/// <param name="metadataFolder">The metadata folder.</param>
/// <param name="episodeFileName">Name of the episode file.</param>
private void SetPrimaryImagePath(Episode item, Folder parent, string metadataFolder, string episodeFileName)
{
foreach (var extension in BaseItem.SupportedImageExtensions)
{
var path = Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, extension));
var file = parent.ResolveArgs.GetMetaFileByPath(path);
if (file != null)
{
item.SetImagePath(ImageType.Primary, file.FullName);
return;
}
}
var seasonFolder = Path.GetDirectoryName(item.Path);
foreach (var extension in BaseItem.SupportedImageExtensions)
{
var imageFilename = Path.GetFileNameWithoutExtension(episodeFileName) + "-thumb" + extension;
var path = Path.Combine(seasonFolder, imageFilename);
var file = parent.ResolveArgs.GetMetaFileByPath(path);
if (file != null)
{
item.SetImagePath(ImageType.Primary, file.FullName);
return;
}
}
}
}
}

@ -1,99 +0,0 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.TV
{
/// <summary>
/// Making this a provider because of how slow it is
/// It only ever needs to run once
/// </summary>
public class EpisodeIndexNumberProvider : BaseMetadataProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
/// </summary>
/// <param name="logManager">The log manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
public EpisodeIndexNumberProvider(ILogManager logManager, IServerConfigurationManager configurationManager)
: base(logManager, configurationManager)
{
}
protected override bool RefreshOnVersionChange
{
get
{
return true;
}
}
protected override string ProviderVersion
{
get
{
return "2";
}
}
/// <summary>
/// Supportses the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
public override bool Supports(BaseItem item)
{
if (item is Episode)
{
var locationType = item.LocationType;
return locationType != LocationType.Virtual && locationType != LocationType.Remote;
}
return false;
}
/// <summary>
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
/// </summary>
/// <param name="item">The item.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns>
public override Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
{
var episode = (Episode)item;
episode.IndexNumber = TVUtils.GetEpisodeNumberFromFile(item.Path, item.Parent is Season);
episode.IndexNumberEnd = TVUtils.GetEndingEpisodeNumberFromFile(item.Path);
if (!episode.ParentIndexNumber.HasValue)
{
var season = episode.Parent as Season;
if (season != null)
{
episode.ParentIndexNumber = season.IndexNumber;
}
}
SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
return TrueTaskResult;
}
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override MetadataProviderPriority Priority
{
get { return MetadataProviderPriority.First; }
}
}
}

@ -0,0 +1,59 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MediaBrowser.Providers.TV
{
public class EpisodeLocalImageProvider : IImageFileProvider
{
public string Name
{
get { return "Local Images"; }
}
public bool Supports(IHasImages item)
{
return item is Episode && item.LocationType == LocationType.FileSystem;
}
public List<LocalImageInfo> GetImages(IHasImages item)
{
var parentPath = Path.GetDirectoryName(item.Path);
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path);
var thumbName = nameWithoutExtension + "-thumb";
return Directory.EnumerateFiles(parentPath, "*", SearchOption.AllDirectories)
.Where(i =>
{
if (BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i) ?? string.Empty))
{
var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i);
if (string.Equals(nameWithoutExtension, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(thumbName, currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
})
.Select(i => new LocalImageInfo
{
Path = i,
Type = ImageType.Primary
})
.ToList();
}
}
}

@ -0,0 +1,137 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.TV
{
public class EpisodeMetadataService : MetadataService<Episode, EpisodeId>
{
private readonly ILibraryManager _libraryManager;
public EpisodeMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IProviderRepository providerRepo, IFileSystem fileSystem, ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, providerRepo, fileSystem)
{
_libraryManager = libraryManager;
}
/// <summary>
/// Merges the specified source.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="target">The target.</param>
/// <param name="lockedFields">The locked fields.</param>
/// <param name="replaceData">if set to <c>true</c> [replace data].</param>
/// <param name="mergeMetadataSettings">if set to <c>true</c> [merge metadata settings].</param>
protected override void MergeData(Episode source, Episode target, List<MetadataFields> lockedFields, bool replaceData, bool mergeMetadataSettings)
{
ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
if (replaceData || !target.AirsBeforeSeasonNumber.HasValue)
{
target.AirsBeforeSeasonNumber = source.AirsBeforeSeasonNumber;
}
if (replaceData || !target.AirsAfterSeasonNumber.HasValue)
{
target.AirsAfterSeasonNumber = source.AirsAfterSeasonNumber;
}
if (replaceData || !target.AirsBeforeEpisodeNumber.HasValue)
{
target.AirsBeforeEpisodeNumber = source.AirsBeforeEpisodeNumber;
}
if (replaceData || !target.DvdSeasonNumber.HasValue)
{
target.DvdSeasonNumber = source.DvdSeasonNumber;
}
if (replaceData || !target.DvdEpisodeNumber.HasValue)
{
target.DvdEpisodeNumber = source.DvdEpisodeNumber;
}
if (replaceData || !target.AbsoluteEpisodeNumber.HasValue)
{
target.AbsoluteEpisodeNumber = source.AbsoluteEpisodeNumber;
}
if (replaceData || !target.IndexNumberEnd.HasValue)
{
target.IndexNumberEnd = source.IndexNumberEnd;
}
}
protected override Task SaveItem(Episode item, ItemUpdateType reason, CancellationToken cancellationToken)
{
return _libraryManager.UpdateItem(item, reason, cancellationToken);
}
protected override EpisodeId GetId(Episode item)
{
var id = base.GetId(item);
var series = item.Series;
if (series != null)
{
id.SeriesProviderIds = series.ProviderIds;
}
id.IndexNumberEnd = item.IndexNumberEnd;
return id;
}
protected override ItemUpdateType BeforeMetadataRefresh(Episode item)
{
var updateType = base.BeforeMetadataRefresh(item);
var locationType = item.LocationType;
if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
{
var currentIndexNumber = item.IndexNumber;
var currentIndexNumberEnd = item.IndexNumberEnd;
var currentParentIndexNumber = item.ParentIndexNumber;
item.IndexNumber = item.IndexNumber ?? TVUtils.GetEpisodeNumberFromFile(item.Path, item.Parent is Season);
item.IndexNumberEnd = item.IndexNumberEnd ?? TVUtils.GetEndingEpisodeNumberFromFile(item.Path);
if (!item.ParentIndexNumber.HasValue)
{
var season = item.Season;
if (season != null)
{
item.ParentIndexNumber = season.IndexNumber;
}
}
if ((currentIndexNumber ?? -1) != (item.IndexNumber ?? -1))
{
updateType = updateType | ItemUpdateType.MetadataImport;
}
if ((currentIndexNumberEnd ?? -1) != (item.IndexNumberEnd ?? -1))
{
updateType = updateType | ItemUpdateType.MetadataImport;
}
if ((currentParentIndexNumber ?? -1) != (item.ParentIndexNumber ?? -1))
{
updateType = updateType | ItemUpdateType.MetadataImport;
}
}
return updateType;
}
}
}

@ -1,103 +0,0 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.TV
{
/// <summary>
/// Class EpisodeProviderFromXml
/// </summary>
public class EpisodeProviderFromXml : BaseMetadataProvider
{
private readonly IItemRepository _itemRepo;
private readonly IFileSystem _fileSystem;
public EpisodeProviderFromXml(ILogManager logManager, IServerConfigurationManager configurationManager, IItemRepository itemRepo, IFileSystem fileSystem)
: base(logManager, configurationManager)
{
_itemRepo = itemRepo;
_fileSystem = fileSystem;
}
/// <summary>
/// Supportses the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
public override bool Supports(BaseItem item)
{
return item is Episode && item.LocationType == LocationType.FileSystem;
}
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override MetadataProviderPriority Priority
{
get { return MetadataProviderPriority.First; }
}
/// <summary>
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
/// </summary>
/// <param name="item">The item.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns>
public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile);
if (file != null)
{
await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await new EpisodeXmlParser(Logger, _itemRepo).FetchAsync((Episode)item, metadataFile, cancellationToken).ConfigureAwait(false);
}
finally
{
XmlParsingResourcePool.Release();
}
}
SetLastRefreshed(item, DateTime.UtcNow, providerInfo);
return true;
}
/// <summary>
/// Needses the refresh based on compare date.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="providerInfo">The provider info.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo)
{
var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile);
if (file == null)
{
return false;
}
return _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
}
}
}

@ -1,13 +1,11 @@
using System; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Xml; using System.Xml;
namespace MediaBrowser.Providers.TV namespace MediaBrowser.Providers.TV
@ -17,28 +15,14 @@ namespace MediaBrowser.Providers.TV
/// </summary> /// </summary>
public class EpisodeXmlParser : BaseItemXmlParser<Episode> public class EpisodeXmlParser : BaseItemXmlParser<Episode>
{ {
private readonly IItemRepository _itemRepo; public EpisodeXmlParser(ILogger logger)
private Task _chaptersTask = null;
public EpisodeXmlParser(ILogger logger, IItemRepository itemRepo)
: base(logger) : base(logger)
{ {
_itemRepo = itemRepo;
} }
public async Task FetchAsync(Episode item, string metadataFile, CancellationToken cancellationToken) public void FetchAsync(Episode item, string metadataFile, CancellationToken cancellationToken)
{ {
_chaptersTask = null; Fetch(item, metadataFile, cancellationToken);
Fetch(item, metadataFile, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
if (_chaptersTask != null)
{
await _chaptersTask.ConfigureAwait(false);
}
} }
private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private static readonly CultureInfo UsCulture = new CultureInfo("en-US");

@ -0,0 +1,62 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Logging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Providers.TV
{
public class EpisodeXmlProvider : BaseXmlProvider, ILocalMetadataProvider<Episode>
{
private readonly ILogger _logger;
public EpisodeXmlProvider(IFileSystem fileSystem, ILogger logger)
: base(fileSystem)
{
_logger = logger;
}
public async Task<MetadataResult<Episode>> GetMetadata(string path, CancellationToken cancellationToken)
{
path = GetXmlFile(path).FullName;
var result = new MetadataResult<Episode>();
await XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
result.Item = new Episode();
new EpisodeXmlParser(_logger).Fetch(result.Item, path, cancellationToken);
result.HasMetadata = true;
}
catch (FileNotFoundException)
{
result.HasMetadata = false;
}
finally
{
XmlParsingResourcePool.Release();
}
return result;
}
public string Name
{
get { return "Media Browser Xml"; }
}
protected override FileInfo GetXmlFile(string path)
{
var metadataPath = Path.GetDirectoryName(path);
metadataPath = Path.Combine(metadataPath, "metadata");
var metadataFile = Path.Combine(metadataPath, Path.ChangeExtension(Path.GetFileName(path), ".xml"));
return new FileInfo(metadataFile);
}
}
}

@ -1,4 +1,5 @@
using MediaBrowser.Common.Net; using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -17,16 +19,18 @@ using System.Xml;
namespace MediaBrowser.Providers.TV namespace MediaBrowser.Providers.TV
{ {
public class TvdbEpisodeImageProvider : IRemoteImageProvider public class TvdbEpisodeImageProvider : IRemoteImageProvider, IHasChangeMonitor
{ {
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileSystem _fileSystem;
public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient) public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
{ {
_config = config; _config = config;
_httpClient = httpClient; _httpClient = httpClient;
_fileSystem = fileSystem;
} }
public string Name public string Name
@ -65,7 +69,7 @@ namespace MediaBrowser.Providers.TV
// Process images // Process images
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId); var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode, seriesDataPath); var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath);
var result = files.Select(i => GetImageInfo(i, cancellationToken)) var result = files.Select(i => GetImageInfo(i, cancellationToken))
.Where(i => i != null); .Where(i => i != null);
@ -186,5 +190,27 @@ namespace MediaBrowser.Providers.TV
ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool ResourcePool = TvdbSeriesProvider.Current.TvDbResourcePool
}); });
} }
public bool HasChanged(IHasMetadata item, DateTime date)
{
if (!item.HasImage(ImageType.Primary))
{
var episode = (Episode)item;
var series = episode.Series;
var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tvdb) : null;
if (!string.IsNullOrEmpty(seriesId))
{
// Process images
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath);
return files.Any(i => _fileSystem.GetLastWriteTimeUtc(i) > date);
}
}
return false;
}
} }
} }

@ -1,12 +1,10 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -25,161 +23,88 @@ namespace MediaBrowser.Providers.TV
/// <summary> /// <summary>
/// Class RemoteEpisodeProvider /// Class RemoteEpisodeProvider
/// </summary> /// </summary>
class TvdbEpisodeProvider : BaseMetadataProvider class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode>, IHasChangeMonitor
{ {
/// <summary>
/// The _provider manager
/// </summary>
private readonly IProviderManager _providerManager;
/// <summary>
/// Gets the HTTP client.
/// </summary>
/// <value>The HTTP client.</value>
protected IHttpClient HttpClient { get; private set; }
private readonly IFileSystem _fileSystem;
internal static TvdbEpisodeProvider Current; internal static TvdbEpisodeProvider Current;
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _config;
/// <summary> public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config)
/// Initializes a new instance of the <see cref="TvdbEpisodeProvider" /> class.
/// </summary>
/// <param name="httpClient">The HTTP client.</param>
/// <param name="logManager">The log manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="providerManager">The provider manager.</param>
public TvdbEpisodeProvider(IHttpClient httpClient, ILogManager logManager, IServerConfigurationManager configurationManager, IProviderManager providerManager, IFileSystem fileSystem)
: base(logManager, configurationManager)
{ {
HttpClient = httpClient;
_providerManager = providerManager;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_config = config;
Current = this; Current = this;
} }
/// <summary> public string Name
/// Supportses the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
public override bool Supports(BaseItem item)
{ {
return item is Episode; get { return "TheTVDB"; }
} }
public override ItemUpdateType ItemUpdateType public Task<MetadataResult<Episode>> GetMetadata(ItemId id, CancellationToken cancellationToken)
{ {
get var episodeId = (EpisodeId)id;
{
return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload;
}
}
/// <summary> string seriesTvdbId;
/// Gets the priority. episodeId.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesTvdbId);
/// </summary>
/// <value>The priority.</value>
public override MetadataProviderPriority Priority
{
get { return MetadataProviderPriority.Third; }
}
/// <summary> var result = new MetadataResult<Episode>();
/// Gets a value indicating whether [requires internet].
/// </summary>
/// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
public override bool RequiresInternet
{
get { return true; }
}
/// <summary> if (!string.IsNullOrEmpty(seriesTvdbId))
/// Gets a value indicating whether [refresh on version change].
/// </summary>
/// <value><c>true</c> if [refresh on version change]; otherwise, <c>false</c>.</value>
protected override bool RefreshOnVersionChange
{
get
{ {
return true; var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesTvdbId);
}
}
/// <summary> try
/// Gets the provider version.
/// </summary>
/// <value>The provider version.</value>
protected override string ProviderVersion
{
get
{
return "5";
}
}
/// <summary>
/// Needses the refresh internal.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="providerInfo">The provider info.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
{
var locationType = item.LocationType;
// Always use tvdb updates for non-file system episodes
if (locationType != LocationType.Remote && locationType != LocationType.Virtual)
{
// Don't proceed if there's local metadata
if (!ConfigurationManager.Configuration.EnableTvDbUpdates && HasLocalMeta(item))
{ {
return false; result.Item = FetchEpisodeData(episodeId, seriesDataPath, cancellationToken);
result.HasMetadata = result.Item != null;
}
catch (FileNotFoundException)
{
// Don't fail the provider because this will just keep on going and going.
} }
} }
return base.NeedsRefreshInternal(item, providerInfo); return Task.FromResult(result);
} }
protected override bool NeedsRefreshBasedOnCompareDate(BaseItem item, BaseProviderInfo providerInfo) public bool HasChanged(IHasMetadata item, DateTime date)
{ {
var episode = (Episode)item; var episode = (Episode)item;
var series = episode.Series;
var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null; var seriesId = series != null ? series.GetProviderId(MetadataProviders.Tvdb) : null;
if (!string.IsNullOrEmpty(seriesId)) if (!string.IsNullOrEmpty(seriesId))
{ {
// Process images // Process images
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId); var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
var files = GetEpisodeXmlFiles(episode, seriesDataPath); var files = GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath);
if (files.Count > 0) return files.Any(i => _fileSystem.GetLastWriteTimeUtc(i) > date);
{
return files.Select(i => _fileSystem.GetLastWriteTimeUtc(i)).Max() > providerInfo.LastRefreshed;
}
} }
return false; return false;
} }
/// <summary> /// <summary>
/// Gets the episode XML files. /// Gets the episode XML files.
/// </summary> /// </summary>
/// <param name="episode">The episode.</param> /// <param name="seasonNumber">The season number.</param>
/// <param name="episodeNumber">The episode number.</param>
/// <param name="endingEpisodeNumber">The ending episode number.</param>
/// <param name="seriesDataPath">The series data path.</param> /// <param name="seriesDataPath">The series data path.</param>
/// <returns>List{FileInfo}.</returns> /// <returns>List{FileInfo}.</returns>
internal List<FileInfo> GetEpisodeXmlFiles(Episode episode, string seriesDataPath) internal List<FileInfo> GetEpisodeXmlFiles(int? seasonNumber, int? episodeNumber, int? endingEpisodeNumber, string seriesDataPath)
{ {
var files = new List<FileInfo>(); var files = new List<FileInfo>();
if (episode.IndexNumber == null) if (episodeNumber == null)
{ {
return files; return files;
} }
var episodeNumber = episode.IndexNumber.Value;
var seasonNumber = episode.ParentIndexNumber;
if (seasonNumber == null) if (seasonNumber == null)
{ {
return files; return files;
@ -205,7 +130,7 @@ namespace MediaBrowser.Providers.TV
} }
} }
var end = episode.IndexNumberEnd ?? episodeNumber; var end = endingEpisodeNumber ?? episodeNumber;
episodeNumber++; episodeNumber++;
while (episodeNumber <= end) while (episodeNumber <= end)
@ -235,73 +160,35 @@ namespace MediaBrowser.Providers.TV
return files; return files;
} }
/// <summary>
/// Fetches metadata and returns true or false indicating if any work that requires persistence was done
/// </summary>
/// <param name="item">The item.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns>
public override async Task<bool> FetchAsync(BaseItem item, bool force, BaseProviderInfo providerInfo, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var status = ProviderRefreshStatus.Success;
var episode = (Episode)item;
var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
if (!string.IsNullOrEmpty(seriesId))
{
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(ConfigurationManager.ApplicationPaths, seriesId);
try
{
status = await FetchEpisodeData(episode, seriesDataPath, cancellationToken).ConfigureAwait(false);
}
catch (FileNotFoundException)
{
// Don't fail the provider because this will just keep on going and going.
}
}
SetLastRefreshed(item, DateTime.UtcNow, providerInfo, status);
return true;
}
/// <summary> /// <summary>
/// Fetches the episode data. /// Fetches the episode data.
/// </summary> /// </summary>
/// <param name="episode">The episode.</param> /// <param name="id">The identifier.</param>
/// <param name="seriesDataPath">The series data path.</param> /// <param name="seriesDataPath">The series data path.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.Boolean}.</returns> /// <returns>Task{System.Boolean}.</returns>
private async Task<ProviderRefreshStatus> FetchEpisodeData(Episode episode, string seriesDataPath, CancellationToken cancellationToken) private Episode FetchEpisodeData(EpisodeId id, string seriesDataPath, CancellationToken cancellationToken)
{ {
var status = ProviderRefreshStatus.Success; if (id.IndexNumber == null)
if (episode.IndexNumber == null)
{ {
return status; return null;
} }
var episodeNumber = episode.IndexNumber.Value; var episodeNumber = id.IndexNumber.Value;
var seasonNumber = episode.ParentIndexNumber; var seasonNumber = id.ParentIndexNumber;
if (seasonNumber == null) if (seasonNumber == null)
{ {
return status; return null;
} }
var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber)); var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber));
var success = false; var success = false;
var usingAbsoluteData = false; var usingAbsoluteData = false;
var episode = new Episode();
try try
{ {
status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); FetchMainEpisodeInfo(episode, file, cancellationToken);
success = true; success = true;
} }
@ -318,11 +205,11 @@ namespace MediaBrowser.Providers.TV
{ {
file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber)); file = Path.Combine(seriesDataPath, string.Format("episode-abs-{0}.xml", episodeNumber));
status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false); FetchMainEpisodeInfo(episode, file, cancellationToken);
usingAbsoluteData = true; usingAbsoluteData = true;
} }
var end = episode.IndexNumberEnd ?? episodeNumber; var end = id.IndexNumberEnd ?? episodeNumber;
episodeNumber++; episodeNumber++;
while (episodeNumber <= end) while (episodeNumber <= end)
@ -348,12 +235,12 @@ namespace MediaBrowser.Providers.TV
episodeNumber++; episodeNumber++;
} }
return status; return success ? episode : null;
} }
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private async Task<ProviderRefreshStatus> FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken) private void FetchMainEpisodeInfo(Episode item, string xmlFile, CancellationToken cancellationToken)
{ {
var status = ProviderRefreshStatus.Success; var status = ProviderRefreshStatus.Success;
@ -506,7 +393,7 @@ namespace MediaBrowser.Providers.TV
item.AirsBeforeSeasonNumber = rval; item.AirsBeforeSeasonNumber = rval;
} }
} }
break; break;
} }
@ -534,7 +421,7 @@ namespace MediaBrowser.Providers.TV
{ {
var url = TVUtils.BannerUrl + val; var url = TVUtils.BannerUrl + val;
await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false); //await _providerManager.SaveImage(item, url, TvdbSeriesProvider.Current.TvDbResourcePool, ImageType.Primary, null, cancellationToken).ConfigureAwait(false);
} }
catch (HttpException) catch (HttpException)
{ {
@ -661,8 +548,6 @@ namespace MediaBrowser.Providers.TV
} }
} }
} }
return status;
} }
private void AddPeople(BaseItem item, string val, string personType) private void AddPeople(BaseItem item, string val, string personType)
@ -802,15 +687,5 @@ namespace MediaBrowser.Providers.TV
} }
} }
} }
/// <summary>
/// Determines whether [has local meta] [the specified episode].
/// </summary>
/// <param name="episode">The episode.</param>
/// <returns><c>true</c> if [has local meta] [the specified episode]; otherwise, <c>false</c>.</returns>
private bool HasLocalMeta(BaseItem episode)
{
return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml"));
}
} }
} }

@ -1,10 +1,8 @@
using MediaBrowser.Common.IO; using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging; using MediaBrowser.Model.Logging;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;

@ -188,8 +188,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
FileName = FFProbePath, FileName = FFProbePath,
Arguments = Arguments = string.Format(
string.Format(
"{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format", "{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format",
probeSizeArgument, inputPath).Trim(), probeSizeArgument, inputPath).Trim(),
@ -830,6 +829,28 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
await ExtractImageInternal(inputArgument, type, threedFormat, offset, outputPath, false, resourcePool, cancellationToken).ConfigureAwait(false); await ExtractImageInternal(inputArgument, type, threedFormat, offset, outputPath, false, resourcePool, cancellationToken).ConfigureAwait(false);
} }
public async Task<Stream> ExtractImage(string[] inputFiles, InputType type, bool isAudio,
Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken)
{
var resourcePool = isAudio ? _audioImageResourcePool : _videoImageResourcePool;
var inputArgument = GetInputArgument(inputFiles, type);
if (!isAudio)
{
try
{
return await ExtractImageInternal(inputArgument, type, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false);
}
catch
{
_logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument);
}
}
return await ExtractImageInternal(inputArgument, type, threedFormat, offset, false, resourcePool, cancellationToken).ConfigureAwait(false);
}
/// <summary> /// <summary>
/// Extracts the image. /// Extracts the image.
/// </summary> /// </summary>
@ -958,13 +979,129 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
} }
} }
private async Task<Stream> ExtractImageInternal(string inputPath, InputType type, Video3DFormat? threedFormat, TimeSpan? offset, bool useIFrame, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(inputPath))
{
throw new ArgumentNullException("inputPath");
}
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600.
// This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar
var vf = "crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,scale=600:600/dar";
if (threedFormat.HasValue)
{
switch (threedFormat.Value)
{
case Video3DFormat.HalfSideBySide:
vf = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,scale=600:600/dar";
// hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
break;
case Video3DFormat.FullSideBySide:
vf = "crop=iw/2:ih:0:0,setdar=dar=a,,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,scale=600:600/dar";
//fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to 600.
break;
case Video3DFormat.HalfTopAndBottom:
vf = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,scale=600:600/dar";
//htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600
break;
case Video3DFormat.FullTopAndBottom:
vf = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,scale=600:600/dar";
// ftab crop heigt in half, set the display aspect,crop out any black bars we may have made the scale width to 600
break;
}
}
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
var args = useIFrame ? string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"thumbnail,{2}\" -f image2 \"{1}\"", inputPath, "-", vf) :
string.Format("-i {0} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, "-", vf);
var probeSize = GetProbeSizeArgument(type);
if (!string.IsNullOrEmpty(probeSize))
{
args = probeSize + " " + args;
}
if (offset.HasValue)
{
args = string.Format("-ss {0} ", Convert.ToInt32(offset.Value.TotalSeconds)).ToString(UsCulture) + args;
}
var process = new Process
{
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
FileName = FFMpegPath,
Arguments = args,
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
process.Start();
var memoryStream = new MemoryStream();
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
process.StandardOutput.BaseStream.CopyToAsync(memoryStream);
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
process.BeginErrorReadLine();
var ranToCompletion = process.WaitForExit(10000);
if (!ranToCompletion)
{
try
{
_logger.Info("Killing ffmpeg process");
process.Kill();
process.WaitForExit(1000);
}
catch (Exception ex)
{
_logger.ErrorException("Error killing process", ex);
}
}
resourcePool.Release();
var exitCode = ranToCompletion ? process.ExitCode : -1;
process.Dispose();
if (exitCode == -1 || memoryStream.Length == 0)
{
memoryStream.Dispose();
var msg = string.Format("ffmpeg image extraction failed for {0}", inputPath);
_logger.Error(msg);
throw new ApplicationException(msg);
}
memoryStream.Position = 0;
return memoryStream;
}
/// <summary> /// <summary>
/// Starts the and wait for process. /// Starts the and wait for process.
/// </summary> /// </summary>
/// <param name="process">The process.</param> /// <param name="process">The process.</param>
/// <param name="timeout">The timeout.</param> /// <param name="timeout">The timeout.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
private bool StartAndWaitForProcess(Process process, int timeout = 12000) private bool StartAndWaitForProcess(Process process, int timeout = 10000)
{ {
process.Start(); process.Start();

Loading…
Cancel
Save