diff options
| author | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
|---|---|---|
| committer | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
| commit | 767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch) | |
| tree | 49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/Providers | |
| parent | 845554722efaed872948a9e0f7202e3ef52f1b6e (diff) | |
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/Providers')
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;
- }
- }
-}
|
