aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/Providers
diff options
context:
space:
mode:
authorLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
committerLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
commit767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch)
tree49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/Providers
parent845554722efaed872948a9e0f7202e3ef52f1b6e (diff)
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/Providers')
-rw-r--r--MediaBrowser.Controller/Providers/AudioInfoProvider.cs262
-rw-r--r--MediaBrowser.Controller/Providers/BaseImageEnhancer.cs113
-rw-r--r--MediaBrowser.Controller/Providers/BaseItemXmlParser.cs1332
-rw-r--r--MediaBrowser.Controller/Providers/BaseMetadataProvider.cs512
-rw-r--r--MediaBrowser.Controller/Providers/BaseProviderInfo.cs75
-rw-r--r--MediaBrowser.Controller/Providers/FanartBaseProvider.cs84
-rw-r--r--MediaBrowser.Controller/Providers/FolderProviderFromXml.cs121
-rw-r--r--MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs359
-rw-r--r--MediaBrowser.Controller/Providers/ImagesByNameProvider.cs103
-rw-r--r--MediaBrowser.Controller/Providers/LocalTrailerProvider.cs47
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs265
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs17
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs74
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs358
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs84
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs137
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs208
-rw-r--r--MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs291
-rw-r--r--MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs220
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs1607
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs100
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs134
-rw-r--r--MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs45
-rw-r--r--MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs113
-rw-r--r--MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs465
-rw-r--r--MediaBrowser.Controller/Providers/ProviderManager.cs332
-rw-r--r--MediaBrowser.Controller/Providers/SortNameProvider.cs129
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs194
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs181
-rw-r--r--MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs164
-rw-r--r--MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs140
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs288
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs287
-rw-r--r--MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs545
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs122
-rw-r--r--MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs159
-rw-r--r--MediaBrowser.Controller/Providers/VideoInfoProvider.cs168
37 files changed, 7970 insertions, 1865 deletions
diff --git a/MediaBrowser.Controller/Providers/AudioInfoProvider.cs b/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
deleted file mode 100644
index 302902646e..0000000000
--- a/MediaBrowser.Controller/Providers/AudioInfoProvider.cs
+++ /dev/null
@@ -1,262 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.FFMpeg;
-using MediaBrowser.Controller.Library;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- [Export(typeof(BaseMetadataProvider))]
- public class AudioInfoProvider : BaseMediaInfoProvider<Audio>
- {
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- protected override string CacheDirectory
- {
- get { return Kernel.Instance.ApplicationPaths.FFProbeAudioCacheDirectory; }
- }
-
- protected override void Fetch(Audio audio, FFProbeResult data)
- {
- MediaStream stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
-
- audio.Channels = stream.channels;
-
- if (!string.IsNullOrEmpty(stream.sample_rate))
- {
- audio.SampleRate = int.Parse(stream.sample_rate);
- }
-
- string bitrate = stream.bit_rate;
- string duration = stream.duration;
-
- if (string.IsNullOrEmpty(bitrate))
- {
- bitrate = data.format.bit_rate;
- }
-
- if (string.IsNullOrEmpty(duration))
- {
- duration = data.format.duration;
- }
-
- if (!string.IsNullOrEmpty(bitrate))
- {
- audio.BitRate = int.Parse(bitrate);
- }
-
- if (!string.IsNullOrEmpty(duration))
- {
- audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
- }
-
- if (data.format.tags != null)
- {
- FetchDataFromTags(audio, data.format.tags);
- }
- }
-
- private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
- {
- string title = GetDictionaryValue(tags, "title");
-
- if (!string.IsNullOrEmpty(title))
- {
- audio.Name = title;
- }
-
- string composer = GetDictionaryValue(tags, "composer");
-
- if (!string.IsNullOrEmpty(composer))
- {
- audio.AddPerson(new PersonInfo { Name = composer, Type = "Composer" });
- }
-
- audio.Album = GetDictionaryValue(tags, "album");
- audio.Artist = GetDictionaryValue(tags, "artist");
- audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
-
- audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
- audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
-
- audio.Language = GetDictionaryValue(tags, "language");
-
- audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
-
- audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
-
- FetchGenres(audio, tags);
-
- FetchStudios(audio, tags, "organization");
- FetchStudios(audio, tags, "ensemble");
- FetchStudios(audio, tags, "publisher");
- }
-
- private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
- {
- string val = GetDictionaryValue(tags, tagName);
-
- if (!string.IsNullOrEmpty(val))
- {
- var list = audio.Studios ?? new List<string>();
- list.AddRange(val.Split('/'));
- audio.Studios = list;
- }
- }
-
- private void FetchGenres(Audio audio, Dictionary<string, string> tags)
- {
- string val = GetDictionaryValue(tags, "genre");
-
- if (!string.IsNullOrEmpty(val))
- {
- var list = audio.Genres ?? new List<string>();
- list.AddRange(val.Split('/'));
- audio.Genres = list;
- }
- }
-
- private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
- {
- string disc = GetDictionaryValue(tags, "disc");
-
- if (!string.IsNullOrEmpty(disc))
- {
- disc = disc.Split('/')[0];
-
- int num;
-
- if (int.TryParse(disc, out num))
- {
- return num;
- }
- }
-
- return null;
- }
- }
-
- public abstract class BaseMediaInfoProvider<T> : BaseMetadataProvider
- where T : BaseItem
- {
- protected abstract string CacheDirectory { get; }
-
- public override bool Supports(BaseEntity item)
- {
- return item is T;
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() =>
- {
- /*T myItem = item as T;
-
- if (CanSkipFFProbe(myItem))
- {
- return;
- }
-
- FFProbeResult result = FFProbe.Run(myItem, CacheDirectory);
-
- if (result == null)
- {
- Logger.LogInfo("Null FFProbeResult for {0} {1}", item.Id, item.Name);
- return;
- }
-
- if (result.format != null && result.format.tags != null)
- {
- result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
- }
-
- if (result.streams != null)
- {
- foreach (MediaStream stream in result.streams)
- {
- if (stream.tags != null)
- {
- stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
- }
- }
- }
-
- Fetch(myItem, result);*/
- });
- }
-
- protected abstract void Fetch(T item, FFProbeResult result);
-
- protected virtual bool CanSkipFFProbe(T item)
- {
- return false;
- }
-
- protected string GetDictionaryValue(Dictionary<string, string> tags, string key)
- {
- if (tags == null)
- {
- return null;
- }
-
- if (!tags.ContainsKey(key))
- {
- return null;
- }
-
- return tags[key];
- }
-
- protected int? GetDictionaryNumericValue(Dictionary<string, string> tags, string key)
- {
- string val = GetDictionaryValue(tags, key);
-
- if (!string.IsNullOrEmpty(val))
- {
- int i;
-
- if (int.TryParse(val, out i))
- {
- return i;
- }
- }
-
- return null;
- }
-
- protected DateTime? GetDictionaryDateTime(Dictionary<string, string> tags, string key)
- {
- string val = GetDictionaryValue(tags, key);
-
- if (!string.IsNullOrEmpty(val))
- {
- DateTime i;
-
- if (DateTime.TryParse(val, out i))
- {
- return i.ToUniversalTime();
- }
- }
-
- return null;
- }
-
- private Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
- {
- var newDict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
-
- foreach (string key in dict.Keys)
- {
- newDict[key] = dict[key];
- }
-
- return newDict;
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs b/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
new file mode 100644
index 0000000000..bd60003beb
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/BaseImageEnhancer.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Drawing;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseImageEnhancer
+ /// </summary>
+ public abstract class BaseImageEnhancer : IDisposable
+ {
+ /// <summary>
+ /// Return true only if the given image for the given item will be enhanced by this enhancer.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns><c>true</c> if this enhancer will enhance the supplied image for the supplied item, <c>false</c> otherwise</returns>
+ public abstract bool Supports(BaseItem item, ImageType imageType);
+
+ /// <summary>
+ /// Gets the priority or order in which this enhancer should be run.
+ /// </summary>
+ /// <value>The priority.</value>
+ public abstract MetadataProviderPriority Priority { get; }
+
+ /// <summary>
+ /// Return the date of the last configuration change affecting the provided baseitem and image type
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <returns>Date of last config change</returns>
+ public virtual DateTime LastConfigurationChange(BaseItem item, ImageType imageType)
+ {
+ return DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ }
+
+ /// <summary>
+ /// Gets the size of the enhanced image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <param name="originalImageSize">Size of the original image.</param>
+ /// <returns>ImageSize.</returns>
+ public virtual ImageSize GetEnhancedImageSize(BaseItem item, ImageType imageType, int imageIndex, ImageSize originalImageSize)
+ {
+ return originalImageSize;
+ }
+
+ /// <summary>
+ /// Enhances the supplied image and returns it
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="originalImage">The original image.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{System.Drawing.Image}.</returns>
+ protected abstract Task<Image> EnhanceImageAsyncInternal(BaseItem item, Image originalImage, ImageType imageType, int imageIndex);
+
+ /// <summary>
+ /// Enhances the image async.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="originalImage">The original image.</param>
+ /// <param name="imageType">Type of the image.</param>
+ /// <param name="imageIndex">Index of the image.</param>
+ /// <returns>Task{Image}.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task<Image> EnhanceImageAsync(BaseItem item, Image originalImage, ImageType imageType, int imageIndex)
+ {
+ if (item == null || originalImage == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var typeName = GetType().Name;
+
+ Logger.LogDebugInfo("Running {0} for {1}", typeName, item.Path ?? item.Name ?? "--Unknown--");
+
+ try
+ {
+ return await EnhanceImageAsyncInternal(item, originalImage, imageType, imageIndex).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogException("{0} failed enhancing {1}", ex, typeName, item.Name);
+
+ throw;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
index 38afb2b524..0869b25bc4 100644
--- a/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/BaseItemXmlParser.cs
@@ -1,724 +1,608 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Xml;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Common.Logging;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides a base class for parsing metadata xml
- /// </summary>
- public class BaseItemXmlParser<T>
- where T : BaseItem, new()
- {
- /// <summary>
- /// Fetches metadata for an item from one xml file
- /// </summary>
- public void Fetch(T item, string metadataFile)
- {
- // Use XmlReader for best performance
- using (XmlReader reader = XmlReader.Create(metadataFile))
- {
- reader.MoveToContent();
-
- // Loop through each element
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- }
- }
- }
-
- /// <summary>
- /// Fetches metadata from one Xml Element
- /// </summary>
- protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
- {
- switch (reader.Name)
- {
- // DateCreated
- case "Added":
- DateTime added;
- if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
- {
- item.DateCreated = added.ToUniversalTime();
- }
- break;
-
- // DisplayMediaType
- case "Type":
- {
- item.DisplayMediaType = reader.ReadElementContentAsString();
-
- switch (item.DisplayMediaType.ToLower())
- {
- case "blu-ray":
- item.DisplayMediaType = VideoType.BluRay.ToString();
- break;
- case "dvd":
- item.DisplayMediaType = VideoType.Dvd.ToString();
- break;
- case "":
- item.DisplayMediaType = null;
- break;
- }
-
- break;
- }
-
- // TODO: Do we still need this?
- case "banner":
- item.BannerImagePath = reader.ReadElementContentAsString();
- break;
-
- case "LocalTitle":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- case "SortTitle":
- item.SortName = reader.ReadElementContentAsString();
- break;
-
- case "Overview":
- case "Description":
- item.Overview = reader.ReadElementContentAsString();
- break;
-
- case "TagLine":
- {
- var list = item.Taglines ?? new List<string>();
- var tagline = reader.ReadElementContentAsString();
-
- if (!list.Contains(tagline))
- {
- list.Add(tagline);
- }
-
- item.Taglines = list;
- break;
- }
-
- case "TagLines":
- {
- FetchFromTaglinesNode(reader.ReadSubtree(), item);
- break;
- }
-
- case "ContentRating":
- case "MPAARating":
- item.OfficialRating = reader.ReadElementContentAsString();
- break;
-
- case "CustomRating":
- item.CustomRating = reader.ReadElementContentAsString();
- break;
-
- case "CustomPin":
- item.CustomPin = reader.ReadElementContentAsString();
- break;
-
- case "Runtime":
- case "RunningTime":
- {
- string text = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(text))
- {
- int runtime;
- if (int.TryParse(text.Split(' ')[0], out runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
- }
- break;
- }
-
- case "Genre":
- {
- var list = item.Genres ?? new List<string>();
- list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
-
- item.Genres = list;
- break;
- }
-
- case "AspectRatio":
- item.AspectRatio = reader.ReadElementContentAsString();
- break;
-
- case "Network":
- {
- var list = item.Studios ?? new List<string>();
- list.AddRange(GetSplitValues(reader.ReadElementContentAsString(), '|'));
-
- item.Studios = list;
- break;
- }
-
- case "Director":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Director" }))
- {
- item.AddPerson(p);
- }
- break;
- }
- case "Writer":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Writer" }))
- {
- item.AddPerson(p);
- }
- break;
- }
-
- case "Actors":
- case "GuestStars":
- {
- foreach (PersonInfo p in GetSplitValues(reader.ReadElementContentAsString(), '|').Select(v => new PersonInfo { Name = v, Type = "Actor" }))
- {
- item.AddPerson(p);
- }
- break;
- }
-
- case "Trailer":
- item.TrailerUrl = reader.ReadElementContentAsString();
- break;
-
- case "ProductionYear":
- {
- string val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- int ProductionYear;
- if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
- {
- item.ProductionYear = ProductionYear;
- }
- }
-
- break;
- }
-
- case "Rating":
- case "IMDBrating":
-
- string rating = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(rating))
- {
- float val;
-
- if (float.TryParse(rating, out val))
- {
- item.CommunityRating = val;
- }
- }
- break;
-
- case "FirstAired":
- {
- string firstAired = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(firstAired))
- {
- DateTime airDate;
-
- if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
- {
- item.PremiereDate = airDate.ToUniversalTime();
- item.ProductionYear = airDate.Year;
- }
- }
-
- break;
- }
-
- case "TMDbId":
- string tmdb = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(tmdb))
- {
- item.SetProviderId(MetadataProviders.Tmdb, tmdb);
- }
- break;
-
- case "TVcomId":
- string TVcomId = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(TVcomId))
- {
- item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
- }
- break;
-
- case "IMDB_ID":
- case "IMDB":
- case "IMDbId":
- string IMDbId = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(IMDbId))
- {
- item.SetProviderId(MetadataProviders.Imdb, IMDbId);
- }
- break;
-
- case "Genres":
- FetchFromGenresNode(reader.ReadSubtree(), item);
- break;
-
- case "Persons":
- FetchDataFromPersonsNode(reader.ReadSubtree(), item);
- break;
-
- case "ParentalRating":
- FetchFromParentalRatingNode(reader.ReadSubtree(), item);
- break;
-
- case "Studios":
- FetchFromStudiosNode(reader.ReadSubtree(), item);
- break;
-
- case "MediaInfo":
- {
- var video = item as Video;
-
- if (video != null)
- {
- FetchMediaInfo(reader.ReadSubtree(), video);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
-
- private void FetchMediaInfo(XmlReader reader, Video item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Audio":
- {
- AudioStream stream = FetchMediaInfoAudio(reader.ReadSubtree());
-
- List<AudioStream> streams = item.AudioStreams ?? new List<AudioStream>();
- streams.Add(stream);
- item.AudioStreams = streams;
-
- break;
- }
-
- case "Video":
- FetchMediaInfoVideo(reader.ReadSubtree(), item);
- break;
-
- case "Subtitle":
- {
- SubtitleStream stream = FetchMediaInfoSubtitles(reader.ReadSubtree());
-
- List<SubtitleStream> streams = item.Subtitles ?? new List<SubtitleStream>();
- streams.Add(stream);
- item.Subtitles = streams;
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private AudioStream FetchMediaInfoAudio(XmlReader reader)
- {
- var stream = new AudioStream();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Default":
- stream.IsDefault = reader.ReadElementContentAsString() == "True";
- break;
-
- case "SamplingRate":
- stream.SampleRate = reader.ReadIntSafe();
- break;
-
- case "BitRate":
- stream.BitRate = reader.ReadIntSafe();
- break;
-
- case "Channels":
- stream.Channels = reader.ReadIntSafe();
- break;
-
- case "Language":
- stream.Language = reader.ReadElementContentAsString();
- break;
-
- case "Codec":
- stream.Codec = reader.ReadElementContentAsString();
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return stream;
- }
-
- private void FetchMediaInfoVideo(XmlReader reader, Video item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Width":
- item.Width = reader.ReadIntSafe();
- break;
-
- case "Height":
- item.Height = reader.ReadIntSafe();
- break;
-
- case "BitRate":
- item.BitRate = reader.ReadIntSafe();
- break;
-
- case "FrameRate":
- item.FrameRate = reader.ReadFloatSafe();
- break;
-
- case "ScanType":
- item.ScanType = reader.ReadElementContentAsString();
- break;
-
- case "Duration":
- item.RunTimeTicks = TimeSpan.FromMinutes(reader.ReadIntSafe()).Ticks;
- break;
-
- case "DurationSeconds":
- int seconds = reader.ReadIntSafe();
- if (seconds > 0)
- {
- item.RunTimeTicks = TimeSpan.FromSeconds(seconds).Ticks;
- }
- break;
-
- case "Codec":
- {
- string videoCodec = reader.ReadElementContentAsString();
-
- switch (videoCodec.ToLower())
- {
- case "sorenson h.263":
- item.Codec = "Sorenson H263";
- break;
- case "h.262":
- item.Codec = "MPEG-2 Video";
- break;
- case "h.264":
- item.Codec = "AVC";
- break;
- default:
- item.Codec = videoCodec;
- break;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private SubtitleStream FetchMediaInfoSubtitles(XmlReader reader)
- {
- var stream = new SubtitleStream();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Language":
- stream.Language = reader.ReadElementContentAsString();
- break;
-
- case "Default":
- stream.IsDefault = reader.ReadElementContentAsString() == "True";
- break;
-
- case "Forced":
- stream.IsForced = reader.ReadElementContentAsString() == "True";
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return stream;
- }
-
- private void FetchFromTaglinesNode(XmlReader reader, T item)
- {
- var list = item.Taglines ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Tagline":
- {
- string val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val) && !list.Contains(val))
- {
- list.Add(val);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Taglines = list;
- }
-
- private void FetchFromGenresNode(XmlReader reader, T item)
- {
- var list = item.Genres ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Genre":
- {
- string genre = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(genre))
- {
- list.Add(genre);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Genres = list;
- }
-
- private void FetchDataFromPersonsNode(XmlReader reader, T item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Person":
- {
- item.AddPerson(GetPersonFromXmlNode(reader.ReadSubtree()));
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private void FetchFromStudiosNode(XmlReader reader, T item)
- {
- var list = item.Studios ?? new List<string>();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Studio":
- {
- string studio = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(studio))
- {
- list.Add(studio);
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- item.Studios = list;
- }
-
- private void FetchFromParentalRatingNode(XmlReader reader, T item)
- {
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Value":
- {
- string ratingString = reader.ReadElementContentAsString();
-
- int rating = 7;
-
- if (!string.IsNullOrWhiteSpace(ratingString))
- {
- int.TryParse(ratingString, out rating);
- }
-
- switch (rating)
- {
- case -1:
- item.OfficialRating = "NR";
- break;
- case 0:
- item.OfficialRating = "UR";
- break;
- case 1:
- item.OfficialRating = "G";
- break;
- case 3:
- item.OfficialRating = "PG";
- break;
- case 4:
- item.OfficialRating = "PG-13";
- break;
- case 5:
- item.OfficialRating = "NC-17";
- break;
- case 6:
- item.OfficialRating = "R";
- break;
- default:
- break;
- }
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- }
- }
-
- private PersonInfo GetPersonFromXmlNode(XmlReader reader)
- {
- var person = new PersonInfo();
-
- reader.MoveToContent();
-
- while (reader.Read())
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "Name":
- person.Name = reader.ReadElementContentAsString();
- break;
-
- case "Type":
- person.Type = reader.ReadElementContentAsString();
- break;
-
- case "Role":
- person.Overview = reader.ReadElementContentAsString();
- break;
-
- default:
- reader.Skip();
- break;
- }
- }
- }
-
- return person;
- }
-
- protected IEnumerable<string> GetSplitValues(string value, char deliminator)
- {
- value = (value ?? string.Empty).Trim(deliminator);
-
- return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(deliminator);
- }
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides a base class for parsing metadata xml
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public class BaseItemXmlParser<T>
+ where T : BaseItem, new()
+ {
+ /// <summary>
+ /// Fetches metadata for an item from one xml file
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="metadataFile">The metadata file.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public void Fetch(T item, string metadataFile, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ if (string.IsNullOrEmpty(metadataFile))
+ {
+ throw new ArgumentNullException();
+ }
+
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(metadataFile))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches metadata from one Xml Element
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected virtual void FetchDataFromXmlNode(XmlReader reader, T item)
+ {
+ switch (reader.Name)
+ {
+ // DateCreated
+ case "Added":
+ DateTime added;
+ if (DateTime.TryParse(reader.ReadElementContentAsString() ?? string.Empty, out added))
+ {
+ item.DateCreated = added.ToUniversalTime();
+ }
+ break;
+
+ case "LocalTitle":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Type":
+ {
+ var type = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(type) && !type.Equals("none",StringComparison.OrdinalIgnoreCase))
+ {
+ item.DisplayMediaType = type;
+ }
+
+ break;
+ }
+ case "SortTitle":
+ item.SortName = reader.ReadElementContentAsString();
+ break;
+
+ case "Overview":
+ case "Description":
+ item.Overview = reader.ReadElementContentAsString();
+ break;
+
+ case "TagLine":
+ {
+ var tagline = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(tagline))
+ {
+ item.AddTagline(tagline);
+ }
+
+ break;
+ }
+
+ case "TagLines":
+ {
+ FetchFromTaglinesNode(reader.ReadSubtree(), item);
+ break;
+ }
+
+ case "ContentRating":
+ case "certification":
+ case "MPAARating":
+ {
+ var rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ item.OfficialRating = rating;
+ }
+ break;
+ }
+
+ case "CustomRating":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.CustomRating = val;
+ }
+ break;
+ }
+
+ case "Runtime":
+ case "RunningTime":
+ {
+ var text = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(text))
+ {
+ int runtime;
+ if (int.TryParse(text.Split(' ')[0], out runtime))
+ {
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
+ }
+ }
+ break;
+ }
+
+ case "Genre":
+ {
+ foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+ item.AddGenre(name);
+ }
+ break;
+ }
+
+ case "AspectRatio":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AspectRatio = val;
+ }
+ break;
+ }
+
+ case "Network":
+ {
+ foreach (var name in SplitNames(reader.ReadElementContentAsString()))
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+ item.AddStudio(name);
+ }
+ break;
+ }
+
+ case "Director":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v, Type = PersonType.Director }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+ case "Writer":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v, Type = PersonType.Writer }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Actors":
+ case "GuestStars":
+ {
+ foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Actor }))
+ {
+ if (string.IsNullOrWhiteSpace(p.Name))
+ {
+ continue;
+ }
+ item.AddPerson(p);
+ }
+ break;
+ }
+
+ case "Trailer":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AddTrailerUrl(val);
+ }
+ break;
+ }
+
+ case "ProductionYear":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ int ProductionYear;
+ if (int.TryParse(val, out ProductionYear) && ProductionYear > 1850)
+ {
+ item.ProductionYear = ProductionYear;
+ }
+ }
+
+ break;
+ }
+
+ case "Rating":
+ case "IMDBrating":
+ {
+
+ var rating = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(rating))
+ {
+ float val;
+
+ if (float.TryParse(rating, out val))
+ {
+ item.CommunityRating = val;
+ }
+ }
+ break;
+ }
+
+ case "FirstAired":
+ {
+ var firstAired = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(firstAired))
+ {
+ DateTime airDate;
+
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ item.PremiereDate = airDate.ToUniversalTime();
+ item.ProductionYear = airDate.Year;
+ }
+ }
+
+ break;
+ }
+
+ case "TMDbId":
+ var tmdb = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(tmdb))
+ {
+ item.SetProviderId(MetadataProviders.Tmdb, tmdb);
+ }
+ break;
+
+ case "TVcomId":
+ var TVcomId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(TVcomId))
+ {
+ item.SetProviderId(MetadataProviders.Tvcom, TVcomId);
+ }
+ break;
+
+ case "IMDB_ID":
+ case "IMDB":
+ case "IMDbId":
+ var IMDbId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(IMDbId))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, IMDbId);
+ }
+ break;
+
+ case "Genres":
+ FetchFromGenresNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Persons":
+ FetchDataFromPersonsNode(reader.ReadSubtree(), item);
+ break;
+
+ case "ParentalRating":
+ FetchFromParentalRatingNode(reader.ReadSubtree(), item);
+ break;
+
+ case "Studios":
+ FetchFromStudiosNode(reader.ReadSubtree(), item);
+ break;
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Fetches from taglines node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromTaglinesNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Tagline":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AddTagline(val);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from genres node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromGenresNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Genre":
+ {
+ var genre = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(genre))
+ {
+ item.AddGenre(genre);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches the data from persons node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchDataFromPersonsNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Person":
+ {
+ item.AddPeople(GetPersonsFromXmlNode(reader.ReadSubtree()));
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from studios node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromStudiosNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Studio":
+ {
+ var studio = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(studio))
+ {
+ item.AddStudio(studio);
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches from parental rating node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ private void FetchFromParentalRatingNode(XmlReader reader, T item)
+ {
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Value":
+ {
+ var ratingString = reader.ReadElementContentAsString();
+
+ int rating = 7;
+
+ if (!string.IsNullOrWhiteSpace(ratingString))
+ {
+ int.TryParse(ratingString, out rating);
+ }
+
+ switch (rating)
+ {
+ case -1:
+ item.OfficialRating = "NR";
+ break;
+ case 0:
+ item.OfficialRating = "UR";
+ break;
+ case 1:
+ item.OfficialRating = "G";
+ break;
+ case 3:
+ item.OfficialRating = "PG";
+ break;
+ case 4:
+ item.OfficialRating = "PG-13";
+ break;
+ case 5:
+ item.OfficialRating = "NC-17";
+ break;
+ case 6:
+ item.OfficialRating = "R";
+ break;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the persons from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>IEnumerable{PersonInfo}.</returns>
+ private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
+ {
+ var names = new List<string>();
+ var type = string.Empty;
+ var role = string.Empty;
+
+ reader.MoveToContent();
+
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "Name":
+ names.AddRange(SplitNames(reader.ReadElementContentAsString()));
+ break;
+
+ case "Type":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ type = val;
+ }
+ break;
+ }
+
+ case "Role":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ role = val;
+ }
+ break;
+ }
+
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+ }
+
+ return names.Select(n => new PersonInfo { Name = n, Role = role, Type = type });
+ }
+
+ /// <summary>
+ /// Used to split names of comma or pipe delimeted genres and people
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>IEnumerable{System.String}.</returns>
+ private IEnumerable<string> SplitNames(string value)
+ {
+ value = value ?? string.Empty;
+
+ // Only split by comma if there is no pipe in the string
+ // We have to be careful to not split names like Matthew, Jr.
+ var separator = value.IndexOf('|') == -1 ? ',' : '|';
+
+ value = value.Trim().Trim(separator);
+
+ return string.IsNullOrWhiteSpace(value) ? new string[] { } : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
index 50004be442..3916c0f541 100644
--- a/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
+++ b/MediaBrowser.Controller/Providers/BaseMetadataProvider.cs
@@ -1,104 +1,408 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Common.Extensions;
-using System.Threading.Tasks;
-using System;
-
-namespace MediaBrowser.Controller.Providers
-{
- public abstract class BaseMetadataProvider
- {
- protected Guid _id;
- public virtual Guid Id
- {
- get
- {
- if (_id == null) _id = this.GetType().FullName.GetMD5();
- return _id;
- }
- }
-
- public abstract bool Supports(BaseEntity item);
-
- public virtual bool RequiresInternet
- {
- get
- {
- return false;
- }
- }
-
- /// <summary>
- /// Returns the last refresh time of this provider for this item. Providers that care should
- /// call SetLastRefreshed to update this value.
- /// </summary>
- /// <param name="item"></param>
- /// <returns></returns>
- protected virtual DateTime LastRefreshed(BaseEntity item)
- {
- return (item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo())).LastRefreshed;
- }
-
- /// <summary>
- /// Sets the persisted last refresh date on the item for this provider.
- /// </summary>
- /// <param name="item"></param>
- /// <param name="value"></param>
- protected virtual void SetLastRefreshed(BaseEntity item, DateTime value)
- {
- var data = item.ProviderData.GetValueOrDefault(this.Id, new BaseProviderInfo());
- data.LastRefreshed = value;
- item.ProviderData[this.Id] = data;
- }
-
- /// <summary>
- /// Returns whether or not this provider should be re-fetched. Default functionality can
- /// compare a provided date with a last refresh time. This can be overridden for more complex
- /// determinations.
- /// </summary>
- /// <returns></returns>
- public virtual bool NeedsRefresh(BaseEntity item)
- {
- return CompareDate(item) > LastRefreshed(item);
- }
-
- /// <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>
- protected virtual DateTime CompareDate(BaseEntity item)
- {
- return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
- }
-
- public virtual Task FetchIfNeededAsync(BaseEntity item)
- {
- if (this.NeedsRefresh(item))
- return FetchAsync(item, item.ResolveArgs);
- else
- return new Task(() => { });
- }
-
- public abstract Task FetchAsync(BaseEntity item, ItemResolveEventArgs args);
-
- public abstract MetadataProviderPriority Priority { get; }
- }
-
- /// <summary>
- /// Determines when a provider should execute, relative to others
- /// </summary>
- public enum MetadataProviderPriority
- {
- // Run this provider at the beginning
- First = 1,
-
- // Run this provider after all first priority providers
- Second = 2,
-
- // Run this provider after all second priority providers
- Third = 3,
-
- // Run this provider last
- Last = 4
- }
-}
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseMetadataProvider
+ /// </summary>
+ public abstract class BaseMetadataProvider : IDisposable
+ {
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ protected ILogger Logger { get; private set; }
+
+ // Cache these since they will be used a lot
+ /// <summary>
+ /// The false task result
+ /// </summary>
+ protected static readonly Task<bool> FalseTaskResult = Task.FromResult(false);
+ /// <summary>
+ /// The true task result
+ /// </summary>
+ protected static readonly Task<bool> TrueTaskResult = Task.FromResult(true);
+
+ /// <summary>
+ /// The _id
+ /// </summary>
+ protected Guid _id;
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public virtual Guid Id
+ {
+ get
+ {
+ if (_id == Guid.Empty) _id = GetType().FullName.GetMD5();
+ return _id;
+ }
+ }
+
+ /// <summary>
+ /// Supportses the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public abstract bool Supports(BaseItem item);
+
+ /// <summary>
+ /// Gets a value indicating whether [requires internet].
+ /// </summary>
+ /// <value><c>true</c> if [requires internet]; otherwise, <c>false</c>.</value>
+ public virtual bool RequiresInternet
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Gets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ protected virtual string ProviderVersion
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ /// <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 virtual bool RefreshOnVersionChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines if this provider is relatively slow and, therefore, should be skipped
+ /// in certain instances. Default is whether or not it requires internet. Can be overridden
+ /// for explicit designation.
+ /// </summary>
+ /// <value><c>true</c> if this instance is slow; otherwise, <c>false</c>.</value>
+ public virtual bool IsSlow
+ {
+ get { return RequiresInternet; }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseMetadataProvider" /> class.
+ /// </summary>
+ protected BaseMetadataProvider()
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// Initializes this instance.
+ /// </summary>
+ protected virtual void Initialize()
+ {
+ Logger = LogManager.GetLogger(GetType().Name);
+ }
+
+ /// <summary>
+ /// Sets the persisted last refresh date on the item for this provider.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="providerVersion">The provider version.</param>
+ /// <param name="status">The status.</param>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ protected virtual void SetLastRefreshed(BaseItem item, DateTime value, string providerVersion, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ var data = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id });
+ data.LastRefreshed = value;
+ data.LastRefreshStatus = status;
+ data.ProviderVersion = providerVersion;
+
+ // Save the file system stamp for future comparisons
+ if (RefreshOnFileSystemStampChange)
+ {
+ data.FileSystemStamp = GetCurrentFileSystemStamp(item);
+ }
+
+ item.ProviderData[Id] = data;
+ }
+
+ /// <summary>
+ /// Sets the last refreshed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="status">The status.</param>
+ protected virtual void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ SetLastRefreshed(item, value, ProviderVersion, status);
+ }
+
+ /// <summary>
+ /// Returns whether or not this provider should be re-fetched. Default functionality can
+ /// compare a provided date with a last refresh time. This can be overridden for more complex
+ /// determinations.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public bool NeedsRefresh(BaseItem item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ var providerInfo = item.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo());
+
+ return NeedsRefreshInternal(item, providerInfo);
+ }
+
+ /// <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>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ protected virtual bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+
+ if (providerInfo == null)
+ {
+ throw new ArgumentNullException("providerInfo");
+ }
+
+ if (CompareDate(item) > providerInfo.LastRefreshed)
+ {
+ return true;
+ }
+
+ if (RefreshOnFileSystemStampChange && HasFileSystemStampChanged(item, providerInfo))
+ {
+ return true;
+ }
+
+ if (RefreshOnVersionChange && !string.Equals(ProviderVersion, providerInfo.ProviderVersion))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines if the item's file system stamp has changed from the last time the provider refreshed
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="providerInfo">The provider info.</param>
+ /// <returns><c>true</c> if [has file system stamp changed] [the specified item]; otherwise, <c>false</c>.</returns>
+ protected bool HasFileSystemStampChanged(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ return GetCurrentFileSystemStamp(item) != providerInfo.FileSystemStamp;
+ }
+
+ /// <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 virtual DateTime CompareDate(BaseItem item)
+ {
+ return DateTime.MinValue.AddMinutes(1); // want this to be greater than mindate so new items will refresh
+ }
+
+ /// <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>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ public async Task<bool> FetchAsync(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException();
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Logger.Info("Running for {0}", item.Path ?? item.Name ?? "--Unknown--");
+
+ // This provides the ability to cancel just this one provider
+ var innerCancellationTokenSource = new CancellationTokenSource();
+
+ Kernel.Instance.ProviderManager.OnProviderRefreshBeginning(this, item, innerCancellationTokenSource);
+
+ try
+ {
+ var task = FetchAsyncInternal(item, force, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token);
+
+ await task.ConfigureAwait(false);
+
+ if (task.IsFaulted)
+ {
+ // Log the AggregateException
+ if (task.Exception != null)
+ {
+ Logger.ErrorException("AggregateException:", task.Exception);
+ }
+
+ return false;
+ }
+
+ return task.Result;
+ }
+ catch (OperationCanceledException ex)
+ {
+ Logger.Info("{0} cancelled for {1}", GetType().Name, item.Name);
+
+ // If the outer cancellation token is the one that caused the cancellation, throw it
+ if (cancellationToken.IsCancellationRequested && ex.CancellationToken == cancellationToken)
+ {
+ throw;
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("failed refreshing {0}", ex, item.Name);
+
+ SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ return true;
+ }
+ finally
+ {
+ innerCancellationTokenSource.Dispose();
+
+ Kernel.Instance.ProviderManager.OnProviderRefreshCompleted(this, item);
+ }
+ }
+
+ /// <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>
+ protected abstract Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public abstract MetadataProviderPriority Priority { get; }
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool dispose)
+ {
+ }
+
+ /// <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 virtual bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// Determines if the parent's file system stamp should be used for comparison
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected virtual bool UseParentFileSystemStamp(BaseItem item)
+ {
+ // True when the current item is just a file
+ return !item.ResolveArgs.IsDirectory;
+ }
+
+ /// <summary>
+ /// Gets the item's current file system stamp
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>Guid.</returns>
+ private Guid GetCurrentFileSystemStamp(BaseItem item)
+ {
+ if (UseParentFileSystemStamp(item) && item.Parent != null)
+ {
+ return item.Parent.FileSystemStamp;
+ }
+
+ return item.FileSystemStamp;
+ }
+ }
+
+ /// <summary>
+ /// Determines when a provider should execute, relative to others
+ /// </summary>
+ public enum MetadataProviderPriority
+ {
+ // Run this provider at the beginning
+ /// <summary>
+ /// The first
+ /// </summary>
+ First = 1,
+
+ // Run this provider after all first priority providers
+ /// <summary>
+ /// The second
+ /// </summary>
+ Second = 2,
+
+ // Run this provider after all second priority providers
+ /// <summary>
+ /// The third
+ /// </summary>
+ Third = 3,
+
+ // Run this provider last
+ /// <summary>
+ /// The last
+ /// </summary>
+ Last = 4
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
index 1538b2262f..877ba5c4bf 100644
--- a/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
+++ b/MediaBrowser.Controller/Providers/BaseProviderInfo.cs
@@ -1,15 +1,60 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- public class BaseProviderInfo
- {
- public Guid ProviderId { get; set; }
- public DateTime LastRefreshed { get; set; }
-
- }
-}
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class BaseProviderInfo
+ /// </summary>
+ public class BaseProviderInfo
+ {
+ /// <summary>
+ /// Gets or sets the provider id.
+ /// </summary>
+ /// <value>The provider id.</value>
+ public Guid ProviderId { get; set; }
+ /// <summary>
+ /// Gets or sets the last refreshed.
+ /// </summary>
+ /// <value>The last refreshed.</value>
+ public DateTime LastRefreshed { get; set; }
+ /// <summary>
+ /// Gets or sets the file system stamp.
+ /// </summary>
+ /// <value>The file system stamp.</value>
+ public Guid FileSystemStamp { get; set; }
+ /// <summary>
+ /// Gets or sets the last refresh status.
+ /// </summary>
+ /// <value>The last refresh status.</value>
+ public ProviderRefreshStatus LastRefreshStatus { get; set; }
+ /// <summary>
+ /// Gets or sets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ public string ProviderVersion { get; set; }
+ /// <summary>
+ /// Gets or sets the data hash.
+ /// </summary>
+ /// <value>The data hash.</value>
+ public Guid DataHash { get; set; }
+ }
+
+ /// <summary>
+ /// Enum ProviderRefreshStatus
+ /// </summary>
+ public enum ProviderRefreshStatus
+ {
+ /// <summary>
+ /// The success
+ /// </summary>
+ Success,
+ /// <summary>
+ /// The failure
+ /// </summary>
+ Failure,
+ /// <summary>
+ /// The completed with errors
+ /// </summary>
+ CompletedWithErrors
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FanartBaseProvider.cs b/MediaBrowser.Controller/Providers/FanartBaseProvider.cs
new file mode 100644
index 0000000000..3063f3c9e1
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/FanartBaseProvider.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities;
+using System;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class FanartBaseProvider
+ /// </summary>
+ public abstract class FanartBaseProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// The LOG o_ FILE
+ /// </summary>
+ protected const string LOGO_FILE = "logo.png";
+ /// <summary>
+ /// The AR t_ FILE
+ /// </summary>
+ protected const string ART_FILE = "clearart.png";
+ /// <summary>
+ /// The THUM b_ FILE
+ /// </summary>
+ protected const string THUMB_FILE = "thumb.jpg";
+ /// <summary>
+ /// The DIS c_ FILE
+ /// </summary>
+ protected const string DISC_FILE = "disc.png";
+ /// <summary>
+ /// The BANNE r_ FILE
+ /// </summary>
+ protected const string BANNER_FILE = "banner.png";
+
+ /// <summary>
+ /// The API key
+ /// </summary>
+ protected const string APIKey = "5c6b04c68e904cfed1e6cbc9a9e683d4";
+
+ /// <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)
+ {
+ if (item.DontFetchMeta) return false;
+
+ return DateTime.UtcNow > (providerInfo.LastRefreshed.AddDays(Kernel.Instance.Configuration.MetadataRefreshDays))
+ && ShouldFetch(item, providerInfo);
+ }
+
+ /// <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 the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Third; }
+ }
+
+ /// <summary>
+ /// Shoulds the fetch.
+ /// </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 virtual bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ return false;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
index b7d9b71893..110502bc1f 100644
--- a/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/FolderProviderFromXml.cs
@@ -1,38 +1,83 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides metadata for Folders and all subclasses by parsing folder.xml
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class FolderProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Folder;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("folder.xml"))
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- new BaseItemXmlParser<Folder>().Fetch(item as Folder, Path.Combine(args.Path, "folder.xml"));
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides metadata for Folders and all subclasses by parsing folder.xml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FolderProviderFromXml : BaseMetadataProvider
+ {
+ /// <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 Folder && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <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)
+ {
+ var entry = item.MetaLocation != null ? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml")) : null;
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "folder.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+ new BaseItemXmlParser<Folder>().Fetch((Folder)item, path, cancellationToken);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
index d6fd26d1c4..9858764069 100644
--- a/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
+++ b/MediaBrowser.Controller/Providers/ImageFromMediaLocationProvider.cs
@@ -1,128 +1,231 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class ImageFromMediaLocationProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return true;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.IsDirectory)
- {
- var baseItem = item as BaseItem;
-
- if (baseItem != null)
- {
- return Task.Run(() => PopulateBaseItemImages(baseItem, args));
- }
-
- return Task.Run(() => PopulateImages(item, args));
- }
-
- return Task.FromResult<object>(null);
- }
-
- /// <summary>
- /// Fills in image paths based on files win the folder
- /// </summary>
- private void PopulateImages(BaseEntity item, ItemResolveEventArgs args)
- {
- for (int i = 0; i < args.FileSystemChildren.Length; i++)
- {
- var file = args.FileSystemChildren[i];
-
- string filePath = file.Path;
-
- string ext = Path.GetExtension(filePath);
-
- // Only support png and jpg files
- if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string name = Path.GetFileNameWithoutExtension(filePath);
-
- if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
- {
- item.PrimaryImagePath = filePath;
- }
- }
- }
-
- /// <summary>
- /// Fills in image paths based on files win the folder
- /// </summary>
- private void PopulateBaseItemImages(BaseItem item, ItemResolveEventArgs args)
- {
- var backdropFiles = new List<string>();
-
- for (int i = 0; i < args.FileSystemChildren.Length; i++)
- {
- var file = args.FileSystemChildren[i];
-
- string filePath = file.Path;
-
- string ext = Path.GetExtension(filePath);
-
- // Only support png and jpg files
- if (!ext.EndsWith("png", StringComparison.OrdinalIgnoreCase) && !ext.EndsWith("jpg", StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- string name = Path.GetFileNameWithoutExtension(filePath);
-
- if (name.Equals("folder", StringComparison.OrdinalIgnoreCase))
- {
- item.PrimaryImagePath = filePath;
- }
- else if (name.StartsWith("backdrop", StringComparison.OrdinalIgnoreCase))
- {
- backdropFiles.Add(filePath);
- }
- if (name.Equals("logo", StringComparison.OrdinalIgnoreCase))
- {
- item.LogoImagePath = filePath;
- }
- if (name.Equals("banner", StringComparison.OrdinalIgnoreCase))
- {
- item.BannerImagePath = filePath;
- }
- if (name.Equals("clearart", StringComparison.OrdinalIgnoreCase))
- {
- item.ArtImagePath = filePath;
- }
- if (name.Equals("thumb", StringComparison.OrdinalIgnoreCase))
- {
- item.ThumbnailImagePath = filePath;
- }
- }
-
- if (backdropFiles.Count > 0)
- {
- item.BackdropImagePaths = backdropFiles;
- }
- }
-
- }
-}
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides images for all types by looking for standard images - folder, backdrop, logo, etc.
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class ImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ /// <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.ResolveArgs.IsDirectory && 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>
+ /// 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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Make sure current image paths still exist
+ ValidateImages(item);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Make sure current backdrop paths still exist
+ ValidateBackdrops(item);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ PopulateBaseItemImages(item);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Validates that images within the item are still on the file system
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void ValidateImages(BaseItem item)
+ {
+ if (item.Images == null)
+ {
+ return;
+ }
+
+ // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+ var deletedKeys = item.Images.Keys.Where(image =>
+ {
+ var path = item.Images[image];
+
+ return IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue;
+ }).ToList();
+
+ // Now remove them from the dictionary
+ foreach(var key in deletedKeys)
+ {
+ item.Images.Remove(key);
+ }
+ }
+
+ /// <summary>
+ /// Validates that backdrops within the item are still on the file system
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void ValidateBackdrops(BaseItem item)
+ {
+ if (item.BackdropImagePaths == null)
+ {
+ return;
+ }
+
+ // Only validate paths from the same directory - need to copy to a list because we are going to potentially modify the collection below
+ var deletedImages = item.BackdropImagePaths.Where(path => IsInSameDirectory(item, path) && !item.ResolveArgs.GetMetaFileByPath(path).HasValue).ToList();
+
+ // Now remove them from the dictionary
+ foreach (var path in deletedImages)
+ {
+ item.BackdropImagePaths.Remove(path);
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [is in same directory] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="path">The path.</param>
+ /// <returns><c>true</c> if [is in same directory] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool IsInSameDirectory(BaseItem item, string path)
+ {
+ return string.Equals(Path.GetDirectoryName(path), item.Path, StringComparison.OrdinalIgnoreCase);
+ }
+
+ /// <summary>
+ /// Gets the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="filenameWithoutExtension">The filename without extension.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ protected virtual WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
+ {
+ return item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".png")) ?? item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.ResolveArgs.Path, filenameWithoutExtension + ".jpg"));
+ }
+
+ /// <summary>
+ /// Fills in image paths based on files win the folder
+ /// </summary>
+ /// <param name="item">The item.</param>
+ private void PopulateBaseItemImages(BaseItem item)
+ {
+ var backdropFiles = new List<string>();
+
+ // Primary Image
+ var image = GetImage(item, "folder");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Primary, image.Value.Path);
+ }
+
+ // Logo Image
+ image = GetImage(item, "logo");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Logo, image.Value.Path);
+ }
+
+ // Banner Image
+ image = GetImage(item, "banner");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Banner, image.Value.Path);
+ }
+
+ // Clearart
+ image = GetImage(item, "clearart");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Art, image.Value.Path);
+ }
+
+ // Thumbnail Image
+ image = GetImage(item, "thumb");
+
+ if (image.HasValue)
+ {
+ item.SetImage(ImageType.Thumb, image.Value.Path);
+ }
+
+ // Backdrop Image
+ image = GetImage(item, "backdrop");
+
+ if (image.HasValue)
+ {
+ backdropFiles.Add(image.Value.Path);
+ }
+
+ var unfound = 0;
+ for (var i = 1; i <= 20; i++)
+ {
+ // Backdrop Image
+ image = GetImage(item, "backdrop" + i);
+
+ if (image.HasValue)
+ {
+ backdropFiles.Add(image.Value.Path);
+ }
+ else
+ {
+ unfound++;
+
+ if (unfound >= 3)
+ {
+ break;
+ }
+ }
+ }
+
+ if (backdropFiles.Count > 0)
+ {
+ item.BackdropImagePaths = backdropFiles;
+ }
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs b/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
new file mode 100644
index 0000000000..114176e2c9
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ImagesByNameProvider.cs
@@ -0,0 +1,103 @@
+using System.Globalization;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Win32;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Provides images for generic types by looking for standard images in the IBN
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class ImagesByNameProvider : ImageFromMediaLocationProvider
+ {
+ /// <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)
+ {
+ //only run for these generic types since we are expensive in file i/o
+ return item is IndexFolder || item is BasePluginFolder;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get
+ {
+ return MetadataProviderPriority.Last;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether [refresh on file system stamp change].
+ /// </summary>
+ /// <value><c>true</c> if [refresh on file system stamp change]; otherwise, <c>false</c>.</value>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ /// <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)
+ {
+ // If the IBN location exists return the last modified date of any file in it
+ var location = GetLocation(item);
+ return Directory.Exists(location) ? FileSystem.GetFiles(location).Select(f => f.CreationTimeUtc > f.LastWriteTimeUtc ? f.CreationTimeUtc : f.LastWriteTimeUtc).Max() : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// The us culture
+ /// </summary>
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets the location.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns>System.String.</returns>
+ protected string GetLocation(BaseItem item)
+ {
+ var invalid = Path.GetInvalidFileNameChars();
+
+ var name = item.Name ?? string.Empty;
+ name = invalid.Aggregate(name, (current, c) => current.Replace(c.ToString(UsCulture), string.Empty));
+
+ return Path.Combine(Kernel.Instance.ApplicationPaths.GeneralPath, name);
+ }
+
+ /// <summary>
+ /// Gets the image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="filenameWithoutExtension">The filename without extension.</param>
+ /// <returns>System.Nullable{WIN32_FIND_DATA}.</returns>
+ protected override WIN32_FIND_DATA? GetImage(BaseItem item, string filenameWithoutExtension)
+ {
+ var location = GetLocation(item);
+
+ var result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".png"));
+ if (!result.HasValue)
+ result = FileSystem.GetFileData(Path.Combine(location, filenameWithoutExtension + ".jpg"));
+
+ return result;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs b/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
deleted file mode 100644
index 8823da6911..0000000000
--- a/MediaBrowser.Controller/Providers/LocalTrailerProvider.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers
-{
- /// <summary>
- /// Provides local trailers by checking the trailers subfolder
- /// </summary>
- [Export(typeof(BaseMetadataProvider))]
- public class LocalTrailerProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is BaseItem;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFolder("trailers"))
- {
- var items = new List<Video>();
-
- foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "trailers"), "*"))
- {
- var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
-
- if (video != null)
- {
- items.Add(video);
- }
- }
-
- (item as BaseItem).LocalTrailers = items;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
new file mode 100644
index 0000000000..6214cb0dae
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BDInfoProvider.cs
@@ -0,0 +1,265 @@
+using BDInfo;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts dvd information using VgtMpeg
+ /// </summary>
+ internal static class BDInfoProvider
+ {
+ internal static void FetchBdInfo(BaseItem item, string inputPath, FileSystemRepository bdInfoCache, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ // Get the path to the cache file
+ var cacheName = item.Id + "_" + item.DateModified.Ticks;
+
+ var cacheFile = bdInfoCache.GetResourcePath(cacheName, ".pb");
+
+ BDInfoResult result;
+
+ try
+ {
+ result = Kernel.Instance.ProtobufSerializer.DeserializeFromFile<BDInfoResult>(cacheFile);
+ }
+ catch (FileNotFoundException)
+ {
+ result = GetBDInfo(inputPath);
+
+ Kernel.Instance.ProtobufSerializer.SerializeToFile(result, cacheFile);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ int? currentHeight = null;
+ int? currentWidth = null;
+ int? currentBitRate = null;
+
+ var videoStream = video.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, inputPath, result);
+
+ videoStream = video.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;
+ }
+ }
+
+ /// <summary>
+ /// Determines whether the specified num is empty.
+ /// </summary>
+ /// <param name="num">The num.</param>
+ /// <returns><c>true</c> if the specified num is empty; otherwise, <c>false</c>.</returns>
+ private static bool IsEmpty(int? num)
+ {
+ return !num.HasValue || num.Value == 0;
+ }
+
+ /// <summary>
+ /// Fills video properties from the VideoStream of the largest playlist
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="inputPath">The input path.</param>
+ /// <param name="stream">The stream.</param>
+ private static void Fetch(Video video, string inputPath, BDInfoResult stream)
+ {
+ // Check all input for null/empty/zero
+
+ video.MediaStreams = stream.MediaStreams;
+
+ if (stream.RunTimeTicks.HasValue && stream.RunTimeTicks.Value > 0)
+ {
+ video.RunTimeTicks = stream.RunTimeTicks;
+ }
+
+ video.PlayableStreamFileNames = stream.Files.ToList();
+
+ if (stream.Chapters != null)
+ {
+ video.Chapters = stream.Chapters.Select(c => new ChapterInfo
+ {
+ StartPositionTicks = TimeSpan.FromSeconds(c).Ticks
+
+ }).ToList();
+ }
+ }
+
+ /// <summary>
+ /// Gets information about the longest playlist on a bdrom
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <returns>VideoStream.</returns>
+ private static BDInfoResult GetBDInfo(string path)
+ {
+ var bdrom = new BDROM(path);
+
+ bdrom.Scan();
+
+ // Get the longest playlist
+ var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
+
+ var outputStream = new BDInfoResult
+ {
+ MediaStreams = new List<MediaStream>()
+ };
+
+ if (playlist == null)
+ {
+ return outputStream;
+ }
+
+ outputStream.Chapters = playlist.Chapters;
+
+ outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+
+ var mediaStreams = new List<MediaStream> {};
+
+ foreach (var stream in playlist.SortedStreams)
+ {
+ var videoStream = stream as TSVideoStream;
+
+ if (videoStream != null)
+ {
+ AddVideoStream(mediaStreams, videoStream);
+ continue;
+ }
+
+ var audioStream = stream as TSAudioStream;
+
+ if (audioStream != null)
+ {
+ AddAudioStream(mediaStreams, audioStream);
+ continue;
+ }
+
+ var textStream = stream as TSTextStream;
+
+ if (textStream != null)
+ {
+ AddSubtitleStream(mediaStreams, textStream);
+ continue;
+ }
+
+ var graphicsStream = stream as TSGraphicsStream;
+
+ if (graphicsStream != null)
+ {
+ AddSubtitleStream(mediaStreams, graphicsStream);
+ }
+ }
+
+ outputStream.MediaStreams = mediaStreams;
+
+ if (playlist.StreamClips != null && playlist.StreamClips.Any())
+ {
+ // Get the files in the playlist
+ outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToList();
+ }
+
+ return outputStream;
+ }
+
+ /// <summary>
+ /// Adds the video stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="videoStream">The video stream.</param>
+ private static void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+ {
+ var mediaStream = new MediaStream
+ {
+ BitRate = Convert.ToInt32(videoStream.BitRate),
+ Width = videoStream.Width,
+ Height = videoStream.Height,
+ Codec = videoStream.CodecShortName,
+ ScanType = videoStream.IsInterlaced ? "interlaced" : "progressive",
+ Type = MediaStreamType.Video,
+ Index = streams.Count
+ };
+
+ if (videoStream.FrameRateDenominator > 0)
+ {
+ float frameRateEnumerator = videoStream.FrameRateEnumerator;
+ float frameRateDenominator = videoStream.FrameRateDenominator;
+
+ mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
+ }
+
+ streams.Add(mediaStream);
+ }
+
+ /// <summary>
+ /// Adds the audio stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="audioStream">The audio stream.</param>
+ private static void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+ {
+ streams.Add(new MediaStream
+ {
+ BitRate = Convert.ToInt32(audioStream.BitRate),
+ Codec = audioStream.CodecShortName,
+ Language = audioStream.LanguageCode,
+ Channels = audioStream.ChannelCount,
+ SampleRate = audioStream.SampleRate,
+ Type = MediaStreamType.Audio,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private static void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+
+ /// <summary>
+ /// Adds the subtitle stream.
+ /// </summary>
+ /// <param name="streams">The streams.</param>
+ /// <param name="textStream">The text stream.</param>
+ private static void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs
new file mode 100644
index 0000000000..95b70044a4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegImageProvider.cs
@@ -0,0 +1,17 @@
+using MediaBrowser.Controller.Entities;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ public abstract class BaseFFMpegImageProvider<T> : BaseFFMpegProvider<T>
+ where T : BaseItem
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Last; }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs
new file mode 100644
index 0000000000..605c03414f
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFMpegProvider.cs
@@ -0,0 +1,74 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Class BaseFFMpegProvider
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseFFMpegProvider<T> : BaseMetadataProvider
+ where T : BaseItem
+ {
+ /// <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 T;
+ }
+
+ /// <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>
+ /// The null mount task result
+ /// </summary>
+ protected readonly Task<IIsoMount> NullMountTaskResult = Task.FromResult<IIsoMount>(null);
+
+ /// <summary>
+ /// Gets the provider version.
+ /// </summary>
+ /// <value>The provider version.</value>
+ protected override string ProviderVersion
+ {
+ get
+ {
+ return Kernel.Instance.FFMpegManager.FFMpegVersion;
+ }
+ }
+
+ /// <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)
+ {
+ // If the last run wasn't successful, try again when there's a new version of ffmpeg
+ if (providerInfo.LastRefreshStatus != ProviderRefreshStatus.Success)
+ {
+ if (!string.Equals(ProviderVersion, providerInfo.ProviderVersion))
+ {
+ return true;
+ }
+ }
+
+ return base.NeedsRefreshInternal(item, providerInfo);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs
new file mode 100644
index 0000000000..bb2b82819c
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/BaseFFProbeProvider.cs
@@ -0,0 +1,358 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Provides a base class for extracting media information through ffprobe
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public abstract class BaseFFProbeProvider<T> : BaseFFMpegProvider<T>
+ where T : BaseItem
+ {
+ /// <summary>
+ /// Gets or sets the FF probe cache.
+ /// </summary>
+ /// <value>The FF probe cache.</value>
+ protected FileSystemRepository FFProbeCache { get; set; }
+
+ /// <summary>
+ /// Initializes this instance.
+ /// </summary>
+ protected override void Initialize()
+ {
+ base.Initialize();
+ FFProbeCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, CacheDirectoryName));
+ }
+
+ /// <summary>
+ /// Gets the name of the cache directory.
+ /// </summary>
+ /// <value>The name of the cache directory.</value>
+ protected virtual string CacheDirectoryName
+ {
+ get
+ {
+ return "ffmpeg-video-info";
+ }
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ // Give this second priority
+ // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ var myItem = (T)item;
+
+ var isoMount = await MountIsoIfNeeded(myItem, cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ OnPreFetch(myItem, isoMount);
+
+ var inputPath = isoMount == null ?
+ Kernel.Instance.FFMpegManager.GetInputArgument(myItem) :
+ Kernel.Instance.FFMpegManager.GetInputArgument((Video)item, isoMount);
+
+ var result = await Kernel.Instance.FFMpegManager.RunFFProbe(item, inputPath, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ NormalizeFFProbeResult(result);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Fetch(myItem, cancellationToken, result, isoMount).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ }
+ finally
+ {
+ if (isoMount != null)
+ {
+ isoMount.Dispose();
+ }
+ }
+
+ 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>
+ /// Mounts the iso if needed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ protected virtual Task<IIsoMount> MountIsoIfNeeded(T item, CancellationToken cancellationToken)
+ {
+ return NullMountTaskResult;
+ }
+
+ /// <summary>
+ /// Called when [pre fetch].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="mount">The mount.</param>
+ protected virtual void OnPreFetch(T item, IIsoMount mount)
+ {
+
+ }
+
+ /// <summary>
+ /// Normalizes the FF probe result.
+ /// </summary>
+ /// <param name="result">The result.</param>
+ private void NormalizeFFProbeResult(FFProbeResult 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>
+ /// Subclasses must set item values using this
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="result">The result.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected abstract Task Fetch(T item, CancellationToken cancellationToken, FFProbeResult result, IIsoMount isoMount);
+
+ /// <summary>
+ /// Converts ffprobe stream info to our MediaStream class
+ /// </summary>
+ /// <param name="streamInfo">The stream info.</param>
+ /// <param name="formatInfo">The format info.</param>
+ /// <returns>MediaStream.</returns>
+ protected MediaStream GetMediaStream(FFProbeMediaStreamInfo streamInfo, FFProbeMediaFormatInfo formatInfo)
+ {
+ var stream = new MediaStream
+ {
+ Codec = streamInfo.codec_name,
+ Language = GetDictionaryValue(streamInfo.tags, "language"),
+ Profile = streamInfo.profile,
+ Index = streamInfo.index
+ };
+
+ if (streamInfo.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Type = MediaStreamType.Audio;
+
+ stream.Channels = streamInfo.channels;
+
+ if (!string.IsNullOrEmpty(streamInfo.sample_rate))
+ {
+ stream.SampleRate = int.Parse(streamInfo.sample_rate);
+ }
+ }
+ else if (streamInfo.codec_type.Equals("subtitle", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.Type = MediaStreamType.Subtitle;
+ }
+ else
+ {
+ stream.Type = MediaStreamType.Video;
+
+ stream.Width = streamInfo.width;
+ stream.Height = streamInfo.height;
+ stream.AspectRatio = streamInfo.display_aspect_ratio;
+
+ stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
+ stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
+ }
+
+ // Get stream bitrate
+ if (stream.Type != MediaStreamType.Subtitle)
+ {
+ if (!string.IsNullOrEmpty(streamInfo.bit_rate))
+ {
+ stream.BitRate = int.Parse(streamInfo.bit_rate);
+ }
+ else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate))
+ {
+ // If the stream info doesn't have a bitrate get the value from the media format info
+ stream.BitRate = int.Parse(formatInfo.bit_rate);
+ }
+ }
+
+ if (streamInfo.disposition != null)
+ {
+ var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
+ var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
+
+ stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
+
+ stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return stream;
+ }
+
+ /// <summary>
+ /// Gets a frame rate from a string value in ffprobe output
+ /// This could be a number or in the format of 2997/125.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ /// <returns>System.Nullable{System.Single}.</returns>
+ private float? GetFrameRate(string value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ var parts = value.Split('/');
+
+ if (parts.Length == 2)
+ {
+ return float.Parse(parts[0]) / float.Parse(parts[1]);
+ }
+ return float.Parse(parts[0]);
+ }
+
+ return null;
+ }
+
+ /// <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);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ FFProbeCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs
new file mode 100644
index 0000000000..523192d4e3
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegAudioImageProvider.cs
@@ -0,0 +1,84 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses ffmpeg to create video images
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFMpegAudioImageProvider : BaseFFMpegImageProvider<Audio>
+ {
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ var audio = (Audio)item;
+
+ if (string.IsNullOrEmpty(audio.PrimaryImagePath))
+ {
+ // First try to use the parent's image
+ audio.PrimaryImagePath = audio.ResolveArgs.Parent.PrimaryImagePath;
+
+ // If it's still empty see if there's an embedded image
+ if (string.IsNullOrEmpty(audio.PrimaryImagePath))
+ {
+ if (audio.MediaStreams != null && audio.MediaStreams.Any(s => s.Type == MediaStreamType.Video))
+ {
+ var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
+
+ var path = Kernel.Instance.FFMpegManager.AudioImageCache.GetResourcePath(filename, ".jpg");
+
+ if (!Kernel.Instance.FFMpegManager.AudioImageCache.ContainsFilePath(path))
+ {
+ return ExtractImage(audio, path, cancellationToken);
+ }
+
+ // Image is already in the cache
+ audio.PrimaryImagePath = path;
+ }
+
+ }
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Extracts the image.
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> ExtractImage(Audio audio, string path, CancellationToken cancellationToken)
+ {
+ var success = await Kernel.Instance.FFMpegManager.ExtractImage(audio, path, cancellationToken).ConfigureAwait(false);
+
+ if (success)
+ {
+ audio.PrimaryImagePath = path;
+ SetLastRefreshed(audio, DateTime.UtcNow);
+ }
+ else
+ {
+ SetLastRefreshed(audio, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs
new file mode 100644
index 0000000000..2f617b5b18
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFMpegVideoImageProvider.cs
@@ -0,0 +1,137 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses ffmpeg to create video images
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFMpegVideoImageProvider : BaseFFMpegImageProvider<Video>
+ {
+ /// <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.LocationType != LocationType.FileSystem)
+ {
+ return false;
+ }
+
+ var video = item as Video;
+
+ if (video != null)
+ {
+ if (video.VideoType == VideoType.Iso && video.IsoType.HasValue && Kernel.Instance.IsoManager.CanMount(item.Path))
+ {
+ return true;
+ }
+
+ // We can only extract images from folder rips if we know the largest stream path
+ return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.BluRay || video.VideoType == VideoType.Dvd;
+ }
+
+ 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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(item.PrimaryImagePath))
+ {
+ var video = (Video)item;
+
+ var filename = item.Id + "_" + item.DateModified.Ticks + "_primary";
+
+ var path = Kernel.Instance.FFMpegManager.VideoImageCache.GetResourcePath(filename, ".jpg");
+
+ if (!Kernel.Instance.FFMpegManager.VideoImageCache.ContainsFilePath(path))
+ {
+ return ExtractImage(video, path, cancellationToken);
+ }
+
+ // Image is already in the cache
+ item.PrimaryImagePath = path;
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <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 Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
+ }
+
+ return NullMountTaskResult;
+ }
+
+ /// <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{System.Boolean}.</returns>
+ private async Task<bool> ExtractImage(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);
+
+ var inputPath = isoMount == null ?
+ Kernel.Instance.FFMpegManager.GetInputArgument(video) :
+ Kernel.Instance.FFMpegManager.GetInputArgument(video, isoMount);
+
+ var success = await Kernel.Instance.FFMpegManager.ExtractImage(inputPath, imageOffset, path, cancellationToken).ConfigureAwait(false);
+
+ if (success)
+ {
+ video.PrimaryImagePath = path;
+ SetLastRefreshed(video, DateTime.UtcNow);
+ }
+ else
+ {
+ SetLastRefreshed(video, DateTime.UtcNow, ProviderRefreshStatus.Failure);
+ }
+ }
+ finally
+ {
+ if (isoMount != null)
+ {
+ isoMount.Dispose();
+ }
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs
new file mode 100644
index 0000000000..d8fd76805b
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeAudioInfoProvider.cs
@@ -0,0 +1,208 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts audio information using ffprobe
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFProbeAudioInfoProvider : BaseFFProbeProvider<Audio>
+ {
+ /// <summary>
+ /// Gets the name of the cache directory.
+ /// </summary>
+ /// <value>The name of the cache directory.</value>
+ protected override string CacheDirectoryName
+ {
+ get
+ {
+ return "ffmpeg-audio-info";
+ }
+ }
+
+ /// <summary>
+ /// Fetches the specified audio.
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected override Task Fetch(Audio audio, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
+ {
+ return Task.Run(() =>
+ {
+ if (data.streams == null)
+ {
+ Logger.Error("Audio item has no streams: " + audio.Path);
+ return;
+ }
+
+ audio.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
+
+ // Get the first audio stream
+ var stream = data.streams.First(s => s.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase));
+
+ // Get duration from stream properties
+ var duration = stream.duration;
+
+ // If it's not there go into format properties
+ if (string.IsNullOrEmpty(duration))
+ {
+ duration = data.format.duration;
+ }
+
+ // If we got something, parse it
+ if (!string.IsNullOrEmpty(duration))
+ {
+ audio.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration)).Ticks;
+ }
+
+ if (data.format.tags != null)
+ {
+ FetchDataFromTags(audio, data.format.tags);
+ }
+ });
+ }
+
+ /// <summary>
+ /// Fetches data from the tags dictionary
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ private void FetchDataFromTags(Audio audio, Dictionary<string, string> tags)
+ {
+ var title = GetDictionaryValue(tags, "title");
+
+ // Only set Name if title was found in the dictionary
+ if (!string.IsNullOrEmpty(title))
+ {
+ audio.Name = title;
+ }
+
+ var composer = GetDictionaryValue(tags, "composer");
+
+ if (!string.IsNullOrWhiteSpace(composer))
+ {
+ // Only use the comma as a delimeter if there are no slashes or pipes.
+ // We want to be careful not to split names that have commas in them
+ var delimeter = composer.IndexOf('/') == -1 && composer.IndexOf('|') == -1 ? new[] { ',' } : new[] { '/', '|' };
+
+ foreach (var person in composer.Split(delimeter, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var name = person.Trim();
+
+ if (!string.IsNullOrEmpty(name))
+ {
+ audio.AddPerson(new PersonInfo { Name = name, Type = PersonType.Composer });
+ }
+ }
+ }
+
+ audio.Album = GetDictionaryValue(tags, "album");
+ audio.Artist = GetDictionaryValue(tags, "artist");
+
+ if (!string.IsNullOrWhiteSpace(audio.Artist))
+ {
+ // Add to people too
+ audio.AddPerson(new PersonInfo {Name = audio.Artist, Type = PersonType.MusicArtist});
+ }
+
+ // Several different forms of albumartist
+ audio.AlbumArtist = GetDictionaryValue(tags, "albumartist") ?? GetDictionaryValue(tags, "album artist") ?? GetDictionaryValue(tags, "album_artist");
+
+ // Track number
+ audio.IndexNumber = GetDictionaryNumericValue(tags, "track");
+
+ // Disc number
+ audio.ParentIndexNumber = GetDictionaryDiscValue(tags);
+
+ audio.Language = GetDictionaryValue(tags, "language");
+
+ audio.ProductionYear = GetDictionaryNumericValue(tags, "date");
+
+ // Several different forms of retaildate
+ audio.PremiereDate = GetDictionaryDateTime(tags, "retaildate") ?? GetDictionaryDateTime(tags, "retail date") ?? GetDictionaryDateTime(tags, "retail_date");
+
+ // If we don't have a ProductionYear try and get it from PremiereDate
+ if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
+ {
+ audio.ProductionYear = audio.PremiereDate.Value.Year;
+ }
+
+ FetchGenres(audio, tags);
+
+ // There's several values in tags may or may not be present
+ FetchStudios(audio, tags, "organization");
+ FetchStudios(audio, tags, "ensemble");
+ FetchStudios(audio, tags, "publisher");
+ }
+
+ /// <summary>
+ /// Gets the studios from the tags collection
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ /// <param name="tagName">Name of the tag.</param>
+ private void FetchStudios(Audio audio, Dictionary<string, string> tags, string tagName)
+ {
+ var val = GetDictionaryValue(tags, tagName);
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ audio.AddStudios(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+
+ /// <summary>
+ /// Gets the genres from the tags collection
+ /// </summary>
+ /// <param name="audio">The audio.</param>
+ /// <param name="tags">The tags.</param>
+ private void FetchGenres(Audio audio, Dictionary<string, string> tags)
+ {
+ var val = GetDictionaryValue(tags, "genre");
+
+ if (!string.IsNullOrEmpty(val))
+ {
+ audio.AddGenres(val.Split(new[] { '/', '|' }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+
+ /// <summary>
+ /// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
+ /// </summary>
+ /// <param name="tags">The tags.</param>
+ /// <returns>System.Nullable{System.Int32}.</returns>
+ private int? GetDictionaryDiscValue(Dictionary<string, string> tags)
+ {
+ var disc = GetDictionaryValue(tags, "disc");
+
+ if (!string.IsNullOrEmpty(disc))
+ {
+ disc = disc.Split('/')[0];
+
+ int num;
+
+ if (int.TryParse(disc, out num))
+ {
+ return num;
+ }
+ }
+
+ return null;
+ }
+ }
+
+}
diff --git a/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs
new file mode 100644
index 0000000000..5092429e84
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/MediaInfo/FFProbeVideoInfoProvider.cs
@@ -0,0 +1,291 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.MediaInfo;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.MediaInfo
+{
+ /// <summary>
+ /// Extracts video information using ffprobe
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class FFProbeVideoInfoProvider : BaseFFProbeProvider<Video>
+ {
+ /// <summary>
+ /// Gets or sets the bd info cache.
+ /// </summary>
+ /// <value>The bd info cache.</value>
+ private FileSystemRepository BdInfoCache { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FFProbeVideoInfoProvider" /> class.
+ /// </summary>
+ public FFProbeVideoInfoProvider()
+ : base()
+ {
+ BdInfoCache = new FileSystemRepository(Path.Combine(Kernel.Instance.ApplicationPaths.CachePath, "bdinfo"));
+ }
+
+ /// <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>
+ /// Supports video files and dvd structures
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ public override bool Supports(BaseItem item)
+ {
+ var video = item as Video;
+
+ if (video != null)
+ {
+ if (video.VideoType == VideoType.Iso)
+ {
+ return Kernel.Instance.IsoManager.CanMount(item.Path);
+ }
+
+ return video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Called when [pre fetch].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="mount">The mount.</param>
+ protected override 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))
+ {
+ PopulateDvdStreamFiles(item, mount);
+ }
+
+ base.OnPreFetch(item, mount);
+ }
+
+ /// <summary>
+ /// Mounts the iso if needed.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>IsoMount.</returns>
+ protected override Task<IIsoMount> MountIsoIfNeeded(Video item, CancellationToken cancellationToken)
+ {
+ if (item.VideoType == VideoType.Iso)
+ {
+ return Kernel.Instance.IsoManager.Mount(item.Path, cancellationToken);
+ }
+
+ return base.MountIsoIfNeeded(item, cancellationToken);
+ }
+
+ /// <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;
+ }
+
+ /// <summary>
+ /// Finds vob files and populates the dvd stream file properties
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ private void PopulateDvdStreamFiles(Video video, IIsoMount isoMount)
+ {
+ // 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
+ video.PlayableStreamFileNames = Directory.EnumerateFiles(root, "*.vob", SearchOption.AllDirectories).SkipWhile(f => new FileInfo(f).Length < minPlayableSize).Select(Path.GetFileName).ToList();
+ }
+
+ /// <summary>
+ /// Fetches the specified video.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="data">The data.</param>
+ /// <param name="isoMount">The iso mount.</param>
+ /// <returns>Task.</returns>
+ protected override Task Fetch(Video video, CancellationToken cancellationToken, FFProbeResult data, IIsoMount isoMount)
+ {
+ return Task.Run(() =>
+ {
+ 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)).Ticks;
+ }
+ }
+
+ if (data.streams != null)
+ {
+ video.MediaStreams = data.streams.Select(s => GetMediaStream(s, data.format)).ToList();
+ }
+
+ if (data.Chapters != null)
+ {
+ video.Chapters = data.Chapters;
+ }
+
+ if (video.Chapters == null || video.Chapters.Count == 0)
+ {
+ AddDummyChapters(video);
+ }
+
+ if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
+ {
+ var inputPath = isoMount != null ? isoMount.MountedPath : video.Path;
+ BDInfoProvider.FetchBdInfo(video, inputPath, BdInfoCache, cancellationToken);
+ }
+
+ AddExternalSubtitles(video);
+ });
+ }
+
+ /// <summary>
+ /// Adds the external subtitles.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ private void AddExternalSubtitles(Video video)
+ {
+ var useParent = (video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso) && !(video is Movie);
+
+ if (useParent && video.Parent == null)
+ {
+ return;
+ }
+
+ var fileSystemChildren = useParent
+ ? video.Parent.ResolveArgs.FileSystemChildren
+ : video.ResolveArgs.FileSystemChildren;
+
+ var startIndex = video.MediaStreams == null ? 0 : video.MediaStreams.Count;
+ var streams = new List<MediaStream>();
+
+ foreach (var file in fileSystemChildren.Where(f => !f.IsDirectory))
+ {
+ var extension = Path.GetExtension(file.Path);
+
+ if (string.Equals(extension, ".srt", StringComparison.OrdinalIgnoreCase))
+ {
+ streams.Add(new MediaStream
+ {
+ Index = startIndex,
+ Type = MediaStreamType.Subtitle,
+ IsExternal = true,
+ Path = file.Path,
+ Codec = "srt"
+ });
+
+ startIndex++;
+ }
+ }
+
+ if (video.MediaStreams == null)
+ {
+ video.MediaStreams = new List<MediaStream>();
+ }
+ video.MediaStreams.AddRange(streams);
+ }
+
+ /// <summary>
+ /// The dummy chapter duration
+ /// </summary>
+ private static readonly long DummyChapterDuration = TimeSpan.FromMinutes(10).Ticks;
+
+ /// <summary>
+ /// Adds the dummy chapters.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ private void AddDummyChapters(Video video)
+ {
+ var runtime = video.RunTimeTicks ?? 0;
+
+ if (runtime < DummyChapterDuration)
+ {
+ return;
+ }
+
+ long currentChapterTicks = 0;
+ var index = 1;
+
+ var chapters = new List<ChapterInfo> { };
+
+ while (currentChapterTicks < runtime)
+ {
+ chapters.Add(new ChapterInfo
+ {
+ Name = "Chapter " + index,
+ StartPositionTicks = currentChapterTicks
+ });
+
+ index++;
+ currentChapterTicks += DummyChapterDuration;
+ }
+
+ video.Chapters = chapters;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ BdInfoCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs b/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs
new file mode 100644
index 0000000000..38e5475230
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/FanArtMovieProvider.cs
@@ -0,0 +1,220 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class FanArtMovieProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class FanArtMovieProvider : FanartBaseProvider
+ {
+ /// <summary>
+ /// The fan art base URL
+ /// </summary>
+ protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/movie/{0}/{1}/xml/all/1/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 is Movie || item is BoxSet;
+ }
+
+ /// <summary>
+ /// Shoulds the fetch.
+ /// </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 ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ var baseItem = item;
+ if (item.Path == null || item.DontFetchMeta || string.IsNullOrEmpty(baseItem.GetProviderId(MetadataProviders.Tmdb))) return false; //nothing to do
+ var artExists = item.ResolveArgs.ContainsMetaFileByName(ART_FILE);
+ var logoExists = item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE);
+ var discExists = item.ResolveArgs.ContainsMetaFileByName(DISC_FILE);
+
+ return (!artExists && Kernel.Instance.Configuration.DownloadMovieArt)
+ || (!logoExists && Kernel.Instance.Configuration.DownloadMovieLogo)
+ || (!discExists && Kernel.Instance.Configuration.DownloadMovieDisc);
+ }
+
+ /// <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>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var movie = item;
+ if (ShouldFetch(movie, movie.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id })))
+ {
+ var language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+ var url = string.Format(FanArtBaseUrl, APIKey, movie.GetProviderId(MetadataProviders.Tmdb));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (doc.HasChildNodes)
+ {
+ string path;
+ var hd = Kernel.Instance.Configuration.DownloadHDFanArt ? "hd" : "";
+ if (Kernel.Instance.Configuration.DownloadMovieLogo && !item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE))
+ {
+ var node =
+ doc.SelectSingleNode("//fanart/movie/movielogos/" + hd + "movielogo[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/movielogos/movielogo[@lang = \"" + language + "\"]/@url");
+ if (node == null && language != "en")
+ {
+ //maybe just couldn't find language - try just first one
+ node = doc.SelectSingleNode("//fanart/movie/movielogos/" + hd + "movielogo/@url");
+ }
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearLogo for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Logo, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, LOGO_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieArt && !item.ResolveArgs.ContainsMetaFileByName(ART_FILE))
+ {
+ var node =
+ doc.SelectSingleNode("//fanart/movie/moviearts/" + hd + "movieart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/" + hd + "movieart/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/movieart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviearts/movieart/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearArt for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Art, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, ART_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieDisc && !item.ResolveArgs.ContainsMetaFileByName(DISC_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviediscs/moviedisc/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting DiscArt for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Disc, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, DISC_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieBanner && !item.ResolveArgs.ContainsMetaFileByName(BANNER_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviebanners/moviebanner/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Banner, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, BANNER_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadMovieThumb && !item.ResolveArgs.ContainsMetaFileByName(THUMB_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/movie/moviethumbs/moviethumb/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting Banner for " + movie.Name);
+ try
+ {
+ movie.SetImage(ImageType.Thumb, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(movie, path, THUMB_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ SetLastRefreshed(movie, DateTime.UtcNow);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs
new file mode 100644
index 0000000000..2319e5cfa4
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieDbProvider.cs
@@ -0,0 +1,1607 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ class MovieDbProviderException : ApplicationException
+ {
+ public MovieDbProviderException(string msg) : base(msg)
+ {
+ }
+
+ }
+ /// <summary>
+ /// Class MovieDbProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieDbProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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 Movie || item is BoxSet;
+ }
+
+ /// <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>
+ /// If we save locally, refresh if they delete something
+ /// </summary>
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return Kernel.Instance.Configuration.SaveLocalMeta;
+ }
+ }
+
+ /// <summary>
+ /// The _TMDB settings task
+ /// </summary>
+ private Task<TmdbSettingsResult> _tmdbSettingsTask;
+ /// <summary>
+ /// The _TMDB settings task initialized
+ /// </summary>
+ private bool _tmdbSettingsTaskInitialized;
+ /// <summary>
+ /// The _TMDB settings task sync lock
+ /// </summary>
+ private object _tmdbSettingsTaskSyncLock = new object();
+
+ /// <summary>
+ /// Gets the TMDB settings.
+ /// </summary>
+ /// <value>The TMDB settings.</value>
+ public Task<TmdbSettingsResult> TmdbSettings
+ {
+ get
+ {
+ LazyInitializer.EnsureInitialized(ref _tmdbSettingsTask, ref _tmdbSettingsTaskInitialized, ref _tmdbSettingsTaskSyncLock, GetTmdbSettings);
+ return _tmdbSettingsTask;
+ }
+ }
+
+ /// <summary>
+ /// Gets the TMDB settings.
+ /// </summary>
+ /// <returns>Task{TmdbSettingsResult}.</returns>
+ private static async Task<TmdbSettingsResult> GetTmdbSettings()
+ {
+ try
+ {
+ using (var json = await Kernel.Instance.HttpManager.Get(String.Format(TmdbConfigUrl, ApiKey), Kernel.Instance.ResourcePools.MovieDb, CancellationToken.None).ConfigureAwait(false))
+ {
+ return JsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
+ }
+ }
+ catch (HttpException e)
+ {
+ return new TmdbSettingsResult
+ {
+ images = new TmdbImageSettings
+ {
+ backdrop_sizes =
+ new List<string>
+ {
+ "w380",
+ "w780",
+ "w1280",
+ "original"
+ },
+ poster_sizes =
+ new List<string>
+ {
+ "w92",
+ "w154",
+ "w185",
+ "w342",
+ "w500",
+ "original"
+ },
+ profile_sizes =
+ new List<string>
+ {
+ "w45",
+ "w185",
+ "h632",
+ "original"
+ },
+ base_url = "http://cf2.imgobject.com/t/p/"
+
+ }
+ };
+ }
+ }
+
+ /// <summary>
+ /// The json provider
+ /// </summary>
+ protected MovieProviderFromJson JsonProvider;
+ /// <summary>
+ /// Sets the persisted last refresh date on the item for this provider.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="status">The status.</param>
+ protected override void SetLastRefreshed(BaseItem item, DateTime value, ProviderRefreshStatus status = ProviderRefreshStatus.Success)
+ {
+ base.SetLastRefreshed(item, value, status);
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ //in addition to ours, we need to set the last refreshed time for the local data provider
+ //so it won't see the new files we download and process them all over again
+ if (JsonProvider == null) JsonProvider = new MovieProviderFromJson();
+ var data = item.ProviderData.GetValueOrDefault(JsonProvider.Id, new BaseProviderInfo { ProviderId = JsonProvider.Id });
+ data.LastRefreshed = value;
+ item.ProviderData[JsonProvider.Id] = data;
+ }
+ }
+
+ private const string TmdbConfigUrl = "http://api.themoviedb.org/3/configuration?api_key={0}";
+ private const string Search3 = @"http://api.themoviedb.org/3/search/movie?api_key={1}&query={0}&language={2}";
+ private const string AltTitleSearch = @"http://api.themoviedb.org/3/movie/{0}/alternative_titles?api_key={1}&country={2}";
+ private const string GetInfo3 = @"http://api.themoviedb.org/3/{3}/{0}?api_key={1}&language={2}";
+ private const string CastInfo = @"http://api.themoviedb.org/3/movie/{0}/casts?api_key={1}";
+ private const string ReleaseInfo = @"http://api.themoviedb.org/3/movie/{0}/releases?api_key={1}";
+ private const string GetImages = @"http://api.themoviedb.org/3/{2}/{0}/images?api_key={1}";
+ public static string ApiKey = "f6bd687ffa63cd282b6ff2c6877f2669";
+
+ static readonly Regex[] NameMatches = new[] {
+ new Regex(@"(?<name>.*)\((?<year>\d{4})\)"), // matches "My Movie (2001)" and gives us the name and the year
+ new Regex(@"(?<name>.*)") // last resort matches the whole string as the name
+ };
+
+ public const string LOCAL_META_FILE_NAME = "MBMovie.json";
+ public const string ALT_META_FILE_NAME = "movie.xml";
+ protected string ItemType = "movie";
+
+ protected override bool NeedsRefreshInternal(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.DontFetchMeta) return false;
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta && HasFileSystemStampChanged(item, providerInfo))
+ {
+ //If they deleted something from file system, chances are, this item was mis-identified the first time
+ item.SetProviderId(MetadataProviders.Tmdb, null);
+ Logger.Debug("MovieProvider reports file system stamp change...");
+ return true;
+
+ }
+
+ if (providerInfo.LastRefreshStatus == ProviderRefreshStatus.CompletedWithErrors)
+ {
+ Logger.Debug("MovieProvider for {0} - last attempt had errors. Will try again.", item.Path);
+ return true;
+ }
+
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (DateTime.Today.Subtract(item.DateCreated).TotalDays > 180 && downloadDate != DateTime.MinValue)
+ return false; // don't trigger a refresh data for item that are more than 6 months old and have been refreshed before
+
+ if (DateTime.Today.Subtract(downloadDate).TotalDays < Kernel.Instance.Configuration.MetadataRefreshDays) // only refresh every n days
+ return false;
+
+ if (HasAltMeta(item))
+ return false; //never refresh if has meta from other source
+
+
+
+ Logger.Debug("MovieDbProvider - " + item.Name + " needs refresh. Download date: " + downloadDate + " item created date: " + item.DateCreated + " Check for Update age: " + Kernel.Instance.Configuration.MetadataRefreshDays);
+ return true;
+ }
+
+ /// <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>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ if (HasAltMeta(item))
+ {
+ Logger.Info("MovieDbProvider - Not fetching because 3rd party meta exists for " + item.Name);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ if (item.DontFetchMeta)
+ {
+ Logger.Info("MovieDbProvider - Not fetching because requested to ignore " + item.Name);
+ return false;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!Kernel.Instance.Configuration.SaveLocalMeta || !HasLocalMeta(item) || (force && !HasLocalMeta(item)))
+ {
+ try
+ {
+ await FetchMovieData(item, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ }
+ catch (MovieDbProviderException e)
+ {
+ SetLastRefreshed(item, DateTime.UtcNow, ProviderRefreshStatus.CompletedWithErrors);
+ }
+
+ return true;
+ }
+ Logger.Debug("MovieDBProvider not fetching because local meta exists for " + item.Name);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //need at least the xml and folder.jpg/png or a movie.xml put in by someone else
+ return item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME);
+ }
+
+ /// <summary>
+ /// Determines whether [has alt meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has alt meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasAltMeta(BaseItem item)
+ {
+ return item.ResolveArgs.ContainsMetaFileByName(ALT_META_FILE_NAME);
+ }
+
+ /// <summary>
+ /// Fetches the movie data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken"></param>
+ /// <returns>Task.</returns>
+ private async Task FetchMovieData(BaseItem item, CancellationToken cancellationToken)
+ {
+ string id = item.GetProviderId(MetadataProviders.Tmdb) ?? await FindId(item, item.ProductionYear, cancellationToken).ConfigureAwait(false);
+ if (id != null)
+ {
+ Logger.Debug("MovieDbProvider - getting movie info with id: " + id);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await FetchMovieData(item, id, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger.Info("MovieDBProvider could not find " + item.Name + ". Check name on themoviedb.org.");
+ }
+ }
+
+ /// <summary>
+ /// Parses the name.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="justName">Name of the just.</param>
+ /// <param name="year">The year.</param>
+ protected void ParseName(string name, out string justName, out int? year)
+ {
+ justName = null;
+ year = null;
+ foreach (var re in NameMatches)
+ {
+ Match m = re.Match(name);
+ if (m.Success)
+ {
+ justName = m.Groups["name"].Value.Trim();
+ string y = m.Groups["year"] != null ? m.Groups["year"].Value : null;
+ int temp;
+ year = Int32.TryParse(y, out temp) ? temp : (int?)null;
+ break;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Finds the id.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="productionYear">The production year.</param>
+ /// <returns>Task{System.String}.</returns>
+ public async Task<string> FindId(BaseItem item, int? productionYear, CancellationToken cancellationToken)
+ {
+ string justName = item.Path != null ? item.Path.Substring(item.Path.LastIndexOf(Path.DirectorySeparatorChar)) : string.Empty;
+ var id = justName.GetAttributeValue("tmdbid");
+ if (id != null)
+ {
+ Logger.Debug("Using tmdb id specified in path.");
+ return id;
+ }
+
+ int? year;
+ string name = item.Name;
+ ParseName(name, out name, out year);
+
+ if (year == null && productionYear != null)
+ {
+ year = productionYear;
+ }
+
+ Logger.Info("MovieDbProvider: Finding id for movie: " + name);
+ string language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+
+ //if we are a boxset - look at our first child
+ var boxset = item as BoxSet;
+ if (boxset != null)
+ {
+ if (!boxset.Children.IsEmpty)
+ {
+ var firstChild = boxset.Children.First();
+ Logger.Debug("MovieDbProvider - Attempting to find boxset ID from: " + firstChild.Name);
+ string childName;
+ int? childYear;
+ ParseName(firstChild.Name, out childName, out childYear);
+ id = await GetBoxsetIdFromMovie(childName, childYear, language, cancellationToken).ConfigureAwait(false);
+ if (id != null)
+ {
+ Logger.Info("MovieDbProvider - Found Boxset ID: " + id);
+ }
+ }
+
+ return id;
+ }
+ //nope - search for it
+ id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (id == null)
+ {
+ //try in english if wasn't before
+ if (language != "en")
+ {
+ id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // try with dot and _ turned to space
+ name = name.Replace(",", " ");
+ name = name.Replace(".", " ");
+ name = name.Replace(" ", " ");
+ name = name.Replace("_", " ");
+ name = name.Replace("-", "");
+ id = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (id == null && language != "en")
+ {
+ //one more time, in english
+ id = await AttemptFindId(name, year, "en", cancellationToken).ConfigureAwait(false);
+
+ }
+ if (id == null)
+ {
+ //last resort - try using the actual folder name
+ id = await AttemptFindId(Path.GetFileName(item.ResolveArgs.Path), year, "en", cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ return id;
+ }
+
+ /// <summary>
+ /// Attempts the find id.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="year">The year.</param>
+ /// <param name="language">The language.</param>
+ /// <returns>Task{System.String}.</returns>
+ public virtual async Task<string> AttemptFindId(string name, int? year, string language, CancellationToken cancellationToken)
+ {
+ string url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
+ TmdbMovieSearchResults searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ if (searchResult == null || searchResult.results.Count == 0)
+ {
+ //try replacing numbers
+ foreach (var pair in ReplaceStartNumbers)
+ {
+ if (name.StartsWith(pair.Key))
+ {
+ name = name.Remove(0, pair.Key.Length);
+ name = pair.Value + name;
+ }
+ }
+ foreach (var pair in ReplaceEndNumbers)
+ {
+ if (name.EndsWith(pair.Key))
+ {
+ name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
+ name = name + pair.Value;
+ }
+ }
+ Logger.Info("MovieDBProvider - No results. Trying replacement numbers: " + name);
+ url3 = string.Format(Search3, UrlEncode(name), ApiKey, language);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<TmdbMovieSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+ if (searchResult != null)
+ {
+ string compName = GetComparableName(name, Logger);
+ foreach (var possible in searchResult.results)
+ {
+ string matchedName = null;
+ string id = possible.id.ToString();
+ string n = possible.title;
+ if (GetComparableName(n, Logger) == compName)
+ {
+ matchedName = n;
+ }
+ else
+ {
+ n = possible.original_title;
+ if (GetComparableName(n, Logger) == compName)
+ {
+ matchedName = n;
+ }
+ }
+
+ Logger.Debug("MovieDbProvider - " + compName + " didn't match " + n);
+ //if main title matches we don't have to look for alternatives
+ if (matchedName == null)
+ {
+ //that title didn't match - look for alternatives
+ url3 = string.Format(AltTitleSearch, id, ApiKey, Kernel.Instance.Configuration.MetadataCountryCode);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url3, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ var response = JsonSerializer.DeserializeFromStream<TmdbAltTitleResults>(json);
+
+ if (response != null && response.titles != null)
+ {
+ foreach (var title in response.titles)
+ {
+ var t = GetComparableName(title.title, Logger);
+ if (t == compName)
+ {
+ Logger.Debug("MovieDbProvider - " + compName +
+ " matched " + t);
+ matchedName = t;
+ break;
+ }
+ Logger.Debug("MovieDbProvider - " + compName +
+ " did not match " + t);
+ }
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+
+ if (matchedName != null)
+ {
+ Logger.Debug("Match " + matchedName + " for " + name);
+ if (year != null)
+ {
+ DateTime r;
+
+ if (DateTime.TryParse(possible.release_date, out r))
+ {
+ if (Math.Abs(r.Year - year.Value) > 1) // allow a 1 year tolerance on release date
+ {
+ Logger.Debug("Result " + matchedName + " released on " + r + " did not match year " + year);
+ continue;
+ }
+ }
+ }
+ //matched name and year
+ return id;
+ }
+
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// URLs the encode.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ private static string UrlEncode(string name)
+ {
+ return WebUtility.UrlEncode(name);
+ }
+
+ /// <summary>
+ /// Gets the boxset id from movie.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="year">The year.</param>
+ /// <param name="language">The language.</param>
+ /// <returns>Task{System.String}.</returns>
+ protected async Task<string> GetBoxsetIdFromMovie(string name, int? year, string language, CancellationToken cancellationToken)
+ {
+ string id = null;
+ string childId = await AttemptFindId(name, year, language, cancellationToken).ConfigureAwait(false);
+ if (childId != null)
+ {
+ string url = string.Format(GetInfo3, childId, ApiKey, language, ItemType);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ var movieResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+
+ if (movieResult != null && movieResult.belongs_to_collection != null)
+ {
+ id = movieResult.belongs_to_collection.id.ToString();
+ }
+ else
+ {
+ Logger.Error("Unable to obtain boxset id.");
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+ return id;
+ }
+
+ /// <summary>
+ /// Fetches the movie data.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task.</returns>
+ protected async Task FetchMovieData(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (String.IsNullOrEmpty(id))
+ {
+ Logger.Info("MoviedbProvider: Ignoring " + item.Name + " because ID forced blank.");
+ return;
+ }
+ if (item.GetProviderId(MetadataProviders.Tmdb) == null) item.SetProviderId(MetadataProviders.Tmdb, id);
+ var mainTask = FetchMainResult(item, id, cancellationToken);
+ var castTask = FetchCastInfo(item, id, cancellationToken);
+ var releaseTask = FetchReleaseInfo(item, id, cancellationToken);
+ var imageTask = FetchImageInfo(item, id, cancellationToken);
+
+ await Task.WhenAll(mainTask, castTask, releaseTask).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var mainResult = mainTask.Result;
+ if (mainResult == null) return;
+
+ if (castTask.Result != null)
+ {
+ mainResult.cast = castTask.Result.cast;
+ mainResult.crew = castTask.Result.crew;
+ }
+
+ if (releaseTask.Result != null)
+ {
+ mainResult.countries = releaseTask.Result.countries;
+ }
+
+ ProcessMainInfo(item, mainResult);
+
+ await Task.WhenAll(imageTask).ConfigureAwait(false);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (imageTask.Result != null)
+ {
+ await ProcessImages(item, imageTask.Result, cancellationToken).ConfigureAwait(false);
+ }
+
+ //and save locally
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ var ms = new MemoryStream();
+ JsonSerializer.SerializeToStream(mainResult, ms);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(item, Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME), ms, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ /// <summary>
+ /// Fetches the main result.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{CompleteMovieData}.</returns>
+ protected async Task<CompleteMovieData> FetchMainResult(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ ItemType = item is BoxSet ? "collection" : "movie";
+ string url = string.Format(GetInfo3, id, ApiKey, Kernel.Instance.Configuration.PreferredMetadataLanguage, ItemType);
+ CompleteMovieData mainResult = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (var json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+ }
+ }
+ catch (HttpException e)
+ {
+ if (e.IsTimedOut)
+ {
+ Logger.ErrorException("MovieDbProvider timed out attempting to retrieve main info for {0}", e, item.Path);
+ throw new MovieDbProviderException("Timed out on main info");
+ }
+ if (e.StatusCode == HttpStatusCode.NotFound)
+ {
+ Logger.ErrorException("MovieDbProvider not found error attempting to retrieve main info for {0}", e, item.Path);
+ throw new MovieDbProviderException("Not Found");
+ }
+
+ throw;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (mainResult != null && string.IsNullOrEmpty(mainResult.overview))
+ {
+ if (Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower() != "en")
+ {
+ Logger.Info("MovieDbProvider couldn't find meta for language " + Kernel.Instance.Configuration.PreferredMetadataLanguage + ". Trying English...");
+ url = string.Format(GetInfo3, id, ApiKey, "en", ItemType);
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ mainResult = JsonSerializer.DeserializeFromStream<CompleteMovieData>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (String.IsNullOrEmpty(mainResult.overview))
+ {
+ Logger.Error("MovieDbProvider - Unable to find information for " + item.Name + " (id:" + id + ")");
+ return null;
+ }
+ }
+ }
+ return mainResult;
+ }
+
+ /// <summary>
+ /// Fetches the cast info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbCastResult}.</returns>
+ protected async Task<TmdbCastResult> FetchCastInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ //get cast and crew info
+ var url = string.Format(CastInfo, id, ApiKey, ItemType);
+ TmdbCastResult cast = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ cast = JsonSerializer.DeserializeFromStream<TmdbCastResult>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ return cast;
+ }
+
+ /// <summary>
+ /// Fetches the release info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbReleasesResult}.</returns>
+ protected async Task<TmdbReleasesResult> FetchReleaseInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ var url = string.Format(ReleaseInfo, id, ApiKey);
+ TmdbReleasesResult releases = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ releases = JsonSerializer.DeserializeFromStream<TmdbReleasesResult>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ return releases;
+ }
+
+ /// <summary>
+ /// Fetches the image info.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="id">The id.</param>
+ /// <returns>Task{TmdbImages}.</returns>
+ protected async Task<TmdbImages> FetchImageInfo(BaseItem item, string id, CancellationToken cancellationToken)
+ {
+ //fetch images
+ var url = string.Format(GetImages, id, ApiKey, ItemType);
+ TmdbImages images = null;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ images = JsonSerializer.DeserializeFromStream<TmdbImages>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ return images;
+ }
+
+ /// <summary>
+ /// Processes the main info.
+ /// </summary>
+ /// <param name="movie">The movie.</param>
+ /// <param name="movieData">The movie data.</param>
+ protected virtual void ProcessMainInfo(BaseItem movie, CompleteMovieData movieData)
+ {
+ if (movie != null && movieData != null)
+ {
+
+ movie.Name = movieData.title ?? movieData.original_title ?? movie.Name;
+ movie.Overview = movieData.overview;
+ movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
+ if (!string.IsNullOrEmpty(movieData.tagline)) movie.AddTagline(movieData.tagline);
+ movie.SetProviderId(MetadataProviders.Imdb, movieData.imdb_id);
+ float rating;
+ string voteAvg = movieData.vote_average.ToString();
+ string cultureStr = Kernel.Instance.Configuration.PreferredMetadataLanguage + "-" + Kernel.Instance.Configuration.MetadataCountryCode;
+ CultureInfo culture;
+ try
+ {
+ culture = new CultureInfo(cultureStr);
+ }
+ catch
+ {
+ culture = CultureInfo.CurrentCulture; //default to windows settings if other was invalid
+ }
+ Logger.Debug("Culture for numeric conversion is: " + culture.Name);
+ if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, culture, out rating))
+ movie.CommunityRating = rating;
+
+ //release date and certification are retrieved based on configured country and we fall back on US if not there
+ if (movieData.countries != null)
+ {
+ var ourRelease = movieData.countries.FirstOrDefault(c => c.iso_3166_1.Equals(Kernel.Instance.Configuration.MetadataCountryCode, StringComparison.OrdinalIgnoreCase)) ?? new Country();
+ var usRelease = movieData.countries.FirstOrDefault(c => c.iso_3166_1.Equals("US", StringComparison.OrdinalIgnoreCase)) ?? new Country();
+
+ movie.OfficialRating = ourRelease.certification ?? usRelease.certification;
+ if (ourRelease.release_date > new DateTime(1900, 1, 1))
+ {
+ movie.PremiereDate = ourRelease.release_date;
+ movie.ProductionYear = ourRelease.release_date.Year;
+ }
+ else
+ {
+ movie.PremiereDate = usRelease.release_date;
+ movie.ProductionYear = usRelease.release_date.Year;
+ }
+ }
+ else
+ {
+ //no specific country release info at all
+ movie.PremiereDate = movieData.release_date;
+ movie.ProductionYear = movieData.release_date.Year;
+ }
+
+ //if that didn't find a rating and we are a boxset, use the one from our first child
+ if (movie.OfficialRating == null && movie is BoxSet)
+ {
+ var boxset = movie as BoxSet;
+ Logger.Info("MovieDbProvider - Using rating of first child of boxset...");
+ boxset.OfficialRating = !boxset.Children.IsEmpty ? boxset.Children.First().OfficialRating : null;
+ }
+
+ if (movie.RunTimeTicks == null && movieData.runtime > 0)
+ movie.RunTimeTicks = TimeSpan.FromMinutes(movieData.runtime).Ticks;
+
+ //studios
+ if (movieData.production_companies != null)
+ {
+ //always clear so they don't double up
+ movie.AddStudios(movieData.production_companies.Select(c => c.name));
+ }
+
+ //genres
+ if (movieData.genres != null)
+ {
+ movie.AddGenres(movieData.genres.Select(g => g.name));
+ }
+
+ //Actors, Directors, Writers - all in People
+ //actors come from cast
+ if (movieData.cast != null)
+ {
+ foreach (var actor in movieData.cast.OrderBy(a => a.order)) movie.AddPerson(new PersonInfo { Name = actor.name, Role = actor.character, Type = PersonType.Actor });
+ }
+ //and the rest from crew
+ if (movieData.crew != null)
+ {
+ foreach (var person in movieData.crew) movie.AddPerson(new PersonInfo { Name = person.name, Role = person.job, Type = person.department });
+ }
+
+
+ }
+
+ }
+
+ /// <summary>
+ /// Processes the images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="images">The images.</param>
+ /// <returns>Task.</returns>
+ protected virtual async Task ProcessImages(BaseItem item, TmdbImages images, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // poster
+ if (images.posters != null && images.posters.Count > 0 && (Kernel.Instance.Configuration.RefreshItemImages || !item.HasLocalImage("folder")))
+ {
+ var tmdbSettings = await TmdbSettings.ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedPosterSize;
+ // get highest rated poster for our language
+
+ var postersSortedByVote = images.posters.OrderByDescending(i => i.vote_average);
+
+ var poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 != null && p.iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage, StringComparison.OrdinalIgnoreCase));
+ if (poster == null && !Kernel.Instance.Configuration.PreferredMetadataLanguage.Equals("en"))
+ {
+ // couldn't find our specific language, find english (if that wasn't our language)
+ poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 != null && p.iso_639_1.Equals("en", StringComparison.OrdinalIgnoreCase));
+ }
+ if (poster == null)
+ {
+ //still couldn't find it - try highest rated null one
+ poster = postersSortedByVote.FirstOrDefault(p => p.iso_639_1 == null);
+ }
+ if (poster == null)
+ {
+ //finally - just get the highest rated one
+ poster = postersSortedByVote.FirstOrDefault();
+ }
+ if (poster != null)
+ {
+ try
+ {
+ item.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + poster.file_path, "folder" + Path.GetExtension(poster.file_path), Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // backdrops
+ if (images.backdrops != null && images.backdrops.Count > 0)
+ {
+ item.BackdropImagePaths = new List<string>();
+
+ var tmdbSettings = await TmdbSettings.ConfigureAwait(false);
+
+ var tmdbImageUrl = tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedBackdropSize;
+ //backdrops should be in order of rating. get first n ones
+ var numToFetch = Math.Min(Kernel.Instance.Configuration.MaxBackdrops, images.backdrops.Count);
+ for (var i = 0; i < numToFetch; i++)
+ {
+ var bdName = "backdrop" + (i == 0 ? "" : i.ToString());
+
+ if (Kernel.Instance.Configuration.RefreshItemImages || !item.HasLocalImage(bdName))
+ {
+ try
+ {
+ item.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(item, tmdbImageUrl + images.backdrops[i].file_path, bdName + Path.GetExtension(images.backdrops[i].file_path), Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+
+
+ /// <summary>
+ /// The remove
+ /// </summary>
+ const string remove = "\"'!`?";
+ // "Face/Off" support.
+ /// <summary>
+ /// The spacers
+ /// </summary>
+ const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
+ /// <summary>
+ /// The replace start numbers
+ /// </summary>
+ static readonly Dictionary<string, string> ReplaceStartNumbers = new Dictionary<string, string> {
+ {"1 ","one "},
+ {"2 ","two "},
+ {"3 ","three "},
+ {"4 ","four "},
+ {"5 ","five "},
+ {"6 ","six "},
+ {"7 ","seven "},
+ {"8 ","eight "},
+ {"9 ","nine "},
+ {"10 ","ten "},
+ {"11 ","eleven "},
+ {"12 ","twelve "},
+ {"13 ","thirteen "},
+ {"100 ","one hundred "},
+ {"101 ","one hundred one "}
+ };
+
+ /// <summary>
+ /// The replace end numbers
+ /// </summary>
+ static readonly Dictionary<string, string> ReplaceEndNumbers = new Dictionary<string, string> {
+ {" 1"," i"},
+ {" 2"," ii"},
+ {" 3"," iii"},
+ {" 4"," iv"},
+ {" 5"," v"},
+ {" 6"," vi"},
+ {" 7"," vii"},
+ {" 8"," viii"},
+ {" 9"," ix"},
+ {" 10"," x"}
+ };
+
+ /// <summary>
+ /// Gets the name of the comparable.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="logger">The logger.</param>
+ /// <returns>System.String.</returns>
+ internal static string GetComparableName(string name, ILogger logger)
+ {
+ name = name.ToLower();
+ name = name.Replace("á", "a");
+ name = name.Replace("é", "e");
+ name = name.Replace("í", "i");
+ name = name.Replace("ó", "o");
+ name = name.Replace("ú", "u");
+ name = name.Replace("ü", "u");
+ name = name.Replace("ñ", "n");
+ foreach (var pair in ReplaceStartNumbers)
+ {
+ if (name.StartsWith(pair.Key))
+ {
+ name = name.Remove(0, pair.Key.Length);
+ name = pair.Value + name;
+ logger.Info("MovieDbProvider - Replaced Start Numbers: " + name);
+ }
+ }
+ foreach (var pair in ReplaceEndNumbers)
+ {
+ if (name.EndsWith(pair.Key))
+ {
+ name = name.Remove(name.IndexOf(pair.Key), pair.Key.Length);
+ name = name + pair.Value;
+ logger.Info("MovieDbProvider - Replaced End Numbers: " + name);
+ }
+ }
+ name = name.Normalize(NormalizationForm.FormKD);
+ var sb = new StringBuilder();
+ foreach (var c in name)
+ {
+ if ((int)c >= 0x2B0 && (int)c <= 0x0333)
+ {
+ // skip char modifier and diacritics
+ }
+ else if (remove.IndexOf(c) > -1)
+ {
+ // skip chars we are removing
+ }
+ else if (spacers.IndexOf(c) > -1)
+ {
+ sb.Append(" ");
+ }
+ else if (c == '&')
+ {
+ sb.Append(" and ");
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ name = sb.ToString();
+ name = name.Replace(", the", "");
+ name = name.Replace(" the ", " ");
+ name = name.Replace("the ", "");
+
+ string prev_name;
+ do
+ {
+ prev_name = name;
+ name = name.Replace(" ", " ");
+ } while (name.Length != prev_name.Length);
+
+ return name.Trim();
+ }
+
+ #region Result Objects
+
+
+ /// <summary>
+ /// Class TmdbMovieSearchResult
+ /// </summary>
+ protected class TmdbMovieSearchResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool adult { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrop_path.
+ /// </summary>
+ /// <value>The backdrop_path.</value>
+ public string backdrop_path { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the original_title.
+ /// </summary>
+ /// <value>The original_title.</value>
+ public string original_title { get; set; }
+ /// <summary>
+ /// Gets or sets the release_date.
+ /// </summary>
+ /// <value>The release_date.</value>
+ public string release_date { get; set; }
+ /// <summary>
+ /// Gets or sets the poster_path.
+ /// </summary>
+ /// <value>The poster_path.</value>
+ public string poster_path { get; set; }
+ /// <summary>
+ /// Gets or sets the popularity.
+ /// </summary>
+ /// <value>The popularity.</value>
+ public double popularity { get; set; }
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string title { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_average.
+ /// </summary>
+ /// <value>The vote_average.</value>
+ public double vote_average { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_count.
+ /// </summary>
+ /// <value>The vote_count.</value>
+ public int vote_count { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbMovieSearchResults
+ /// </summary>
+ protected class TmdbMovieSearchResults
+ {
+ /// <summary>
+ /// Gets or sets the page.
+ /// </summary>
+ /// <value>The page.</value>
+ public int page { get; set; }
+ /// <summary>
+ /// Gets or sets the results.
+ /// </summary>
+ /// <value>The results.</value>
+ public List<TmdbMovieSearchResult> results { get; set; }
+ /// <summary>
+ /// Gets or sets the total_pages.
+ /// </summary>
+ /// <value>The total_pages.</value>
+ public int total_pages { get; set; }
+ /// <summary>
+ /// Gets or sets the total_results.
+ /// </summary>
+ /// <value>The total_results.</value>
+ public int total_results { get; set; }
+ }
+
+ /// <summary>
+ /// Class BelongsToCollection
+ /// </summary>
+ protected class BelongsToCollection
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the poster_path.
+ /// </summary>
+ /// <value>The poster_path.</value>
+ public string poster_path { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrop_path.
+ /// </summary>
+ /// <value>The backdrop_path.</value>
+ public string backdrop_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Genre
+ /// </summary>
+ protected class Genre
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class ProductionCompany
+ /// </summary>
+ protected class ProductionCompany
+ {
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ }
+
+ /// <summary>
+ /// Class ProductionCountry
+ /// </summary>
+ protected class ProductionCountry
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class SpokenLanguage
+ /// </summary>
+ protected class SpokenLanguage
+ {
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ }
+
+ /// <summary>
+ /// Class Cast
+ /// </summary>
+ protected class Cast
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the character.
+ /// </summary>
+ /// <value>The character.</value>
+ public string character { get; set; }
+ /// <summary>
+ /// Gets or sets the order.
+ /// </summary>
+ /// <value>The order.</value>
+ public int order { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_path.
+ /// </summary>
+ /// <value>The profile_path.</value>
+ public string profile_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Crew
+ /// </summary>
+ protected class Crew
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string name { get; set; }
+ /// <summary>
+ /// Gets or sets the department.
+ /// </summary>
+ /// <value>The department.</value>
+ public string department { get; set; }
+ /// <summary>
+ /// Gets or sets the job.
+ /// </summary>
+ /// <value>The job.</value>
+ public string job { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_path.
+ /// </summary>
+ /// <value>The profile_path.</value>
+ public object profile_path { get; set; }
+ }
+
+ /// <summary>
+ /// Class Country
+ /// </summary>
+ protected class Country
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the certification.
+ /// </summary>
+ /// <value>The certification.</value>
+ public string certification { get; set; }
+ /// <summary>
+ /// Gets or sets the release_date.
+ /// </summary>
+ /// <value>The release_date.</value>
+ public DateTime release_date { get; set; }
+ }
+
+ //protected class TmdbMovieResult
+ //{
+ // public bool adult { get; set; }
+ // public string backdrop_path { get; set; }
+ // public int belongs_to_collection { get; set; }
+ // public int budget { get; set; }
+ // public List<Genre> genres { get; set; }
+ // public string homepage { get; set; }
+ // public int id { get; set; }
+ // public string imdb_id { get; set; }
+ // public string original_title { get; set; }
+ // public string overview { get; set; }
+ // public double popularity { get; set; }
+ // public string poster_path { get; set; }
+ // public List<ProductionCompany> production_companies { get; set; }
+ // public List<ProductionCountry> production_countries { get; set; }
+ // public string release_date { get; set; }
+ // public int revenue { get; set; }
+ // public int runtime { get; set; }
+ // public List<SpokenLanguage> spoken_languages { get; set; }
+ // public string tagline { get; set; }
+ // public string title { get; set; }
+ // public double vote_average { get; set; }
+ // public int vote_count { get; set; }
+ //}
+
+ /// <summary>
+ /// Class TmdbTitle
+ /// </summary>
+ protected class TmdbTitle
+ {
+ /// <summary>
+ /// Gets or sets the iso_3166_1.
+ /// </summary>
+ /// <value>The iso_3166_1.</value>
+ public string iso_3166_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <value>The title.</value>
+ public string title { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbAltTitleResults
+ /// </summary>
+ protected class TmdbAltTitleResults
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the titles.
+ /// </summary>
+ /// <value>The titles.</value>
+ public List<TmdbTitle> titles { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbCastResult
+ /// </summary>
+ protected class TmdbCastResult
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the cast.
+ /// </summary>
+ /// <value>The cast.</value>
+ public List<Cast> cast { get; set; }
+ /// <summary>
+ /// Gets or sets the crew.
+ /// </summary>
+ /// <value>The crew.</value>
+ public List<Crew> crew { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbReleasesResult
+ /// </summary>
+ protected class TmdbReleasesResult
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the countries.
+ /// </summary>
+ /// <value>The countries.</value>
+ public List<Country> countries { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbImage
+ /// </summary>
+ protected class TmdbImage
+ {
+ /// <summary>
+ /// Gets or sets the file_path.
+ /// </summary>
+ /// <value>The file_path.</value>
+ public string file_path { get; set; }
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int width { get; set; }
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int height { get; set; }
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the aspect_ratio.
+ /// </summary>
+ /// <value>The aspect_ratio.</value>
+ public double aspect_ratio { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_average.
+ /// </summary>
+ /// <value>The vote_average.</value>
+ public double vote_average { get; set; }
+ /// <summary>
+ /// Gets or sets the vote_count.
+ /// </summary>
+ /// <value>The vote_count.</value>
+ public int vote_count { get; set; }
+ }
+
+ /// <summary>
+ /// Class TmdbImages
+ /// </summary>
+ protected class TmdbImages
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int id { get; set; }
+ /// <summary>
+ /// Gets or sets the backdrops.
+ /// </summary>
+ /// <value>The backdrops.</value>
+ public List<TmdbImage> backdrops { get; set; }
+ /// <summary>
+ /// Gets or sets the posters.
+ /// </summary>
+ /// <value>The posters.</value>
+ public List<TmdbImage> posters { get; set; }
+ }
+
+ /// <summary>
+ /// Class CompleteMovieData
+ /// </summary>
+ protected class CompleteMovieData
+ {
+ public bool adult { get; set; }
+ public string backdrop_path { get; set; }
+ public BelongsToCollection belongs_to_collection { get; set; }
+ public int budget { get; set; }
+ public List<Genre> genres { get; set; }
+ public string homepage { get; set; }
+ public int id { get; set; }
+ public string imdb_id { get; set; }
+ public string original_title { get; set; }
+ public string overview { get; set; }
+ public double popularity { get; set; }
+ public string poster_path { get; set; }
+ public List<ProductionCompany> production_companies { get; set; }
+ public List<ProductionCountry> production_countries { get; set; }
+ public DateTime release_date { get; set; }
+ public int revenue { get; set; }
+ public int runtime { get; set; }
+ public List<SpokenLanguage> spoken_languages { get; set; }
+ public string tagline { get; set; }
+ public string title { get; set; }
+ public double vote_average { get; set; }
+ public int vote_count { get; set; }
+ public List<Country> countries { get; set; }
+ public List<Cast> cast { get; set; }
+ public List<Crew> crew { get; set; }
+ }
+
+ public class TmdbImageSettings
+ {
+ public List<string> backdrop_sizes { get; set; }
+ public string base_url { get; set; }
+ public List<string> poster_sizes { get; set; }
+ public List<string> profile_sizes { get; set; }
+ }
+
+ public class TmdbSettingsResult
+ {
+ public TmdbImageSettings images { get; set; }
+ }
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs
new file mode 100644
index 0000000000..ad5f6626be
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromJson.cs
@@ -0,0 +1,100 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class MovieProviderFromJson
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromJson : MovieDbProvider
+ {
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <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 false; }
+ }
+
+ /// <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)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <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)
+ {
+ if (item.ResolveArgs.ContainsMetaFileByName(ALT_META_FILE_NAME))
+ {
+ return false; // don't read our file if 3rd party data exists
+ }
+
+ if (!item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME))
+ {
+ return false; // nothing to read
+ }
+
+ // Need to re-override to jump over intermediate implementation
+ return CompareDate(item) > providerInfo.LastRefreshed;
+ }
+
+ /// <summary>
+ /// Fetches the async.
+ /// </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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ // Since we don't have anything truly async, and since deserializing can be expensive, create a task to force parallelism
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, LOCAL_META_FILE_NAME));
+ if (entry.HasValue)
+ {
+ // read in our saved meta and pass to processing function
+ var movieData = JsonSerializer.DeserializeFromFile<CompleteMovieData>(entry.Value.Path);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ProcessMainInfo(item, movieData);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
index 7ef53d546a..b87c71df3d 100644
--- a/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/Movies/MovieProviderFromXml.cs
@@ -1,43 +1,91 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-using System;
-
-namespace MediaBrowser.Controller.Providers.Movies
-{
- [Export(typeof(BaseMetadataProvider))]
- public class MovieProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Movie;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- protected override DateTime CompareDate(BaseEntity item)
- {
- var entry = item.ResolveArgs.GetFileSystemEntry(Path.Combine(item.Path, "movie.xml"));
- return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("movie.xml"))
- {
- new BaseItemXmlParser<Movie>().Fetch(item as Movie, Path.Combine(args.Path, "movie.xml"));
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class MovieProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class MovieProviderFromXml : BaseMetadataProvider
+ {
+ /// <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 Movie || item is BoxSet;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <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)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "movie.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "movie.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+ var boxset = item as BoxSet;
+ if (boxset != null)
+ {
+ new BaseItemXmlParser<BoxSet>().Fetch(boxset, path, cancellationToken);
+ }
+ else
+ {
+ new BaseItemXmlParser<Movie>().Fetch((Movie)item, path, cancellationToken);
+ }
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs b/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
deleted file mode 100644
index b6b856d292..0000000000
--- a/MediaBrowser.Controller/Providers/Movies/MovieSpecialFeaturesProvider.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.IO;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.Movies
-{
- [Export(typeof(BaseMetadataProvider))]
- public class MovieSpecialFeaturesProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Movie;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public async override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFolder("specials"))
- {
- var items = new List<Video>();
-
- foreach (WIN32_FIND_DATA file in FileData.GetFileSystemEntries(Path.Combine(args.Path, "specials"), "*"))
- {
- var video = await Kernel.Instance.ItemController.GetItem(file.Path, fileInfo: file).ConfigureAwait(false) as Video;
-
- if (video != null)
- {
- items.Add(video);
- }
- }
-
- (item as Movie).SpecialFeatures = items;
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs b/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs
new file mode 100644
index 0000000000..19a707be30
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/PersonProviderFromJson.cs
@@ -0,0 +1,113 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class PersonProviderFromJson
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class PersonProviderFromJson : TmdbPersonProvider
+ {
+ /// <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 Person;
+ }
+
+ /// <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 false;
+ }
+ }
+
+ // Need to re-override to jump over intermediate implementation
+ /// <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)
+ {
+ if (!item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ {
+ return false;
+ }
+
+ return CompareDate(item) > providerInfo.LastRefreshed;
+ }
+
+ /// <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)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation,MetaFileName));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get
+ {
+ return MetadataProviderPriority.Third;
+ }
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var personInfo = JsonSerializer.DeserializeFromFile<PersonResult>(Path.Combine(item.MetaLocation, MetaFileName));
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ ProcessInfo((Person)item, personInfo);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ catch (FileNotFoundException)
+ {
+ // This is okay - just means we force refreshed and there isn't a json file
+ return false;
+ }
+
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs b/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs
new file mode 100644
index 0000000000..4cdfc58940
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/Movies/TmdbPersonProvider.cs
@@ -0,0 +1,465 @@
+using MediaBrowser.Common.Serialization;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.Movies
+{
+ /// <summary>
+ /// Class TmdbPersonProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class TmdbPersonProvider : BaseMetadataProvider
+ {
+ /// <summary>
+ /// The meta file name
+ /// </summary>
+ protected const string MetaFileName = "MBPerson.json";
+
+ /// <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 Person;
+ }
+
+ /// <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)
+ {
+ //we fetch if either info or image needed and haven't already tried recently
+ return (string.IsNullOrEmpty(item.PrimaryImagePath) || !item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ && DateTime.Today.Subtract(providerInfo.LastRefreshed).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ /// <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>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var person = (Person)item;
+ var tasks = new List<Task>();
+
+ var id = person.GetProviderId(MetadataProviders.Tmdb);
+
+ // We don't already have an Id, need to fetch it
+ if (string.IsNullOrEmpty(id))
+ {
+ id = await GetTmdbId(item, cancellationToken).ConfigureAwait(false);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!string.IsNullOrEmpty(id))
+ {
+ //get info only if not already saved
+ if (!item.ResolveArgs.ContainsMetaFileByName(MetaFileName))
+ {
+ tasks.Add(FetchInfo(person, id, cancellationToken));
+ }
+
+ //get image only if not already there
+ if (string.IsNullOrEmpty(item.PrimaryImagePath))
+ {
+ tasks.Add(FetchImages(person, id, cancellationToken));
+ }
+
+ //and wait for them to complete
+ await Task.WhenAll(tasks).ConfigureAwait(false);
+ }
+ else
+ {
+ Logger.Debug("TmdbPersonProvider Unable to obtain id for " + item.Name);
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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 the TMDB id.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> GetTmdbId(BaseItem person, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(person.Name), MovieDbProvider.ApiKey);
+ PersonSearchResults searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonSearchResults>(json);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ return searchResult != null && searchResult.Total_Results > 0 ? searchResult.Results[0].Id.ToString() : null;
+ }
+
+ /// <summary>
+ /// Fetches the info.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="id">The id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchInfo(Person person, string id, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/person/{1}?api_key={0}", MovieDbProvider.ApiKey, id);
+ PersonResult searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (json != null)
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonResult>(json);
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (searchResult != null && searchResult.Biography != null)
+ {
+ ProcessInfo(person, searchResult);
+
+ //save locally
+ var memoryStream = new MemoryStream();
+
+ JsonSerializer.SerializeToStream(searchResult, memoryStream);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(person, Path.Combine(person.MetaLocation, MetaFileName), memoryStream, cancellationToken);
+
+ Logger.Debug("TmdbPersonProvider downloaded and saved information for {0}", person.Name);
+ }
+ }
+
+ /// <summary>
+ /// Processes the info.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="searchResult">The search result.</param>
+ protected void ProcessInfo(Person person, PersonResult searchResult)
+ {
+ person.Overview = searchResult.Biography;
+
+ DateTime date;
+
+ if (DateTime.TryParseExact(searchResult.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+ {
+ person.PremiereDate = date;
+ }
+
+ person.SetProviderId(MetadataProviders.Tmdb, searchResult.Id.ToString());
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="person">The person.</param>
+ /// <param name="id">The id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchImages(Person person, string id, CancellationToken cancellationToken)
+ {
+ string url = string.Format(@"http://api.themoviedb.org/3/person/{1}/images?api_key={0}", MovieDbProvider.ApiKey, id);
+
+ PersonImages searchResult = null;
+
+ try
+ {
+ using (Stream json = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (json != null)
+ {
+ searchResult = JsonSerializer.DeserializeFromStream<PersonImages>(json);
+ }
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (searchResult != null && searchResult.Profiles.Count > 0)
+ {
+ //get our language
+ var profile =
+ searchResult.Profiles.FirstOrDefault(
+ p =>
+ !string.IsNullOrEmpty(p.Iso_639_1) &&
+ p.Iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage,
+ StringComparison.OrdinalIgnoreCase));
+ if (profile == null)
+ {
+ //didn't find our language - try first null one
+ profile =
+ searchResult.Profiles.FirstOrDefault(
+ p =>
+ !string.IsNullOrEmpty(p.Iso_639_1) &&
+ p.Iso_639_1.Equals(Kernel.Instance.Configuration.PreferredMetadataLanguage,
+ StringComparison.OrdinalIgnoreCase));
+
+ }
+ if (profile == null)
+ {
+ //still nothing - just get first one
+ profile = searchResult.Profiles[0];
+ }
+ if (profile != null)
+ {
+ var tmdbSettings = await Kernel.Instance.MetadataProviders.OfType<MovieDbProvider>().First().TmdbSettings.ConfigureAwait(false);
+
+ var img = await DownloadAndSaveImage(person, tmdbSettings.images.base_url + Kernel.Instance.Configuration.TmdbFetchedProfileSize + profile.File_Path,
+ "folder" + Path.GetExtension(profile.File_Path), cancellationToken).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(img))
+ {
+ person.PrimaryImagePath = img;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Downloads the and save image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="targetName">Name of the target.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, CancellationToken cancellationToken)
+ {
+ if (source == null) return null;
+
+ //download and save locally (if not already there)
+ var localPath = Path.Combine(item.MetaLocation, targetName);
+ if (!item.ResolveArgs.ContainsMetaFileByName(targetName))
+ {
+ using (var sourceStream = await Kernel.Instance.HttpManager.FetchToMemoryStream(source, Kernel.Instance.ResourcePools.MovieDb, cancellationToken).ConfigureAwait(false))
+ {
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(item, localPath, sourceStream, cancellationToken).ConfigureAwait(false);
+
+ Logger.Debug("TmdbPersonProvider downloaded and saved image for {0}", item.Name);
+ }
+ }
+ return localPath;
+ }
+
+ #region Result Objects
+ /// <summary>
+ /// Class PersonSearchResult
+ /// </summary>
+ public class PersonSearchResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_ path.
+ /// </summary>
+ /// <value>The profile_ path.</value>
+ public string Profile_Path { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonSearchResults
+ /// </summary>
+ public class PersonSearchResults
+ {
+ /// <summary>
+ /// Gets or sets the page.
+ /// </summary>
+ /// <value>The page.</value>
+ public int Page { get; set; }
+ /// <summary>
+ /// Gets or sets the results.
+ /// </summary>
+ /// <value>The results.</value>
+ public List<PersonSearchResult> Results { get; set; }
+ /// <summary>
+ /// Gets or sets the total_ pages.
+ /// </summary>
+ /// <value>The total_ pages.</value>
+ public int Total_Pages { get; set; }
+ /// <summary>
+ /// Gets or sets the total_ results.
+ /// </summary>
+ /// <value>The total_ results.</value>
+ public int Total_Results { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonResult
+ /// </summary>
+ public class PersonResult
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="PersonResult" /> is adult.
+ /// </summary>
+ /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+ public bool Adult { get; set; }
+ /// <summary>
+ /// Gets or sets the also_ known_ as.
+ /// </summary>
+ /// <value>The also_ known_ as.</value>
+ public List<object> Also_Known_As { get; set; }
+ /// <summary>
+ /// Gets or sets the biography.
+ /// </summary>
+ /// <value>The biography.</value>
+ public string Biography { get; set; }
+ /// <summary>
+ /// Gets or sets the birthday.
+ /// </summary>
+ /// <value>The birthday.</value>
+ public string Birthday { get; set; }
+ /// <summary>
+ /// Gets or sets the deathday.
+ /// </summary>
+ /// <value>The deathday.</value>
+ public string Deathday { get; set; }
+ /// <summary>
+ /// Gets or sets the homepage.
+ /// </summary>
+ /// <value>The homepage.</value>
+ public string Homepage { get; set; }
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name { get; set; }
+ /// <summary>
+ /// Gets or sets the place_ of_ birth.
+ /// </summary>
+ /// <value>The place_ of_ birth.</value>
+ public string Place_Of_Birth { get; set; }
+ /// <summary>
+ /// Gets or sets the profile_ path.
+ /// </summary>
+ /// <value>The profile_ path.</value>
+ public string Profile_Path { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonProfile
+ /// </summary>
+ public class PersonProfile
+ {
+ /// <summary>
+ /// Gets or sets the aspect_ ratio.
+ /// </summary>
+ /// <value>The aspect_ ratio.</value>
+ public double Aspect_Ratio { get; set; }
+ /// <summary>
+ /// Gets or sets the file_ path.
+ /// </summary>
+ /// <value>The file_ path.</value>
+ public string File_Path { get; set; }
+ /// <summary>
+ /// Gets or sets the height.
+ /// </summary>
+ /// <value>The height.</value>
+ public int Height { get; set; }
+ /// <summary>
+ /// Gets or sets the iso_639_1.
+ /// </summary>
+ /// <value>The iso_639_1.</value>
+ public string Iso_639_1 { get; set; }
+ /// <summary>
+ /// Gets or sets the width.
+ /// </summary>
+ /// <value>The width.</value>
+ public int Width { get; set; }
+ }
+
+ /// <summary>
+ /// Class PersonImages
+ /// </summary>
+ public class PersonImages
+ {
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <value>The id.</value>
+ public int Id { get; set; }
+ /// <summary>
+ /// Gets or sets the profiles.
+ /// </summary>
+ /// <value>The profiles.</value>
+ public List<PersonProfile> Profiles { get; set; }
+ }
+
+ #endregion
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/ProviderManager.cs b/MediaBrowser.Controller/Providers/ProviderManager.cs
new file mode 100644
index 0000000000..0d5d497e8f
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/ProviderManager.cs
@@ -0,0 +1,332 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Kernel;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class ProviderManager
+ /// </summary>
+ public class ProviderManager : BaseManager<Kernel>
+ {
+ /// <summary>
+ /// The remote image cache
+ /// </summary>
+ private readonly FileSystemRepository _remoteImageCache;
+
+ /// <summary>
+ /// The currently running metadata providers
+ /// </summary>
+ private readonly ConcurrentDictionary<string, Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>> _currentlyRunningProviders =
+ new ConcurrentDictionary<string, Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>>();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ProviderManager" /> class.
+ /// </summary>
+ /// <param name="kernel">The kernel.</param>
+ public ProviderManager(Kernel kernel)
+ : base(kernel)
+ {
+ _remoteImageCache = new FileSystemRepository(ImagesDataPath);
+ }
+
+ /// <summary>
+ /// The _images data path
+ /// </summary>
+ private string _imagesDataPath;
+ /// <summary>
+ /// Gets the images data path.
+ /// </summary>
+ /// <value>The images data path.</value>
+ public string ImagesDataPath
+ {
+ get
+ {
+ if (_imagesDataPath == null)
+ {
+ _imagesDataPath = Path.Combine(Kernel.ApplicationPaths.DataPath, "remote-images");
+
+ if (!Directory.Exists(_imagesDataPath))
+ {
+ Directory.CreateDirectory(_imagesDataPath);
+ }
+ }
+
+ return _imagesDataPath;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the supported providers key.
+ /// </summary>
+ /// <value>The supported providers key.</value>
+ private Guid SupportedProvidersKey { get; set; }
+
+ /// <summary>
+ /// Runs all metadata providers for an entity, and returns true or false indicating if at least one was refreshed and requires persistence
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="force">if set to <c>true</c> [force].</param>
+ /// <param name="allowSlowProviders">if set to <c>true</c> [allow slow providers].</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ internal async Task<bool> ExecuteMetadataProviders(BaseItem item, CancellationToken cancellationToken, bool force = false, bool allowSlowProviders = true)
+ {
+ // Allow providers of the same priority to execute in parallel
+ MetadataProviderPriority? currentPriority = null;
+ var currentTasks = new List<Task<bool>>();
+
+ var result = false;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Determine if supported providers have changed
+ var supportedProviders = Kernel.MetadataProviders.Where(p => p.Supports(item)).ToList();
+
+ BaseProviderInfo supportedProvidersInfo;
+
+ if (SupportedProvidersKey == Guid.Empty)
+ {
+ SupportedProvidersKey = "SupportedProviders".GetMD5();
+ }
+
+ var supportedProvidersHash = string.Join("+", supportedProviders.Select(i => i.GetType().Name)).GetMD5();
+ bool providersChanged;
+
+ item.ProviderData.TryGetValue(SupportedProvidersKey, out supportedProvidersInfo);
+ if (supportedProvidersInfo == null)
+ {
+ // First time
+ supportedProvidersInfo = new BaseProviderInfo { ProviderId = SupportedProvidersKey, FileSystemStamp = supportedProvidersHash };
+ providersChanged = force = true;
+ }
+ else
+ {
+ // Force refresh if the supported providers have changed
+ providersChanged = force = force || supportedProvidersInfo.FileSystemStamp != supportedProvidersHash;
+ }
+
+ // If providers have changed, clear provider info and update the supported providers hash
+ if (providersChanged)
+ {
+ Logger.Debug("Providers changed for {0}. Clearing and forcing refresh.", item.Name);
+ item.ProviderData.Clear();
+ supportedProvidersInfo.FileSystemStamp = supportedProvidersHash;
+ }
+
+ if (force) item.ClearMetaValues();
+
+ // Run the normal providers sequentially in order of priority
+ foreach (var provider in supportedProviders)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Skip if internet providers are currently disabled
+ if (provider.RequiresInternet && !Kernel.Configuration.EnableInternetProviders)
+ {
+ continue;
+ }
+
+ // Skip if is slow and we aren't allowing slow ones
+ if (provider.IsSlow && !allowSlowProviders)
+ {
+ continue;
+ }
+
+ // Skip if internet provider and this type is not allowed
+ if (provider.RequiresInternet && Kernel.Configuration.EnableInternetProviders && Kernel.Configuration.InternetProviderExcludeTypes.Contains(item.GetType().Name, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // When a new priority is reached, await the ones that are currently running and clear the list
+ if (currentPriority.HasValue && currentPriority.Value != provider.Priority && currentTasks.Count > 0)
+ {
+ var results = await Task.WhenAll(currentTasks).ConfigureAwait(false);
+ result |= results.Contains(true);
+
+ currentTasks.Clear();
+ }
+
+ // Put this check below the await because the needs refresh of the next tier of providers may depend on the previous ones running
+ // This is the case for the fan art provider which depends on the movie and tv providers having run before them
+ if (!force && !provider.NeedsRefresh(item))
+ {
+ continue;
+ }
+
+ currentTasks.Add(provider.FetchAsync(item, force, cancellationToken));
+ currentPriority = provider.Priority;
+ }
+
+ if (currentTasks.Count > 0)
+ {
+ var results = await Task.WhenAll(currentTasks).ConfigureAwait(false);
+ result |= results.Contains(true);
+ }
+
+ if (providersChanged)
+ {
+ item.ProviderData[SupportedProvidersKey] = supportedProvidersInfo;
+ }
+
+ return result || providersChanged;
+ }
+
+ /// <summary>
+ /// Notifies the kernal that a provider has begun refreshing
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationTokenSource">The cancellation token source.</param>
+ internal void OnProviderRefreshBeginning(BaseMetadataProvider provider, BaseItem item, CancellationTokenSource cancellationTokenSource)
+ {
+ var key = item.Id + provider.GetType().Name;
+
+ Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource> current;
+
+ if (_currentlyRunningProviders.TryGetValue(key, out current))
+ {
+ try
+ {
+ current.Item3.Cancel();
+ }
+ catch (ObjectDisposedException)
+ {
+
+ }
+ }
+
+ var tuple = new Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource>(provider, item, cancellationTokenSource);
+
+ _currentlyRunningProviders.AddOrUpdate(key, tuple, (k, v) => tuple);
+ }
+
+ /// <summary>
+ /// Notifies the kernal that a provider has completed refreshing
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ /// <param name="item">The item.</param>
+ internal void OnProviderRefreshCompleted(BaseMetadataProvider provider, BaseItem item)
+ {
+ var key = item.Id + provider.GetType().Name;
+
+ Tuple<BaseMetadataProvider, BaseItem, CancellationTokenSource> current;
+
+ if (_currentlyRunningProviders.TryRemove(key, out current))
+ {
+ current.Item3.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Validates the currently running providers and cancels any that should not be run due to configuration changes
+ /// </summary>
+ internal void ValidateCurrentlyRunningProviders()
+ {
+ Logger.Info("Validing currently running providers");
+
+ var enableInternetProviders = Kernel.Configuration.EnableInternetProviders;
+ var internetProviderExcludeTypes = Kernel.Configuration.InternetProviderExcludeTypes;
+
+ foreach (var tuple in _currentlyRunningProviders.Values
+ .Where(p => p.Item1.RequiresInternet && (!enableInternetProviders || internetProviderExcludeTypes.Contains(p.Item2.GetType().Name, StringComparer.OrdinalIgnoreCase)))
+ .ToList())
+ {
+ tuple.Item3.Cancel();
+ }
+ }
+
+ /// <summary>
+ /// Downloads the and save image.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="source">The source.</param>
+ /// <param name="targetName">Name of the target.</param>
+ /// <param name="resourcePool">The resource pool.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ /// <exception cref="System.ArgumentNullException">item</exception>
+ public async Task<string> DownloadAndSaveImage(BaseItem item, string source, string targetName, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException("item");
+ }
+ if (string.IsNullOrEmpty(source))
+ {
+ throw new ArgumentNullException("source");
+ }
+ if (string.IsNullOrEmpty(targetName))
+ {
+ throw new ArgumentNullException("targetName");
+ }
+ if (resourcePool == null)
+ {
+ throw new ArgumentNullException("resourcePool");
+ }
+
+ //download and save locally
+ var localPath = Kernel.Configuration.SaveLocalMeta ?
+ Path.Combine(item.MetaLocation, targetName) :
+ _remoteImageCache.GetResourcePath(item.GetType().FullName + item.Path.ToLower(), targetName);
+
+ var img = await Kernel.HttpManager.FetchToMemoryStream(source, resourcePool, cancellationToken).ConfigureAwait(false);
+
+ if (Kernel.Configuration.SaveLocalMeta) // queue to media directories
+ {
+ await Kernel.FileSystemManager.SaveToLibraryFilesystem(item, localPath, img, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // we can write directly here because it won't affect the watchers
+
+ try
+ {
+ using (var fs = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ await img.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ Logger.ErrorException("Error downloading and saving image " + localPath, e);
+ throw;
+ }
+ finally
+ {
+ img.Dispose();
+ }
+
+ }
+ return localPath;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected override void Dispose(bool dispose)
+ {
+ if (dispose)
+ {
+ _remoteImageCache.Dispose();
+ }
+
+ base.Dispose(dispose);
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/SortNameProvider.cs b/MediaBrowser.Controller/Providers/SortNameProvider.cs
new file mode 100644
index 0000000000..071732f3e3
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/SortNameProvider.cs
@@ -0,0 +1,129 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.TV;
+using System.ComponentModel.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers
+{
+ /// <summary>
+ /// Class SortNameProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class SortNameProvider : BaseMetadataProvider
+ {
+ /// <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 true;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Last; }
+ }
+
+ /// <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)
+ {
+ return !string.IsNullOrEmpty(item.Name) && string.IsNullOrEmpty(item.SortName);
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return SetSortName(item, cancellationToken) ? TrueTaskResult : FalseTaskResult;
+ }
+
+ /// <summary>
+ /// Sets the name of the sort.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ protected bool SetSortName(BaseItem item, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(item.SortName)) return false; //let the earlier provider win
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (item is Episode)
+ {
+ //special handling for TV episodes season and episode number
+ item.SortName = (item.ParentIndexNumber != null ? item.ParentIndexNumber.Value.ToString("000-") : "")
+ + (item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000 - ") : "") + item.Name;
+
+ }
+ else if (item is Season)
+ {
+ //sort seasons by season number - numerically
+ item.SortName = item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000") : item.Name;
+ }
+ else if (item is Audio)
+ {
+ //sort tracks by production year and index no so they will sort in order if in a multi-album list
+ item.SortName = (item.ProductionYear != null ? item.ProductionYear.Value.ToString("000-") : "")
+ + (item.IndexNumber != null ? item.IndexNumber.Value.ToString("0000 - ") : "") + item.Name;
+ }
+ else if (item is MusicAlbum)
+ {
+ //sort albums by year
+ item.SortName = item.ProductionYear != null ? item.ProductionYear.Value.ToString("0000") : item.Name;
+ }
+ else
+ {
+ if (item.Name == null) return false; //some items may not have name filled in properly
+
+ var sortable = item.Name.Trim().ToLower();
+ sortable = Kernel.Instance.Configuration.SortRemoveCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), string.Empty));
+
+ sortable = Kernel.Instance.Configuration.SortReplaceCharacters.Aggregate(sortable, (current, search) => current.Replace(search.ToLower(), " "));
+
+ foreach (var search in Kernel.Instance.Configuration.SortRemoveWords)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var searchLower = search.ToLower();
+ // Remove from beginning if a space follows
+ if (sortable.StartsWith(searchLower + " "))
+ {
+ sortable = sortable.Remove(0, searchLower.Length + 1);
+ }
+ // Remove from middle if surrounded by spaces
+ sortable = sortable.Replace(" " + searchLower + " ", " ");
+
+ // Remove from end if followed by a space
+ if (sortable.EndsWith(" " + searchLower))
+ {
+ sortable = sortable.Remove(sortable.Length - (searchLower.Length + 1));
+ }
+ }
+ item.SortName = sortable;
+ }
+
+ return true;
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
index 0b9cf85ebe..a493ce746f 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeImageFromMediaLocationProvider.cs
@@ -1,67 +1,127 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Episode;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- return Task.Run(() =>
- {
- var episode = item as Episode;
-
- string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
-
- string episodeFileName = Path.GetFileName(episode.Path);
-
- var season = args.Parent as Season;
-
- SetPrimaryImagePath(episode, season, metadataFolder, episodeFileName);
- });
- }
-
- private void SetPrimaryImagePath(Episode item, Season season, string metadataFolder, string episodeFileName)
- {
- // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
- var imageFiles = new string[] {
- Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
- Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
- };
-
- string image;
-
- if (season == null)
- {
- // Epsiode directly in Series folder. Gotta do this the slow way
- image = imageFiles.FirstOrDefault(f => File.Exists(f));
- }
- else
- {
- image = imageFiles.FirstOrDefault(f => season.ContainsMetadataFile(f));
- }
-
- // If we found something, set PrimaryImagePath
- if (!string.IsNullOrEmpty(image))
- {
- item.PrimaryImagePath = image;
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeImageFromMediaLocationProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeImageFromMediaLocationProvider : BaseMetadataProvider
+ {
+ /// <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>
+ /// 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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var episode = (Episode)item;
+
+ var episodeFileName = Path.GetFileName(episode.Path);
+
+ var parent = item.ResolveArgs.Parent;
+
+ ValidateImage(episode, item.MetaLocation);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ SetPrimaryImagePath(episode, parent, item.MetaLocation, episodeFileName);
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return TrueTaskResult;
+ }
+
+ /// <summary>
+ /// Validates the primary image path still exists
+ /// </summary>
+ /// <param name="episode">The episode.</param>
+ /// <param name="metadataFolderPath">The metadata folder path.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private void ValidateImage(Episode episode, string metadataFolderPath)
+ {
+ var path = episode.PrimaryImagePath;
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return;
+ }
+
+ // Only validate images in the season/metadata folder
+ if (!string.Equals(Path.GetDirectoryName(path), metadataFolderPath, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ if (!episode.Parent.ResolveArgs.GetMetaFileByPath(path).HasValue)
+ {
+ episode.PrimaryImagePath = 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)
+ {
+ // Look for the image file in the metadata folder, and if found, set PrimaryImagePath
+ var imageFiles = new[] {
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".jpg")),
+ Path.Combine(metadataFolder, Path.ChangeExtension(episodeFileName, ".png"))
+ };
+
+ var file = parent.ResolveArgs.GetMetaFileByPath(imageFiles[0]) ??
+ parent.ResolveArgs.GetMetaFileByPath(imageFiles[1]);
+
+ if (file.HasValue)
+ {
+ item.PrimaryImagePath = file.Value.Path;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
index f3c19a7048..df46b7167f 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeProviderFromXml.cs
@@ -1,59 +1,122 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class EpisodeProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Episode;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- string metadataFolder = Path.Combine(args.Parent.Path, "metadata");
-
- string metadataFile = Path.Combine(metadataFolder, Path.ChangeExtension(Path.GetFileName(args.Path), ".xml"));
-
- FetchMetadata(item as Episode, args.Parent as Season, metadataFile);
- }
-
- private void FetchMetadata(Episode item, Season season, string metadataFile)
- {
- if (season == null)
- {
- // Episode directly in Series folder
- // Need to validate it the slow way
- if (!File.Exists(metadataFile))
- {
- return;
- }
- }
- else
- {
- if (!season.ContainsMetadataFile(metadataFile))
- {
- return;
- }
- }
-
- new EpisodeXmlParser().Fetch(item, metadataFile);
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class EpisodeProviderFromXml : BaseMetadataProvider
+ {
+ /// <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; }
+ }
+
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <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)
+ {
+ var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
+
+ var file = item.ResolveArgs.Parent.ResolveArgs.GetMetaFileByPath(metadataFile);
+
+ if (!file.HasValue)
+ {
+ return base.CompareDate(item);
+ }
+
+ return file.Value.LastWriteTimeUtc;
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = Path.Combine(item.MetaLocation, Path.ChangeExtension(Path.GetFileName(item.Path), ".xml"));
+
+ var episode = (Episode)item;
+
+ if (!FetchMetadata(episode, item.ResolveArgs.Parent, metadataFile, cancellationToken))
+ {
+ // Don't set last refreshed if we didn't do anything
+ return false;
+ }
+
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+
+ /// <summary>
+ /// Fetches the metadata.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="parent">The parent.</param>
+ /// <param name="metadataFile">The metadata file.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool FetchMetadata(Episode item, Folder parent, string metadataFile, CancellationToken cancellationToken)
+ {
+ var file = parent.ResolveArgs.GetMetaFileByPath(metadataFile);
+
+ if (!file.HasValue)
+ {
+ return false;
+ }
+
+ new EpisodeXmlParser().Fetch(item, metadataFile, cancellationToken);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
index 1cb604a51c..7133d87451 100644
--- a/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/TV/EpisodeXmlParser.cs
@@ -1,60 +1,104 @@
-using MediaBrowser.Controller.Entities.TV;
-using System.IO;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- public class EpisodeXmlParser : BaseItemXmlParser<Episode>
- {
- protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
- {
- switch (reader.Name)
- {
- case "filename":
- {
- string filename = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(filename))
- {
- // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
- // even though it's actually using the metadata folder.
- filename = Path.GetFileName(filename);
-
- string seasonFolder = Path.GetDirectoryName(item.Path);
- item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
- }
- break;
- }
- case "SeasonNumber":
- {
- string number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- item.ParentIndexNumber = int.Parse(number);
- }
- break;
- }
-
- case "EpisodeNumber":
- {
- string number = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(number))
- {
- item.IndexNumber = int.Parse(number);
- }
- break;
- }
-
- case "EpisodeName":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- default:
- base.FetchDataFromXmlNode(reader, item);
- break;
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities.TV;
+using System.IO;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class EpisodeXmlParser
+ /// </summary>
+ public class EpisodeXmlParser : BaseItemXmlParser<Episode>
+ {
+ /// <summary>
+ /// Fetches the data from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected override void FetchDataFromXmlNode(XmlReader reader, Episode item)
+ {
+ switch (reader.Name)
+ {
+ case "Episode":
+ //MB generated metadata is within an "Episode" node
+ using (var subTree = reader.ReadSubtree())
+ {
+ subTree.MoveToContent();
+
+ // Loop through each element
+ while (subTree.Read())
+ {
+ if (subTree.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(subTree, item);
+ }
+ }
+
+ }
+ break;
+
+ case "filename":
+ {
+ string filename = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(filename))
+ {
+ // Strip off everything but the filename. Some metadata tools like MetaBrowser v1.0 will have an 'episodes' prefix
+ // even though it's actually using the metadata folder.
+ filename = Path.GetFileName(filename);
+
+ string seasonFolder = Path.GetDirectoryName(item.Path);
+ item.PrimaryImagePath = Path.Combine(seasonFolder, "metadata", filename);
+ }
+ break;
+ }
+ case "SeasonNumber":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.ParentIndexNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "EpisodeNumber":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.IndexNumber = num;
+ }
+ }
+ break;
+ }
+
+ case "EpisodeName":
+ {
+ var name = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ item.Name = name;
+ }
+ break;
+ }
+
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs b/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs
new file mode 100644
index 0000000000..2640e04825
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/FanArtTVProvider.cs
@@ -0,0 +1,140 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ [Export(typeof(BaseMetadataProvider))]
+ class FanArtTVProvider : FanartBaseProvider
+ {
+ protected string FanArtBaseUrl = "http://api.fanart.tv/webservice/series/{0}/{1}/xml/all/1/1";
+
+ public override bool Supports(BaseItem item)
+ {
+ return item is Series;
+ }
+
+ protected override bool ShouldFetch(BaseItem item, BaseProviderInfo providerInfo)
+ {
+ if (item.DontFetchMeta || string.IsNullOrEmpty(item.GetProviderId(MetadataProviders.Tvdb))) return false; //nothing to do
+ var artExists = item.ResolveArgs.ContainsMetaFileByName(ART_FILE);
+ var logoExists = item.ResolveArgs.ContainsMetaFileByName(LOGO_FILE);
+ var thumbExists = item.ResolveArgs.ContainsMetaFileByName(THUMB_FILE);
+
+
+ return (!artExists && Kernel.Instance.Configuration.DownloadTVArt)
+ || (!logoExists && Kernel.Instance.Configuration.DownloadTVLogo)
+ || (!thumbExists && Kernel.Instance.Configuration.DownloadTVThumb);
+ }
+
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var series = (Series)item;
+ if (ShouldFetch(series, series.ProviderData.GetValueOrDefault(Id, new BaseProviderInfo { ProviderId = Id })))
+ {
+ string language = Kernel.Instance.Configuration.PreferredMetadataLanguage.ToLower();
+ string url = string.Format(FanArtBaseUrl, APIKey, series.GetProviderId(MetadataProviders.Tvdb));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (doc.HasChildNodes)
+ {
+ string path;
+ if (Kernel.Instance.Configuration.DownloadTVLogo && !series.ResolveArgs.ContainsMetaFileByName(LOGO_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/clearlogos/clearlogo/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearLogo for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Logo, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, LOGO_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadTVArt && !series.ResolveArgs.ContainsMetaFileByName(ART_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/cleararts/clearart[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/cleararts/clearart/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ClearArt for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Art, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, ART_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Kernel.Instance.Configuration.DownloadTVThumb && !series.ResolveArgs.ContainsMetaFileByName(THUMB_FILE))
+ {
+ var node = doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb[@lang = \"" + language + "\"]/@url") ??
+ doc.SelectSingleNode("//fanart/series/tvthumbs/tvthumb/@url");
+ path = node != null ? node.Value : null;
+ if (!string.IsNullOrEmpty(path))
+ {
+ Logger.Debug("FanArtProvider getting ThumbArt for " + series.Name);
+ try
+ {
+ series.SetImage(ImageType.Disc, await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, path, THUMB_FILE, Kernel.Instance.ResourcePools.FanArt, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ SetLastRefreshed(series, DateTime.UtcNow);
+ return true;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs
new file mode 100644
index 0000000000..dbe744ed40
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteEpisodeProvider.cs
@@ -0,0 +1,288 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+
+ /// <summary>
+ /// Class RemoteEpisodeProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteEpisodeProvider : BaseMetadataProvider
+ {
+
+ /// <summary>
+ /// The episode query
+ /// </summary>
+ private const string episodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/default/{2}/{3}/{4}.xml";
+ /// <summary>
+ /// The abs episode query
+ /// </summary>
+ private const string absEpisodeQuery = "http://www.thetvdb.com/api/{0}/series/{1}/absolute/{2}/{3}.xml";
+
+ /// <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;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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; }
+ }
+
+ protected override bool RefreshOnFileSystemStampChange
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ /// <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)
+ {
+ bool fetch = false;
+ var episode = (Episode)item;
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (!item.DontFetchMeta && !HasLocalMeta(episode))
+ {
+ fetch = Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.Today.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ return fetch;
+ }
+
+ /// <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>
+ /// <returns>Task{System.Boolean}.</returns>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var episode = (Episode)item;
+ if (!item.DontFetchMeta && !HasLocalMeta(episode))
+ {
+ var seriesId = episode.Series != null ? episode.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+ if (seriesId != null)
+ {
+ await FetchEpisodeData(episode, seriesId, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Episode provider cannot determine Series Id for " + item.Path);
+ return false;
+ }
+ Logger.Info("Episode provider not fetching because local meta exists or requested to ignore: " + item.Name);
+ return false;
+ }
+
+
+ /// <summary>
+ /// Fetches the episode data.
+ /// </summary>
+ /// <param name="episode">The episode.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchEpisodeData(Episode episode, string seriesId, CancellationToken cancellationToken)
+ {
+
+ string name = episode.Name;
+ string location = episode.Path;
+
+ Logger.Debug("TvDbProvider: Fetching episode data for: " + name);
+ string epNum = TVUtils.EpisodeNumberFromFile(location, episode.Season != null);
+
+ if (epNum == null)
+ {
+ Logger.Warn("TvDbProvider: Could not determine episode number for: " + episode.Path);
+ return false;
+ }
+
+ var episodeNumber = Int32.Parse(epNum);
+
+ episode.IndexNumber = episodeNumber;
+ var usingAbsoluteData = false;
+
+ if (string.IsNullOrEmpty(seriesId)) return false;
+
+ var seasonNumber = "";
+ if (episode.Parent is Season)
+ {
+ seasonNumber = episode.Parent.IndexNumber.ToString();
+ }
+
+ if (string.IsNullOrEmpty(seasonNumber))
+ seasonNumber = TVUtils.SeasonNumberFromEpisodeFile(location); // try and extract the season number from the file name for S1E1, 1x04 etc.
+
+ if (!string.IsNullOrEmpty(seasonNumber))
+ {
+ seasonNumber = seasonNumber.TrimStart('0');
+
+ if (string.IsNullOrEmpty(seasonNumber))
+ {
+ seasonNumber = "0"; // Specials
+ }
+
+ var url = string.Format(episodeQuery, TVUtils.TVDBApiKey, seriesId, seasonNumber, episodeNumber, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var result = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(result);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ //episode does not exist under this season, try absolute numbering.
+ //still assuming it's numbered as 1x01
+ //this is basicly just for anime.
+ if (!doc.HasChildNodes && Int32.Parse(seasonNumber) == 1)
+ {
+ url = string.Format(absEpisodeQuery, TVUtils.TVDBApiKey, seriesId, episodeNumber, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+
+ try
+ {
+ using (var result = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ if (result != null) doc.Load(result);
+ usingAbsoluteData = true;
+ }
+ }
+ catch (HttpException)
+ {
+ }
+ }
+
+ if (doc.HasChildNodes)
+ {
+ var p = doc.SafeGetString("//filename");
+ if (p != null)
+ {
+ if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
+
+ try
+ {
+ episode.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(episode, TVUtils.BannerUrl + p, Path.GetFileName(p), Kernel.Instance.ResourcePools.TvDb, cancellationToken);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+
+ episode.Overview = doc.SafeGetString("//Overview");
+ if (usingAbsoluteData)
+ episode.IndexNumber = doc.SafeGetInt32("//absolute_number", -1);
+ if (episode.IndexNumber < 0)
+ episode.IndexNumber = doc.SafeGetInt32("//EpisodeNumber");
+
+ episode.Name = episode.IndexNumber.Value.ToString("000") + " - " + doc.SafeGetString("//EpisodeName");
+ episode.CommunityRating = doc.SafeGetSingle("//Rating", -1, 10);
+ var firstAired = doc.SafeGetString("//FirstAired");
+ DateTime airDate;
+ if (DateTime.TryParse(firstAired, out airDate) && airDate.Year > 1850)
+ {
+ episode.PremiereDate = airDate.ToUniversalTime();
+ episode.ProductionYear = airDate.Year;
+ }
+
+ var actors = doc.SafeGetString("//GuestStars");
+ if (actors != null)
+ {
+ episode.AddPeople(actors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Actor", Name = str }));
+ }
+
+
+ var directors = doc.SafeGetString("//Director");
+ if (directors != null)
+ {
+ episode.AddPeople(directors.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Director", Name = str }));
+ }
+
+
+ var writers = doc.SafeGetString("//Writer");
+ if (writers != null)
+ {
+ episode.AddPeople(writers.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(str => new PersonInfo { Type = "Writer", Name = str }));
+ }
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ if (!Directory.Exists(episode.MetaLocation)) Directory.CreateDirectory(episode.MetaLocation);
+ var ms = new MemoryStream();
+ doc.Save(ms);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(episode, Path.Combine(episode.MetaLocation, Path.GetFileNameWithoutExtension(episode.Path) + ".xml"), ms, cancellationToken).ConfigureAwait(false);
+ }
+
+ return true;
+ }
+
+ }
+
+ return false;
+ }
+
+ /// <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(Episode episode)
+ {
+ return (episode.Parent.ResolveArgs.ContainsMetaFileByName(Path.GetFileNameWithoutExtension(episode.Path) + ".xml"));
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs
new file mode 100644
index 0000000000..0d6cc41b8d
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteSeasonProvider.cs
@@ -0,0 +1,287 @@
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class RemoteSeasonProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteSeasonProvider : BaseMetadataProvider
+ {
+
+ /// <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 Season;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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>
+ /// 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)
+ {
+ bool fetch = false;
+ var downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ return false;
+
+ if (!HasLocalMeta(item))
+ {
+ fetch = Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.UtcNow.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays;
+ }
+
+ return fetch;
+ }
+
+ /// <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>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var season = (Season)item;
+
+ if (!HasLocalMeta(item))
+ {
+ var seriesId = season.Series != null ? season.Series.GetProviderId(MetadataProviders.Tvdb) : null;
+
+ if (seriesId != null)
+ {
+ await FetchSeasonData(season, seriesId, cancellationToken).ConfigureAwait(false);
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Season provider unable to obtain series id for {0}", item.Path);
+ }
+ else
+ {
+ Logger.Info("Season provider not fetching because local meta exists: " + season.Name);
+ }
+ return false;
+ }
+
+
+ /// <summary>
+ /// Fetches the season data.
+ /// </summary>
+ /// <param name="season">The season.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchSeasonData(Season season, string seriesId, CancellationToken cancellationToken)
+ {
+ string name = season.Name;
+
+ Logger.Debug("TvDbProvider: Fetching season data: " + name);
+ var seasonNumber = TVUtils.GetSeasonNumberFromPath(season.Path) ?? -1;
+
+ season.IndexNumber = seasonNumber;
+
+ if (seasonNumber == 0)
+ {
+ season.Name = "Specials";
+ }
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+ if ((season.PrimaryImagePath == null) || (!season.HasImage(ImageType.Banner)) || (season.BackdropImagePaths == null))
+ {
+ var images = new XmlDocument();
+ var url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TVDBApiKey + "/series/{0}/banners.xml", seriesId);
+
+ try
+ {
+ using (var imgs = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ images.Load(imgs);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (images.HasChildNodes)
+ {
+ if (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("folder"))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='season'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+
+ try
+ {
+ if (n != null)
+ season.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVSeasonBanner && (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("banner")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='season'][BannerType2='seasonwide'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ var bannerImagePath =
+ await
+ Kernel.Instance.ProviderManager.DownloadAndSaveImage(season,
+ TVUtils.BannerUrl + n.InnerText,
+ "banner" +
+ Path.GetExtension(n.InnerText),
+ Kernel.Instance.ResourcePools.TvDb, cancellationToken).
+ ConfigureAwait(false);
+
+ season.SetImage(ImageType.Banner, bannerImagePath);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVSeasonBackdrops && (Kernel.Instance.Configuration.RefreshItemImages || !season.HasLocalImage("backdrop")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='fanart'][Season='" + seasonNumber + "']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ if (season.BackdropImagePaths == null) season.BackdropImagePaths = new List<string>();
+ season.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(season, TVUtils.BannerUrl + n.InnerText, "backdrop" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ else if (!Kernel.Instance.Configuration.SaveLocalMeta) //if saving local - season will inherit from series
+ {
+ // not necessarily accurate but will give a different bit of art to each season
+ var lst = images.SelectNodes("//Banner[BannerType='fanart']");
+ if (lst != null && lst.Count > 0)
+ {
+ var num = seasonNumber % lst.Count;
+ n = lst[num];
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ if (season.BackdropImagePaths == null)
+ season.BackdropImagePaths = new List<string>();
+
+ try
+ {
+ season.BackdropImagePaths.Add(
+ await
+ Kernel.Instance.ProviderManager.DownloadAndSaveImage(season,
+ TVUtils.BannerUrl +
+ n.InnerText,
+ "backdrop" +
+ Path.GetExtension(
+ n.InnerText),
+ Kernel.Instance.ResourcePools.TvDb, cancellationToken)
+ .ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //just folder.jpg/png
+ return (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") ||
+ item.ResolveArgs.ContainsMetaFileByName("folder.png"));
+ }
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs
new file mode 100644
index 0000000000..901d390407
--- /dev/null
+++ b/MediaBrowser.Controller/Providers/TV/RemoteSeriesProvider.cs
@@ -0,0 +1,545 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class RemoteSeriesProvider
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ class RemoteSeriesProvider : BaseMetadataProvider
+ {
+
+ /// <summary>
+ /// The root URL
+ /// </summary>
+ private const string rootUrl = "http://www.thetvdb.com/api/";
+ /// <summary>
+ /// The series query
+ /// </summary>
+ private const string seriesQuery = "GetSeries.php?seriesname={0}";
+ /// <summary>
+ /// The series get
+ /// </summary>
+ private const string seriesGet = "http://www.thetvdb.com/api/{0}/series/{1}/{2}.xml";
+ /// <summary>
+ /// The get actors
+ /// </summary>
+ private const string getActors = "http://www.thetvdb.com/api/{0}/series/{1}/actors.xml";
+
+ /// <summary>
+ /// The LOCA l_ MET a_ FIL e_ NAME
+ /// </summary>
+ protected const string LOCAL_META_FILE_NAME = "Series.xml";
+
+ /// <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 Series;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.Second; }
+ }
+
+ /// <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>
+ /// 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 downloadDate = providerInfo.LastRefreshed;
+
+ if (Kernel.Instance.Configuration.MetadataRefreshDays == -1 && downloadDate != DateTime.MinValue)
+ {
+ return false;
+ }
+
+ if (item.DontFetchMeta) return false;
+
+ return !HasLocalMeta(item) && (Kernel.Instance.Configuration.MetadataRefreshDays != -1 &&
+ DateTime.UtcNow.Subtract(downloadDate).TotalDays > Kernel.Instance.Configuration.MetadataRefreshDays);
+ }
+
+ /// <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>
+ protected override async Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var series = (Series)item;
+ if (!item.DontFetchMeta && !HasLocalMeta(series))
+ {
+ var path = item.Path ?? "";
+ var seriesId = Path.GetFileName(path).GetAttributeValue("tvdbid") ?? await GetSeriesId(series, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+ series.SetProviderId(MetadataProviders.Tvdb, seriesId);
+ if (!HasCompleteMetadata(series))
+ {
+ await FetchSeriesData(series, seriesId, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ SetLastRefreshed(item, DateTime.UtcNow);
+ return true;
+ }
+ Logger.Info("Series provider not fetching because local meta exists or requested to ignore: " + item.Name);
+ return false;
+
+ }
+
+ /// <summary>
+ /// Fetches the series data.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.Boolean}.</returns>
+ private async Task<bool> FetchSeriesData(Series series, string seriesId, CancellationToken cancellationToken)
+ {
+ var success = false;
+
+ var name = series.Name;
+ Logger.Debug("TvDbProvider: Fetching series data: " + name);
+
+ if (!string.IsNullOrEmpty(seriesId))
+ {
+
+ string url = string.Format(seriesGet, TVUtils.TVDBApiKey, seriesId, Kernel.Instance.Configuration.PreferredMetadataLanguage);
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var xml = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(xml);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (doc.HasChildNodes)
+ {
+ //kick off the actor and image fetch simultaneously
+ var actorTask = FetchActors(series, seriesId, doc, cancellationToken);
+ var imageTask = FetchImages(series, seriesId, cancellationToken);
+
+ success = true;
+
+ series.Name = doc.SafeGetString("//SeriesName");
+ series.Overview = doc.SafeGetString("//Overview");
+ series.CommunityRating = doc.SafeGetSingle("//Rating", 0, 10);
+ series.AirDays = TVUtils.GetAirDays(doc.SafeGetString("//Airs_DayOfWeek"));
+ series.AirTime = doc.SafeGetString("//Airs_Time");
+
+ string n = doc.SafeGetString("//banner");
+ if (!string.IsNullOrWhiteSpace(n))
+ {
+ series.SetImage(ImageType.Banner, TVUtils.BannerUrl + n);
+ }
+
+ string s = doc.SafeGetString("//Network");
+ if (!string.IsNullOrWhiteSpace(s))
+ series.AddStudios(new List<string>(s.Trim().Split('|')));
+
+ series.OfficialRating = doc.SafeGetString("//ContentRating");
+
+ string g = doc.SafeGetString("//Genre");
+
+ if (g != null)
+ {
+ string[] genres = g.Trim('|').Split('|');
+ if (g.Length > 0)
+ {
+ series.AddGenres(genres);
+ }
+ }
+
+ //wait for other tasks
+ await Task.WhenAll(actorTask, imageTask).ConfigureAwait(false);
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ var ms = new MemoryStream();
+ doc.Save(ms);
+
+ await Kernel.Instance.FileSystemManager.SaveToLibraryFilesystem(series, Path.Combine(series.MetaLocation, LOCAL_META_FILE_NAME), ms, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+
+
+
+ return success;
+ }
+
+ /// <summary>
+ /// Fetches the actors.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="doc">The doc.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchActors(Series series, string seriesId, XmlDocument doc, CancellationToken cancellationToken)
+ {
+ string urlActors = string.Format(getActors, TVUtils.TVDBApiKey, seriesId);
+ var docActors = new XmlDocument();
+
+ try
+ {
+ using (var actors = await Kernel.Instance.HttpManager.Get(urlActors, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ docActors.Load(actors);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (docActors.HasChildNodes)
+ {
+ XmlNode actorsNode = null;
+ if (Kernel.Instance.Configuration.SaveLocalMeta)
+ {
+ //add to the main doc for saving
+ var seriesNode = doc.SelectSingleNode("//Series");
+ if (seriesNode != null)
+ {
+ actorsNode = doc.CreateNode(XmlNodeType.Element, "Persons", null);
+ seriesNode.AppendChild(actorsNode);
+ }
+ }
+
+ var xmlNodeList = docActors.SelectNodes("Actors/Actor");
+ if (xmlNodeList != null)
+ foreach (XmlNode p in xmlNodeList)
+ {
+ string actorName = p.SafeGetString("Name");
+ string actorRole = p.SafeGetString("Role");
+ if (!string.IsNullOrWhiteSpace(actorName))
+ {
+ series.AddPerson(new PersonInfo { Type = PersonType.Actor, Name = actorName, Role = actorRole });
+
+ if (Kernel.Instance.Configuration.SaveLocalMeta && actorsNode != null)
+ {
+ //create in main doc
+ var personNode = doc.CreateNode(XmlNodeType.Element, "Person", null);
+ foreach (XmlNode subNode in p.ChildNodes)
+ personNode.AppendChild(doc.ImportNode(subNode, true));
+ //need to add the type
+ var typeNode = doc.CreateNode(XmlNodeType.Element, "Type", null);
+ typeNode.InnerText = "Actor";
+ personNode.AppendChild(typeNode);
+ actorsNode.AppendChild(personNode);
+ }
+
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fetches the images.
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <param name="seriesId">The series id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task FetchImages(Series series, string seriesId, CancellationToken cancellationToken)
+ {
+ if ((!string.IsNullOrEmpty(seriesId)) && ((series.PrimaryImagePath == null) || (series.BackdropImagePaths == null)))
+ {
+ string url = string.Format("http://www.thetvdb.com/api/" + TVUtils.TVDBApiKey + "/series/{0}/banners.xml", seriesId);
+ var images = new XmlDocument();
+
+ try
+ {
+ using (var imgs = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ images.Load(imgs);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (images.HasChildNodes)
+ {
+ if (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage("folder"))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='poster']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ series.PrimaryImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "folder" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ if (Kernel.Instance.Configuration.DownloadTVBanner && (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage("banner")))
+ {
+ var n = images.SelectSingleNode("//Banner[BannerType='series']");
+ if (n != null)
+ {
+ n = n.SelectSingleNode("./BannerPath");
+ if (n != null)
+ {
+ try
+ {
+ var bannerImagePath = await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + n.InnerText, "banner" + Path.GetExtension(n.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken);
+
+ series.SetImage(ImageType.Banner, bannerImagePath);
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ }
+ }
+
+ var bdNo = 0;
+ var xmlNodeList = images.SelectNodes("//Banner[BannerType='fanart']");
+ if (xmlNodeList != null)
+ foreach (XmlNode b in xmlNodeList)
+ {
+ series.BackdropImagePaths = new List<string>();
+ var p = b.SelectSingleNode("./BannerPath");
+ if (p != null)
+ {
+ var bdName = "backdrop" + (bdNo > 0 ? bdNo.ToString() : "");
+ if (Kernel.Instance.Configuration.RefreshItemImages || !series.HasLocalImage(bdName))
+ {
+ try
+ {
+ series.BackdropImagePaths.Add(await Kernel.Instance.ProviderManager.DownloadAndSaveImage(series, TVUtils.BannerUrl + p.InnerText, bdName + Path.GetExtension(p.InnerText), Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false));
+ }
+ catch (HttpException)
+ {
+ }
+ catch (IOException)
+ {
+
+ }
+ }
+ bdNo++;
+ if (bdNo >= Kernel.Instance.Configuration.MaxBackdrops) break;
+ }
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Determines whether [has complete metadata] [the specified series].
+ /// </summary>
+ /// <param name="series">The series.</param>
+ /// <returns><c>true</c> if [has complete metadata] [the specified series]; otherwise, <c>false</c>.</returns>
+ private bool HasCompleteMetadata(Series series)
+ {
+ return (series.HasImage(ImageType.Banner)) && (series.CommunityRating != null)
+ && (series.Overview != null) && (series.Name != null) && (series.People != null)
+ && (series.Genres != null) && (series.OfficialRating != null);
+ }
+
+ /// <summary>
+ /// Determines whether [has local meta] [the specified item].
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <returns><c>true</c> if [has local meta] [the specified item]; otherwise, <c>false</c>.</returns>
+ private bool HasLocalMeta(BaseItem item)
+ {
+ //need at least the xml and folder.jpg/png
+ return item.ResolveArgs.ContainsMetaFileByName(LOCAL_META_FILE_NAME) && (item.ResolveArgs.ContainsMetaFileByName("folder.jpg") ||
+ item.ResolveArgs.ContainsMetaFileByName("folder.png"));
+ }
+
+ /// <summary>
+ /// Gets the series id.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ private async Task<string> GetSeriesId(BaseItem item, CancellationToken cancellationToken)
+ {
+ var seriesId = item.GetProviderId(MetadataProviders.Tvdb);
+ if (string.IsNullOrEmpty(seriesId))
+ {
+ seriesId = await FindSeries(item.Name, cancellationToken).ConfigureAwait(false);
+ }
+ return seriesId;
+ }
+
+
+ /// <summary>
+ /// Finds the series.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task{System.String}.</returns>
+ public async Task<string> FindSeries(string name, CancellationToken cancellationToken)
+ {
+
+ //nope - search for it
+ string url = string.Format(rootUrl + seriesQuery, WebUtility.UrlEncode(name));
+ var doc = new XmlDocument();
+
+ try
+ {
+ using (var results = await Kernel.Instance.HttpManager.Get(url, Kernel.Instance.ResourcePools.TvDb, cancellationToken).ConfigureAwait(false))
+ {
+ doc.Load(results);
+ }
+ }
+ catch (HttpException)
+ {
+ }
+
+ if (doc.HasChildNodes)
+ {
+ XmlNodeList nodes = doc.SelectNodes("//Series");
+ string comparableName = GetComparableName(name);
+ if (nodes != null)
+ foreach (XmlNode node in nodes)
+ {
+ var n = node.SelectSingleNode("./SeriesName");
+ if (n != null && GetComparableName(n.InnerText) == comparableName)
+ {
+ n = node.SelectSingleNode("./seriesid");
+ if (n != null)
+ return n.InnerText;
+ }
+ else
+ {
+ if (n != null)
+ Logger.Info("TVDb Provider - " + n.InnerText + " did not match " + comparableName);
+ }
+ }
+ }
+
+ Logger.Info("TVDb Provider - Could not find " + name + ". Check name on Thetvdb.org.");
+ return null;
+ }
+
+ /// <summary>
+ /// The remove
+ /// </summary>
+ const string remove = "\"'!`?";
+ /// <summary>
+ /// The spacers
+ /// </summary>
+ const string spacers = "/,.:;\\(){}[]+-_=–*"; // (there are not actually two - in the they are different char codes)
+
+ /// <summary>
+ /// Gets the name of the comparable.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <returns>System.String.</returns>
+ internal static string GetComparableName(string name)
+ {
+ name = name.ToLower();
+ name = name.Normalize(NormalizationForm.FormKD);
+ var sb = new StringBuilder();
+ foreach (var c in name)
+ {
+ if ((int)c >= 0x2B0 && (int)c <= 0x0333)
+ {
+ // skip char modifier and diacritics
+ }
+ else if (remove.IndexOf(c) > -1)
+ {
+ // skip chars we are removing
+ }
+ else if (spacers.IndexOf(c) > -1)
+ {
+ sb.Append(" ");
+ }
+ else if (c == '&')
+ {
+ sb.Append(" and ");
+ }
+ else
+ {
+ sb.Append(c);
+ }
+ }
+ name = sb.ToString();
+ name = name.Replace(", the", "");
+ name = name.Replace("the ", " ");
+ name = name.Replace(" the ", " ");
+
+ string prevName;
+ do
+ {
+ prevName = name;
+ name = name.Replace(" ", " ");
+ } while (name.Length != prevName.Length);
+
+ return name.Trim();
+ }
+
+
+
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
index 76d7e7ac11..728ac0549c 100644
--- a/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
+++ b/MediaBrowser.Controller/Providers/TV/SeriesProviderFromXml.cs
@@ -1,36 +1,86 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.ComponentModel.Composition;
-using System.IO;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- [Export(typeof(BaseMetadataProvider))]
- public class SeriesProviderFromXml : BaseMetadataProvider
- {
- public override bool Supports(BaseEntity item)
- {
- return item is Series;
- }
-
- public override MetadataProviderPriority Priority
- {
- get { return MetadataProviderPriority.First; }
- }
-
- public override async Task FetchAsync(BaseEntity item, ItemResolveEventArgs args)
- {
- await Task.Run(() => Fetch(item, args)).ConfigureAwait(false);
- }
-
- private void Fetch(BaseEntity item, ItemResolveEventArgs args)
- {
- if (args.ContainsFile("series.xml"))
- {
- new SeriesXmlParser().Fetch(item as Series, Path.Combine(args.Path, "series.xml"));
- }
- }
- }
-}
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class SeriesProviderFromXml
+ /// </summary>
+ [Export(typeof(BaseMetadataProvider))]
+ public class SeriesProviderFromXml : BaseMetadataProvider
+ {
+ /// <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 Series && item.LocationType == LocationType.FileSystem;
+ }
+
+ /// <summary>
+ /// Gets the priority.
+ /// </summary>
+ /// <value>The priority.</value>
+ public override MetadataProviderPriority Priority
+ {
+ get { return MetadataProviderPriority.First; }
+ }
+
+ /// <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)
+ {
+ var entry = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "series.xml"));
+ return entry != null ? entry.Value.LastWriteTimeUtc : DateTime.MinValue;
+ }
+
+ /// <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>
+ protected override Task<bool> FetchAsyncInternal(BaseItem item, bool force, CancellationToken cancellationToken)
+ {
+ return Task.Run(() => Fetch(item, cancellationToken));
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+ private bool Fetch(BaseItem item, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var metadataFile = item.ResolveArgs.GetMetaFileByPath(Path.Combine(item.MetaLocation, "series.xml"));
+
+ if (metadataFile.HasValue)
+ {
+ var path = metadataFile.Value.Path;
+
+ new SeriesXmlParser().Fetch((Series)item, path, cancellationToken);
+ SetLastRefreshed(item, DateTime.UtcNow);
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
index 36c0a99efd..7516904255 100644
--- a/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
+++ b/MediaBrowser.Controller/Providers/TV/SeriesXmlParser.cs
@@ -1,69 +1,90 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Xml;
-
-namespace MediaBrowser.Controller.Providers.TV
-{
- public class SeriesXmlParser : BaseItemXmlParser<Series>
- {
- protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
- {
- switch (reader.Name)
- {
- case "id":
- string id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(MetadataProviders.Tvdb, id);
- }
- break;
-
- case "Airs_DayOfWeek":
- {
- string day = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(day))
- {
- if (day.Equals("Daily", StringComparison.OrdinalIgnoreCase))
- {
- item.AirDays = new DayOfWeek[] {
- DayOfWeek.Sunday,
- DayOfWeek.Monday,
- DayOfWeek.Tuesday,
- DayOfWeek.Wednesday,
- DayOfWeek.Thursday,
- DayOfWeek.Friday,
- DayOfWeek.Saturday
- };
- }
- else
- {
- item.AirDays = new DayOfWeek[] {
- (DayOfWeek)Enum.Parse(typeof(DayOfWeek), day, true)
- };
- }
- }
-
- break;
- }
-
- case "Airs_Time":
- item.AirTime = reader.ReadElementContentAsString();
- break;
-
- case "SeriesName":
- item.Name = reader.ReadElementContentAsString();
- break;
-
- case "Status":
- item.Status = reader.ReadElementContentAsString();
- break;
-
- default:
- base.FetchDataFromXmlNode(reader, item);
- break;
- }
- }
- }
-}
+using MediaBrowser.Common.Logging;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Resolvers.TV;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Xml;
+
+namespace MediaBrowser.Controller.Providers.TV
+{
+ /// <summary>
+ /// Class SeriesXmlParser
+ /// </summary>
+ public class SeriesXmlParser : BaseItemXmlParser<Series>
+ {
+ /// <summary>
+ /// Fetches the data from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <param name="item">The item.</param>
+ protected override void FetchDataFromXmlNode(XmlReader reader, Series item)
+ {
+ switch (reader.Name)
+ {
+ case "Series":
+ //MB generated metadata is within a "Series" node
+ using (var subTree = reader.ReadSubtree())
+ {
+ subTree.MoveToContent();
+
+ // Loop through each element
+ while (subTree.Read())
+ {
+ if (subTree.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(subTree, item);
+ }
+ }
+
+ }
+ break;
+
+ case "id":
+ string id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(MetadataProviders.Tvdb, id);
+ }
+ break;
+
+ case "Airs_DayOfWeek":
+ {
+ item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
+ break;
+ }
+
+ case "Airs_Time":
+ item.AirTime = reader.ReadElementContentAsString();
+ break;
+
+ case "SeriesName":
+ item.Name = reader.ReadElementContentAsString();
+ break;
+
+ case "Status":
+ {
+ var status = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(status))
+ {
+ SeriesStatus seriesStatus;
+ if (Enum.TryParse(status, true, out seriesStatus))
+ {
+ item.Status = seriesStatus;
+ }
+ else
+ {
+ Logger.LogInfo("Unrecognized series status: " + status);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs b/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
deleted file mode 100644
index 264825fe08..0000000000
--- a/MediaBrowser.Controller/Providers/VideoInfoProvider.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.FFMpeg;
-using MediaBrowser.Model.Entities;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.Composition;
-using System.Linq;
-
-namespace MediaBrowser.Controller.Providers
-{
- [Export(typeof(BaseMetadataProvider))]
- public class VideoInfoProvider : BaseMediaInfoProvider<Video>
- {
- public override MetadataProviderPriority Priority
- {
- // Give this second priority
- // Give metadata xml providers a chance to fill in data first, so that we can skip this whenever possible
- get { return MetadataProviderPriority.Second; }
- }
-
- protected override string CacheDirectory
- {
- get { return Kernel.Instance.ApplicationPaths.FFProbeVideoCacheDirectory; }
- }
-
- protected override void Fetch(Video video, FFProbeResult data)
- {
- if (data.format != null)
- {
- if (!string.IsNullOrEmpty(data.format.duration))
- {
- video.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration)).Ticks;
- }
-
- if (!string.IsNullOrEmpty(data.format.bit_rate))
- {
- video.BitRate = int.Parse(data.format.bit_rate);
- }
- }
-
- if (data.streams != null)
- {
- // For now, only read info about first video stream
- // Files with multiple video streams are possible, but extremely rare
- bool foundVideo = false;
-
- foreach (MediaStream stream in data.streams)
- {
- if (stream.codec_type.Equals("video", StringComparison.OrdinalIgnoreCase))
- {
- if (!foundVideo)
- {
- FetchFromVideoStream(video, stream);
- }
-
- foundVideo = true;
- }
- else if (stream.codec_type.Equals("audio", StringComparison.OrdinalIgnoreCase))
- {
- FetchFromAudioStream(video, stream);
- }
- }
- }
- }
-
- private void FetchFromVideoStream(Video video, MediaStream stream)
- {
- video.Codec = stream.codec_name;
- video.Width = stream.width;
- video.Height = stream.height;
- video.AspectRatio = stream.display_aspect_ratio;
-
- if (!string.IsNullOrEmpty(stream.avg_frame_rate))
- {
- string[] parts = stream.avg_frame_rate.Split('/');
-
- if (parts.Length == 2)
- {
- video.FrameRate = float.Parse(parts[0]) / float.Parse(parts[1]);
- }
- else
- {
- video.FrameRate = float.Parse(parts[0]);
- }
- }
- }
-
- private void FetchFromAudioStream(Video video, MediaStream stream)
- {
- var audio = new AudioStream{};
-
- audio.Codec = stream.codec_name;
-
- if (!string.IsNullOrEmpty(stream.bit_rate))
- {
- audio.BitRate = int.Parse(stream.bit_rate);
- }
-
- audio.Channels = stream.channels;
-
- if (!string.IsNullOrEmpty(stream.sample_rate))
- {
- audio.SampleRate = int.Parse(stream.sample_rate);
- }
-
- audio.Language = GetDictionaryValue(stream.tags, "language");
-
- List<AudioStream> streams = video.AudioStreams ?? new List<AudioStream>();
- streams.Add(audio);
- video.AudioStreams = streams;
- }
-
- private void FetchFromSubtitleStream(Video video, MediaStream stream)
- {
- var subtitle = new SubtitleStream{};
-
- subtitle.Language = GetDictionaryValue(stream.tags, "language");
-
- List<SubtitleStream> streams = video.Subtitles ?? new List<SubtitleStream>();
- streams.Add(subtitle);
- video.Subtitles = streams;
- }
-
- /// <summary>
- /// Determines if there's already enough info in the Video object to allow us to skip running ffprobe
- /// </summary>
- protected override bool CanSkipFFProbe(Video video)
- {
- if (video.VideoType != VideoType.VideoFile)
- {
- // Not supported yet
- return true;
- }
-
- if (video.AudioStreams == null || !video.AudioStreams.Any())
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.AspectRatio))
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.Codec))
- {
- return false;
- }
-
- if (string.IsNullOrEmpty(video.ScanType))
- {
- return false;
- }
-
- if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value == 0)
- {
- return false;
- }
-
- if (Convert.ToInt32(video.FrameRate) == 0 || video.Height == 0 || video.Width == 0 || video.BitRate == 0)
- {
- return false;
- }
-
- return true;
- }
- }
-}