convert episode providers to new system
This commit is contained in:
parent
351cfef7a7
commit
04d62d3420
|
@ -11,13 +11,16 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace MediaBrowser.Api
|
||||
{
|
||||
[Route("/Items/{Id}/Refresh", "POST")]
|
||||
[Api(Description = "Refreshes metadata for an item")]
|
||||
public class RefreshItem : IReturnVoid
|
||||
public class BaseRefreshRequest : IReturnVoid
|
||||
{
|
||||
[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; }
|
||||
}
|
||||
|
||||
[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")]
|
||||
public bool Recursive { get; set; }
|
||||
|
||||
|
@ -27,66 +30,48 @@ namespace MediaBrowser.Api
|
|||
|
||||
[Route("/Artists/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Genres/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/MusicGenres/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/GameGenres/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Persons/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Studios/{Name}/Refresh", "POST")]
|
||||
[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")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
@ -132,11 +117,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -157,11 +138,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -182,11 +159,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -207,11 +180,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -232,11 +201,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -257,11 +222,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -291,11 +252,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
try
|
||||
{
|
||||
await item.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.RefreshMetadata(GetRefreshOptions(request), CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
if (item.IsFolder)
|
||||
{
|
||||
|
@ -328,13 +285,11 @@ namespace MediaBrowser.Api
|
|||
/// <returns>Task.</returns>
|
||||
private async Task RefreshCollectionFolderChildren(RefreshItem request, CollectionFolder collectionFolder)
|
||||
{
|
||||
var options = GetRefreshOptions(request);
|
||||
|
||||
foreach (var child in collectionFolder.Children.ToList())
|
||||
{
|
||||
await child.RefreshMetadata(new MetadataRefreshOptions
|
||||
{
|
||||
ReplaceAllMetadata = request.Forced,
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
await child.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
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.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
|
@ -57,6 +57,12 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
/// </summary>
|
||||
/// <value>The absolute episode number.</value>
|
||||
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>
|
||||
/// We want to group into series not show individually in an index
|
||||
|
@ -89,7 +95,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
return value;
|
||||
}
|
||||
|
||||
var season = Parent as Season;
|
||||
var season = Season;
|
||||
|
||||
return season != null ? season.IndexNumber : null;
|
||||
}
|
||||
|
@ -140,10 +146,6 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
get { return Series != null ? Series.CustomRatingForComparison : base.CustomRatingForComparison; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The _series
|
||||
/// </summary>
|
||||
private Series _series;
|
||||
/// <summary>
|
||||
/// This Episode's Series Instance
|
||||
/// </summary>
|
||||
|
@ -151,14 +153,14 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
[IgnoreDataMember]
|
||||
public Series Series
|
||||
{
|
||||
get { return _series ?? (_series = FindParent<Series>()); }
|
||||
get { return FindParent<Series>(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the ending episode number for double episodes.
|
||||
/// </summary>
|
||||
/// <value>The index number.</value>
|
||||
public int? IndexNumberEnd { get; set; }
|
||||
[IgnoreDataMember]
|
||||
public Season Season
|
||||
{
|
||||
get { return FindParent<Season>(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the name of the sort.
|
||||
|
@ -217,7 +219,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
get
|
||||
{
|
||||
// First see if the parent is a Season
|
||||
var season = Parent as Season;
|
||||
var season = Season;
|
||||
|
||||
if (season != null)
|
||||
{
|
||||
|
@ -229,7 +231,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||
// Parent is a Series
|
||||
if (seasonNumber.HasValue)
|
||||
{
|
||||
var series = Parent as Series;
|
||||
var series = Series;
|
||||
|
||||
if (series != null)
|
||||
{
|
||||
|
|
|
@ -144,7 +144,6 @@
|
|||
<Compile Include="Persistence\IFileOrganizationRepository.cs" />
|
||||
<Compile Include="Persistence\MediaStreamQuery.cs" />
|
||||
<Compile Include="Providers\ICustomMetadataProvider.cs" />
|
||||
<Compile Include="Providers\IDynamicInfoProvider.cs" />
|
||||
<Compile Include="Providers\IHasChangeMonitor.cs" />
|
||||
<Compile Include="Providers\IHasMetadata.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.Tasks;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaInfo
|
||||
{
|
||||
|
@ -35,6 +36,18 @@ namespace MediaBrowser.Controller.MediaInfo
|
|||
/// <returns>Task.</returns>
|
||||
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>
|
||||
/// Extracts the text subtitle.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Threading;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Providers
|
||||
|
@ -10,6 +11,6 @@ namespace MediaBrowser.Controller.Providers
|
|||
public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider
|
||||
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>
|
||||
string GetPreferredMetadataCountryCode();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the date modified.
|
||||
/// </summary>
|
||||
/// <value>The date modified.</value>
|
||||
DateTime DateModified { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the locked fields.
|
||||
/// </summary>
|
||||
|
|
|
@ -26,7 +26,7 @@ namespace MediaBrowser.Controller.Providers
|
|||
public ImageType Type { get; set; }
|
||||
}
|
||||
|
||||
public interface IDynamicImageProvider : ILocalImageProvider
|
||||
public interface IDynamicImageProvider : IImageProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported images.
|
||||
|
|
|
@ -31,6 +31,8 @@ namespace MediaBrowser.Controller.Providers
|
|||
/// </summary>
|
||||
/// <value>The year.</value>
|
||||
public int? Year { get; set; }
|
||||
public int? IndexNumber { get; set; }
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
|
||||
public ItemId()
|
||||
{
|
||||
|
@ -45,11 +47,17 @@ namespace MediaBrowser.Controller.Providers
|
|||
/// </summary>
|
||||
/// <value>The album artist.</value>
|
||||
public string AlbumArtist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the artist music brainz identifier.
|
||||
/// Gets or sets the artist provider ids.
|
||||
/// </summary>
|
||||
/// <value>The artist music brainz identifier.</value>
|
||||
public string ArtistMusicBrainzId { get; set; }
|
||||
/// <value>The artist provider ids.</value>
|
||||
public Dictionary<string, string> ArtistProviderIds { get; set; }
|
||||
|
||||
public AlbumId()
|
||||
{
|
||||
ArtistProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public class GameId : ItemId
|
||||
|
@ -69,4 +77,16 @@ namespace MediaBrowser.Controller.Providers
|
|||
/// <value>The path.</value>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
59
MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs
Normal file
59
MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs
Normal file
|
@ -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.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
@ -230,13 +231,23 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
protected virtual TIdType GetId(TItemType item)
|
||||
{
|
||||
return new TIdType
|
||||
var id = new TIdType
|
||||
{
|
||||
MetadataCountryCode = item.GetPreferredMetadataCountryCode(),
|
||||
MetadataLanguage = item.GetPreferredMetadataLanguage(),
|
||||
Name = item.Name,
|
||||
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)
|
||||
|
@ -253,6 +264,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
};
|
||||
|
||||
var temp = CreateNew();
|
||||
temp.Path = item.Path;
|
||||
|
||||
// If replacing all metadata, run internet providers first
|
||||
if (options.ReplaceAllMetadata)
|
||||
|
@ -313,29 +325,32 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
foreach (var provider in providers.OfType<ICustomMetadataProvider<TItemType>>())
|
||||
{
|
||||
Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
|
||||
|
||||
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);
|
||||
}
|
||||
await RunCustomProvider(provider, item, refreshResult, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
return new TItemType();
|
||||
|
|
|
@ -246,11 +246,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Don't clog up the log with these providers
|
||||
if (!(provider is IDynamicInfoProvider))
|
||||
{
|
||||
_logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
|
||||
}
|
||||
_logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name ?? "--Unknown--");
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -637,7 +633,7 @@ namespace MediaBrowser.Providers.Manager
|
|||
}));
|
||||
|
||||
// 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,
|
||||
Type = MetadataPluginType.MetadataFetcher
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdultVideos\AdultVideoMetadataService.cs" />
|
||||
<Compile Include="All\LocalImageProvider.cs" />
|
||||
<Compile Include="Books\BookMetadataService.cs" />
|
||||
<Compile Include="BoxSets\BoxSetMetadataService.cs" />
|
||||
|
@ -89,7 +90,11 @@
|
|||
<Compile Include="Games\GameSystemXmlProvider.cs" />
|
||||
<Compile Include="ImageFromMediaLocationProvider.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\MovieXmlProvider.cs" />
|
||||
<Compile Include="MusicGenres\MusicGenreImageProvider.cs" />
|
||||
<Compile Include="GameGenres\GameGenreImageProvider.cs" />
|
||||
<Compile Include="Genres\GenreImageProvider.cs" />
|
||||
|
@ -107,6 +112,8 @@
|
|||
<Compile Include="Music\ArtistMetadataService.cs" />
|
||||
<Compile Include="Music\LastfmArtistProvider.cs" />
|
||||
<Compile Include="Music\MusicBrainzArtistProvider.cs" />
|
||||
<Compile Include="Music\MusicVideoMetadataService.cs" />
|
||||
<Compile Include="Music\MusicVideoXmlProvider.cs" />
|
||||
<Compile Include="Omdb\OmdbProvider.cs" />
|
||||
<Compile Include="Omdb\OmdbSeriesProvider.cs" />
|
||||
<Compile Include="People\MovieDbPersonImageProvider.cs" />
|
||||
|
@ -150,9 +157,9 @@
|
|||
<Compile Include="Savers\XmlSaverHelpers.cs" />
|
||||
<Compile Include="Studios\StudiosImageProvider.cs" />
|
||||
<Compile Include="Studios\StudioMetadataService.cs" />
|
||||
<Compile Include="TV\EpisodeImageFromMediaLocationProvider.cs" />
|
||||
<Compile Include="TV\EpisodeIndexNumberProvider.cs" />
|
||||
<Compile Include="TV\EpisodeProviderFromXml.cs" />
|
||||
<Compile Include="TV\EpisodeLocalImageProvider.cs" />
|
||||
<Compile Include="TV\EpisodeMetadataService.cs" />
|
||||
<Compile Include="TV\EpisodeXmlProvider.cs" />
|
||||
<Compile Include="TV\EpisodeXmlParser.cs" />
|
||||
<Compile Include="TV\FanArtTvUpdatesPrescanTask.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();
|
||||
|
||||
NormalizeFFProbeResult(result);
|
||||
FFProbeHelpers.NormalizeFFProbeResult(result);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
@ -102,7 +102,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <param name="tags">The tags.</param>
|
||||
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
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
|
@ -114,7 +114,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
audio.People.Clear();
|
||||
|
||||
var composer = GetDictionaryValue(tags, "composer");
|
||||
var composer = FFProbeHelpers.GetDictionaryValue(tags, "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))
|
||||
{
|
||||
|
@ -142,7 +142,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
}
|
||||
|
||||
// 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
|
||||
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
|
||||
|
@ -150,10 +150,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
|
||||
|
||||
audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
|
||||
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// 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 (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
|
@ -219,7 +219,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <param name="tagName">Name of the tag.</param>
|
||||
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))
|
||||
{
|
||||
|
@ -240,7 +240,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Audio audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = GetDictionaryValue(tags, "genre");
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
|
@ -261,7 +261,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var disc = GetDictionaryValue(tags, tagName);
|
||||
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
|
|
113
MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs
Normal file
113
MediaBrowser.Providers/MediaInfo/FFProbeHelpers.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
103
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
Normal file
103
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
587
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
Normal file
587
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
Normal file
|
@ -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();
|
||||
|
||||
NormalizeFFProbeResult(result);
|
||||
FFProbeHelpers.NormalizeFFProbeResult(result);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
@ -417,7 +417,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
@ -428,7 +428,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
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))
|
||||
{
|
||||
|
@ -443,7 +443,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
{
|
||||
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))
|
||||
{
|
||||
|
@ -457,7 +457,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
|
@ -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.Library;
|
||||
using MediaBrowser.Controller.MediaInfo;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 IMediaEncoder _mediaEncoder;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public VideoImageProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IMediaEncoder mediaEncoder, IIsoManager isoManager)
|
||||
: base(logManager, configurationManager)
|
||||
public VideoImageProvider(IIsoManager isoManager, IMediaEncoder mediaEncoder, IServerConfigurationManager config)
|
||||
{
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_isoManager = isoManager;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -94,16 +32,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
private bool QualifiesForExtraction(Video item)
|
||||
{
|
||||
if (!ConfigurationManager.Configuration.EnableVideoImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.PrimaryImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// No support for this
|
||||
if (item.VideoType == VideoType.HdDvd)
|
||||
{
|
||||
|
@ -125,148 +53,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Override this to return the date that should be compared to the last refresh date
|
||||
/// 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>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Last; }
|
||||
}
|
||||
|
||||
public override ItemUpdateType ItemUpdateType
|
||||
{
|
||||
get
|
||||
{
|
||||
return ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
item.ValidateImages();
|
||||
|
||||
var video = (Video)item;
|
||||
|
||||
// Double check this here in case force was used
|
||||
if (QualifiesForExtraction(video))
|
||||
{
|
||||
try
|
||||
{
|
||||
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 true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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();
|
||||
|
||||
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
|
||||
item.SetImagePath(ImageType.Primary, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
|
||||
try
|
||||
{
|
||||
// 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
|
||||
var imageOffset = video.VideoType != VideoType.Dvd && video.RunTimeTicks.HasValue &&
|
||||
video.RunTimeTicks.Value > 0
|
||||
? TimeSpan.FromTicks(Convert.ToInt64(video.RunTimeTicks.Value * .1))
|
||||
: TimeSpan.FromSeconds(10);
|
||||
|
||||
InputType type;
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(video.Path, video.LocationType == LocationType.Remote, video.VideoType, video.IsoType, isoMount, video.PlayableStreamFileNames, out type);
|
||||
|
||||
await _mediaEncoder.ExtractImage(inputPath, type, false, video.Video3DFormat, imageOffset, path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
video.SetImagePath(ImageType.Primary, path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The null mount task result
|
||||
/// </summary>
|
||||
|
@ -288,42 +74,64 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
return NullMountTaskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>SemaphoreSlim.</returns>
|
||||
private SemaphoreSlim GetLock(string filename)
|
||||
public IEnumerable<ImageType> GetSupportedImages(IHasImages item)
|
||||
{
|
||||
return _locks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
|
||||
return new List<ImageType> { ImageType.Primary };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video images data path.
|
||||
/// </summary>
|
||||
/// <value>The video images data path.</value>
|
||||
public string VideoImagesPath
|
||||
public Task<DynamicImageResponse> GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken)
|
||||
{
|
||||
get
|
||||
return GetVideoImage((Video)item, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
|
||||
{
|
||||
var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
return Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "extracted-video-images");
|
||||
// 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
|
||||
var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
|
||||
item.RunTimeTicks.Value > 0
|
||||
? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1))
|
||||
: TimeSpan.FromSeconds(10);
|
||||
|
||||
InputType type;
|
||||
|
||||
var inputPath = MediaEncoderHelpers.GetInputArgument(item.Path, item.LocationType == LocationType.Remote, item.VideoType, item.IsoType, isoMount, item.PlayableStreamFileNames, out type);
|
||||
|
||||
var stream = await _mediaEncoder.ExtractImage(inputPath, type, false, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return new DynamicImageResponse
|
||||
{
|
||||
Format = ImageFormat.Jpg,
|
||||
HasImage = true,
|
||||
Stream = stream
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (isoMount != null)
|
||||
{
|
||||
isoMount.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the audio image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetVideoImagePath(Video item)
|
||||
public string Name
|
||||
{
|
||||
var filename = item.Path + "_" + item.DateModified.Ticks + "_primary";
|
||||
get { return "Embedded Image"; }
|
||||
}
|
||||
|
||||
filename = filename.GetMD5() + ".jpg";
|
||||
public bool Supports(IHasImages item)
|
||||
{
|
||||
if (!_config.Configuration.EnableVideoImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prefix = filename.Substring(0, 1);
|
||||
|
||||
return Path.Combine(VideoImagesPath, prefix, filename);
|
||||
return item.LocationType == LocationType.FileSystem && item is Video;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -371,6 +371,11 @@ namespace MediaBrowser.Providers.Movies
|
|||
{
|
||||
var path = GetDataFilePath(item);
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return _cachedTask;
|
||||
}
|
||||
|
||||
var fileInfo = _fileSystem.GetFileSystemInfo(path);
|
||||
|
||||
if (fileInfo.Exists)
|
||||
|
|
|
@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.Movies
|
|||
|
||||
try
|
||||
{
|
||||
await new MovieXmlParser(Logger, _itemRepo).FetchAsync(video, path, cancellationToken).ConfigureAwait(false);
|
||||
new MovieXmlParser(Logger).FetchAsync(video, path, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
|
||||
namespace MediaBrowser.Providers.Movies
|
||||
|
@ -14,28 +12,14 @@ namespace MediaBrowser.Providers.Movies
|
|||
/// </summary>
|
||||
public class MovieXmlParser : BaseItemXmlParser<Video>
|
||||
{
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
private Task _chaptersTask = null;
|
||||
|
||||
public MovieXmlParser(ILogger logger, IItemRepository itemRepo)
|
||||
public MovieXmlParser(ILogger 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);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_chaptersTask != null)
|
||||
{
|
||||
await _chaptersTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
78
MediaBrowser.Providers/Movies/MovieXmlProvider.cs
Normal file
78
MediaBrowser.Providers/Movies/MovieXmlProvider.cs
Normal file
|
@ -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)
|
||||
{
|
||||
id.ArtistMusicBrainzId = artist.GetProviderId(MetadataProviders.Musicbrainz);
|
||||
id.ArtistProviderIds = artist.ProviderIds;
|
||||
id.AlbumArtist = id.AlbumArtist ?? artist.Name;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,10 @@ namespace MediaBrowser.Providers.Music
|
|||
|
||||
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();
|
||||
|
||||
|
|
53
MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
Normal file
53
MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
60
MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs
Normal file
60
MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs
Normal file
|
@ -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.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
|
@ -26,13 +27,13 @@ namespace MediaBrowser.Providers.Omdb
|
|||
_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);
|
||||
|
||||
if (string.IsNullOrEmpty(imdbId))
|
||||
{
|
||||
return;
|
||||
return ItemUpdateType.Unspecified;
|
||||
}
|
||||
|
||||
var imdbParam = imdbId.StartsWith("tt", StringComparison.OrdinalIgnoreCase) ? imdbId : "tt" + imdbId;
|
||||
|
@ -97,6 +98,8 @@ namespace MediaBrowser.Providers.Omdb
|
|||
|
||||
ParseAdditionalMetadata(item, result);
|
||||
}
|
||||
|
||||
return ItemUpdateType.MetadataDownload;
|
||||
}
|
||||
|
||||
private void ParseAdditionalMetadata(BaseItem item, RootObject result)
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System.Threading;
|
||||
|
@ -7,7 +10,8 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace MediaBrowser.Providers.Omdb
|
||||
{
|
||||
public class OmdbSeriesProvider : ICustomMetadataProvider<Series>
|
||||
public class OmdbSeriesProvider : ICustomMetadataProvider<Series>,
|
||||
ICustomMetadataProvider<Movie>, ICustomMetadataProvider<Trailer>
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
@ -18,14 +22,30 @@ namespace MediaBrowser.Providers.Omdb
|
|||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public Task FetchAsync(Series item, CancellationToken cancellationToken)
|
||||
{
|
||||
return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "OMDb"; }
|
||||
}
|
||||
|
||||
public Task<ItemUpdateType> FetchAsync(Series item, CancellationToken cancellationToken)
|
||||
{
|
||||
return new OmdbProvider(_jsonSerializer, _httpClient).Fetch(item, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<ItemUpdateType> FetchAsync(Movie item, CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
MergeBudget(source, target, lockedFields, replaceData);
|
||||
|
||||
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.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
59
MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs
Normal file
59
MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
137
MediaBrowser.Providers/TV/EpisodeMetadataService.cs
Normal file
137
MediaBrowser.Providers/TV/EpisodeMetadataService.cs
Normal file
|
@ -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.Persistence;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
|
||||
namespace MediaBrowser.Providers.TV
|
||||
|
@ -17,28 +15,14 @@ namespace MediaBrowser.Providers.TV
|
|||
/// </summary>
|
||||
public class EpisodeXmlParser : BaseItemXmlParser<Episode>
|
||||
{
|
||||
private readonly IItemRepository _itemRepo;
|
||||
|
||||
private Task _chaptersTask = null;
|
||||
|
||||
public EpisodeXmlParser(ILogger logger, IItemRepository itemRepo)
|
||||
public EpisodeXmlParser(ILogger 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);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_chaptersTask != null)
|
||||
{
|
||||
await _chaptersTask.ConfigureAwait(false);
|
||||
}
|
||||
Fetch(item, metadataFile, cancellationToken);
|
||||
}
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
|
62
MediaBrowser.Providers/TV/EpisodeXmlProvider.cs
Normal file
62
MediaBrowser.Providers/TV/EpisodeXmlProvider.cs
Normal file
|
@ -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.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
|
@ -6,6 +7,7 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -17,16 +19,18 @@ using System.Xml;
|
|||
|
||||
namespace MediaBrowser.Providers.TV
|
||||
{
|
||||
public class TvdbEpisodeImageProvider : IRemoteImageProvider
|
||||
public class TvdbEpisodeImageProvider : IRemoteImageProvider, IHasChangeMonitor
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient)
|
||||
public TvdbEpisodeImageProvider(IServerConfigurationManager config, IHttpClient httpClient, IFileSystem fileSystem)
|
||||
{
|
||||
_config = config;
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public string Name
|
||||
|
@ -65,7 +69,7 @@ namespace MediaBrowser.Providers.TV
|
|||
// Process images
|
||||
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))
|
||||
.Where(i => i != null);
|
||||
|
@ -186,5 +190,27 @@ namespace MediaBrowser.Providers.TV
|
|||
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.Net;
|
||||
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 MediaBrowser.Model.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
@ -25,161 +23,88 @@ namespace MediaBrowser.Providers.TV
|
|||
/// <summary>
|
||||
/// Class RemoteEpisodeProvider
|
||||
/// </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;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
public TvdbEpisodeProvider(IFileSystem fileSystem, IServerConfigurationManager config)
|
||||
{
|
||||
HttpClient = httpClient;
|
||||
_providerManager = providerManager;
|
||||
_fileSystem = fileSystem;
|
||||
_config = config;
|
||||
Current = this;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public string Name
|
||||
{
|
||||
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;
|
||||
|
||||
string seriesTvdbId;
|
||||
episodeId.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out seriesTvdbId);
|
||||
|
||||
var result = new MetadataResult<Episode>();
|
||||
|
||||
if (!string.IsNullOrEmpty(seriesTvdbId))
|
||||
{
|
||||
return ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataDownload;
|
||||
}
|
||||
}
|
||||
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesTvdbId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override MetadataProviderPriority Priority
|
||||
{
|
||||
get { return MetadataProviderPriority.Third; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// 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 "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))
|
||||
try
|
||||
{
|
||||
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 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))
|
||||
{
|
||||
// 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.Select(i => _fileSystem.GetLastWriteTimeUtc(i)).Max() > providerInfo.LastRefreshed;
|
||||
}
|
||||
return files.Any(i => _fileSystem.GetLastWriteTimeUtc(i) > date);
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the episode XML files.
|
||||
/// </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>
|
||||
/// <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>();
|
||||
|
||||
if (episode.IndexNumber == null)
|
||||
if (episodeNumber == null)
|
||||
{
|
||||
return files;
|
||||
}
|
||||
|
||||
var episodeNumber = episode.IndexNumber.Value;
|
||||
var seasonNumber = episode.ParentIndexNumber;
|
||||
|
||||
if (seasonNumber == null)
|
||||
{
|
||||
return files;
|
||||
|
@ -205,7 +130,7 @@ namespace MediaBrowser.Providers.TV
|
|||
}
|
||||
}
|
||||
|
||||
var end = episode.IndexNumberEnd ?? episodeNumber;
|
||||
var end = endingEpisodeNumber ?? episodeNumber;
|
||||
episodeNumber++;
|
||||
|
||||
while (episodeNumber <= end)
|
||||
|
@ -235,73 +160,35 @@ namespace MediaBrowser.Providers.TV
|
|||
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>
|
||||
/// Fetches the episode data.
|
||||
/// </summary>
|
||||
/// <param name="episode">The episode.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="seriesDataPath">The series data path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <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 (episode.IndexNumber == null)
|
||||
if (id.IndexNumber == null)
|
||||
{
|
||||
return status;
|
||||
return null;
|
||||
}
|
||||
|
||||
var episodeNumber = episode.IndexNumber.Value;
|
||||
var seasonNumber = episode.ParentIndexNumber;
|
||||
var episodeNumber = id.IndexNumber.Value;
|
||||
var seasonNumber = id.ParentIndexNumber;
|
||||
|
||||
if (seasonNumber == null)
|
||||
{
|
||||
return status;
|
||||
return null;
|
||||
}
|
||||
|
||||
var file = Path.Combine(seriesDataPath, string.Format("episode-{0}-{1}.xml", seasonNumber.Value, episodeNumber));
|
||||
var success = false;
|
||||
var usingAbsoluteData = false;
|
||||
|
||||
var episode = new Episode();
|
||||
try
|
||||
{
|
||||
status = await FetchMainEpisodeInfo(episode, file, cancellationToken).ConfigureAwait(false);
|
||||
FetchMainEpisodeInfo(episode, file, cancellationToken);
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
@ -318,11 +205,11 @@ namespace MediaBrowser.Providers.TV
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
||||
var end = episode.IndexNumberEnd ?? episodeNumber;
|
||||
var end = id.IndexNumberEnd ?? episodeNumber;
|
||||
episodeNumber++;
|
||||
|
||||
while (episodeNumber <= end)
|
||||
|
@ -348,12 +235,12 @@ namespace MediaBrowser.Providers.TV
|
|||
episodeNumber++;
|
||||
}
|
||||
|
||||
return status;
|
||||
return success ? episode : null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -506,7 +393,7 @@ namespace MediaBrowser.Providers.TV
|
|||
item.AirsBeforeSeasonNumber = rval;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -534,7 +421,7 @@ namespace MediaBrowser.Providers.TV
|
|||
{
|
||||
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)
|
||||
{
|
||||
|
@ -661,8 +548,6 @@ namespace MediaBrowser.Providers.TV
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
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.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
|
|
|
@ -188,8 +188,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
FileName = FFProbePath,
|
||||
Arguments =
|
||||
string.Format(
|
||||
Arguments = string.Format(
|
||||
"{0} -i {1} -threads 0 -v info -print_format json -show_streams -show_format",
|
||||
probeSizeArgument, inputPath).Trim(),
|
||||
|
||||
|
@ -830,6 +829,28 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
|||
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>
|
||||
/// Extracts the image.
|
||||
/// </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>
|
||||
/// Starts the and wait for process.
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
/// <param name="timeout">The timeout.</param>
|
||||
/// <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();
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user