From 3d47b495a96fce84c03d9f3177dc6dbc8a4afa3c Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 29 Jun 2014 23:04:50 -0400 Subject: fixes #795 - Support reading Xbmc nfo's --- .../Configuration/NfoOptions.cs | 29 + MediaBrowser.XbmcMetadata/EntryPoint.cs | 99 ++ MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs | 272 ++++++ .../MediaBrowser.XbmcMetadata.csproj | 89 ++ MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 992 +++++++++++++++++++++ .../Parsers/EpisodeNfoParser.cs | 211 +++++ .../Parsers/MovieNfoParser.cs | 97 ++ .../Parsers/SeasonNfoParser.cs | 45 + .../Parsers/SeriesNfoParser.cs | 93 ++ .../Properties/AssemblyInfo.cs | 36 + .../Providers/AlbumNfoProvider.cs | 34 + .../Providers/ArtistNfoProvider.cs | 34 + .../Providers/BaseNfoProvider.cs | 89 ++ .../Providers/BaseVideoNfoProvider.cs | 55 ++ .../Providers/EpisodeNfoProvider.cs | 44 + .../Providers/MovieNfoProvider.cs | 45 + .../Providers/SeasonNfoProvider.cs | 35 + .../Providers/SeriesNfoProvider.cs | 34 + MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs | 143 +++ MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs | 124 +++ .../Savers/EpisodeXmlSaver.cs | 149 ++++ MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs | 143 +++ MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs | 90 ++ MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs | 130 +++ .../Savers/XmlSaverHelpers.cs | 906 +++++++++++++++++++ 25 files changed, 4018 insertions(+) create mode 100644 MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs create mode 100644 MediaBrowser.XbmcMetadata/EntryPoint.cs create mode 100644 MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj create mode 100644 MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs create mode 100644 MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs create mode 100644 MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs create mode 100644 MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs create mode 100644 MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs create mode 100644 MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs create mode 100644 MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs (limited to 'MediaBrowser.XbmcMetadata') diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs new file mode 100644 index 0000000000..d0e2dbc6df --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; + +namespace MediaBrowser.XbmcMetadata.Configuration +{ + public class ConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + ConfigurationType = typeof(XbmcMetadataOptions), + Key = "xbmcmetadata" + } + }; + } + } + + public static class ConfigurationExtension + { + public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration("xbmcmetadata"); + } + } +} diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs new file mode 100644 index 0000000000..2d978781f7 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -0,0 +1,99 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.XbmcMetadata.Configuration; +using System; +using System.Linq; + +namespace MediaBrowser.XbmcMetadata +{ + public class EntryPoint : IServerEntryPoint + { + private readonly IUserDataManager _userDataManager; + private readonly ILogger _logger; + private readonly ILibraryManager _libraryManager; + private readonly IProviderManager _providerManager; + private readonly IConfigurationManager _config; + + public EntryPoint(IUserDataManager userDataManager, ILibraryManager libraryManager, ILogger logger, IProviderManager providerManager, IConfigurationManager config) + { + _userDataManager = userDataManager; + _libraryManager = libraryManager; + _logger = logger; + _providerManager = providerManager; + _config = config; + } + + public void Run() + { + _userDataManager.UserDataSaved += _userDataManager_UserDataSaved; + _libraryManager.ItemUpdated += _libraryManager_ItemUpdated; + } + + void _libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e) + { + if (e.UpdateReason == ItemUpdateType.ImageUpdate && e.Item is Person) + { + var person = e.Item.Name; + + var items = _libraryManager.RootFolder + .GetRecursiveChildren(i => !i.IsFolder && i.People.Any(p => string.Equals(p.Name, person, StringComparison.OrdinalIgnoreCase))); + + foreach (var item in items) + { + SaveMetadataForItem(item, ItemUpdateType.MetadataEdit); + } + } + } + + void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e) + { + if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed) + { + var item = e.Item as BaseItem; + + if (item != null) + { + if (!item.IsFolder && !(item is IItemByName)) + { + SaveMetadataForItem(item, ItemUpdateType.MetadataEdit); + } + } + } + } + + public void Dispose() + { + _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved; + } + + private async void SaveMetadataForItem(BaseItem item, ItemUpdateType updateReason) + { + var userId = _config.GetNfoConfiguration().UserId; + if (string.IsNullOrWhiteSpace(userId)) + { + return; + } + + var locationType = item.LocationType; + if (locationType == LocationType.Remote || + locationType == LocationType.Virtual) + { + return; + } + + try + { + await _providerManager.SaveMetadata(item, updateReason).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving metadata for {0}", ex, item.Path ?? item.Name); + } + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs b/MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs new file mode 100644 index 0000000000..a45a449044 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs @@ -0,0 +1,272 @@ +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; + +namespace MediaBrowser.XbmcMetadata.Images +{ + public class XbmcImageSaver : IImageFileSaver + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public IEnumerable GetSavePaths(IHasImages item, ImageType type, ImageFormat format, int index) + { + var season = item as Season; + + if (!SupportsItem(item, type, season)) + { + return new string[] { }; + } + + var extension = "." + format.ToString().ToLower(); + + // Backdrop paths + if (type == ImageType.Backdrop) + { + if (index == 0) + { + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart", extension) }; + } + + if (season != null && season.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", _usCulture); + + var imageFilename = "season" + seasonMarker + "-fanart" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + return new[] + { + Path.Combine(item.ContainingFolderPath, "fanart" + extension) + }; + } + + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + index.ToString(_usCulture), extension) }; + } + + var extraFanartFilename = GetBackdropSaveFilename(item.GetImages(ImageType.Backdrop), "fanart", "fanart", index); + + return new[] + { + Path.Combine(item.ContainingFolderPath, "extrafanart", extraFanartFilename + extension), + Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + index.ToString(_usCulture) + extension) + }; + } + + if (type == ImageType.Primary) + { + if (season != null && season.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", _usCulture); + + var imageFilename = "season" + seasonMarker + "-poster" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + if (item is Episode) + { + var seasonFolder = Path.GetDirectoryName(item.Path); + + var imageFilename = Path.GetFileNameWithoutExtension(item.Path) + "-thumb" + extension; + + return new[] { Path.Combine(seasonFolder, imageFilename) }; + } + + if (item.IsInMixedFolder || item is MusicVideo) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, string.Empty, extension) }; + } + + if (item is MusicAlbum || item is MusicArtist) + { + return new[] { Path.Combine(item.ContainingFolderPath, "folder" + extension) }; + } + + return new[] { Path.Combine(item.ContainingFolderPath, "poster" + extension) }; + } + + if (type == ImageType.Banner) + { + if (season != null && season.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", _usCulture); + + var imageFilename = "season" + seasonMarker + "-banner" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + } + + if (type == ImageType.Thumb) + { + if (season != null && season.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", _usCulture); + + var imageFilename = "season" + seasonMarker + "-landscape" + extension; + + return new[] { Path.Combine(seriesFolder, imageFilename) }; + } + + if (item.IsInMixedFolder) + { + return new[] { GetSavePathForItemInMixedFolder(item, type, "landscape", extension) }; + } + + return new[] { Path.Combine(item.ContainingFolderPath, "landscape" + extension) }; + } + + return GetStandardSavePaths(item, type, index, extension); + } + + private IEnumerable GetStandardSavePaths(IHasImages item, ImageType type, int imageIndex, string extension) + { + string filename; + + switch (type) + { + case ImageType.Art: + filename = "clearart"; + break; + case ImageType.BoxRear: + filename = "back"; + break; + case ImageType.Disc: + filename = item is MusicAlbum ? "cdart" : "disc"; + break; + case ImageType.Screenshot: + filename = GetBackdropSaveFilename(item.GetImages(type), "screenshot", "screenshot", imageIndex); + break; + default: + filename = type.ToString().ToLower(); + break; + } + + string path = null; + + if (item.IsInMixedFolder) + { + path = GetSavePathForItemInMixedFolder(item, type, filename, extension); + } + + if (string.IsNullOrEmpty(path)) + { + path = Path.Combine(item.ContainingFolderPath, filename + extension); + } + + if (string.IsNullOrEmpty(path)) + { + return new string[] { }; + } + + return new[] { path }; + } + + + private string GetSavePathForItemInMixedFolder(IHasImages item, ImageType type, string imageFilename, string extension) + { + if (type == ImageType.Primary) + { + imageFilename = "poster"; + } + var folder = Path.GetDirectoryName(item.Path); + + return Path.Combine(folder, Path.GetFileNameWithoutExtension(item.Path) + "-" + imageFilename + extension); + } + + private bool SupportsItem(IHasImages item, ImageType type, Season season) + { + if (item.IsOwnedItem || item is Audio || item is User) + { + return false; + } + + if (type != ImageType.Primary && item is Episode) + { + return false; + } + + if (!item.SupportsLocalMetadata) + { + return false; + } + + var locationType = item.LocationType; + if (locationType == LocationType.Remote || locationType == LocationType.Virtual) + { + var allowSaving = false; + + // If season is virtual under a physical series, save locally if using compatible convention + if (season != null) + { + var series = season.Series; + + if (series != null && series.SupportsLocalMetadata) + { + allowSaving = true; + } + } + + if (!allowSaving) + { + return false; + } + } + + return true; + } + + private string GetBackdropSaveFilename(IEnumerable images, string zeroIndexFilename, string numberedIndexPrefix, int? index) + { + if (index.HasValue && index.Value == 0) + { + return zeroIndexFilename; + } + + var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList(); + + var current = 1; + while (filenames.Contains(numberedIndexPrefix + current.ToString(_usCulture), StringComparer.OrdinalIgnoreCase)) + { + current++; + } + + return numberedIndexPrefix + current.ToString(_usCulture); + } + + public string Name + { + get { return "Media Browser/Plex/Xbmc Images"; } + } + } +} diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj new file mode 100644 index 0000000000..a3928d7742 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -0,0 +1,89 @@ + + + + + Debug + AnyCPU + {23499896-B135-4527-8574-C26E926EA99E} + Library + Properties + MediaBrowser.XbmcMetadata + MediaBrowser.XbmcMetadata + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs new file mode 100644 index 0000000000..0515148f02 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -0,0 +1,992 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using MediaBrowser.XbmcMetadata.Configuration; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Xml; + +namespace MediaBrowser.XbmcMetadata.Parsers +{ + public class BaseNfoParser + where T : BaseItem + { + /// + /// The logger + /// + protected ILogger Logger { get; private set; } + + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The configuration. + public BaseNfoParser(ILogger logger, IConfigurationManager config) + { + Logger = logger; + _config = config; + } + + /// + /// Fetches metadata for an item from one xml file + /// + /// The item. + /// The metadata file. + /// The cancellation token. + /// + public void Fetch(T item, string metadataFile, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException(); + } + + if (string.IsNullOrEmpty(metadataFile)) + { + throw new ArgumentNullException(); + } + + var settings = new XmlReaderSettings + { + CheckCharacters = false, + IgnoreProcessingInstructions = true, + IgnoreComments = true, + ValidationType = ValidationType.None + }; + + //Fetch(item, metadataFile, settings, Encoding.GetEncoding("ISO-8859-1"), cancellationToken); + Fetch(item, metadataFile, settings, Encoding.UTF8, cancellationToken); + } + + /// + /// Fetches the specified item. + /// + /// The item. + /// The metadata file. + /// The settings. + /// The encoding. + /// The cancellation token. + private void Fetch(T item, string metadataFile, XmlReaderSettings settings, Encoding encoding, CancellationToken cancellationToken) + { + using (var streamReader = new StreamReader(metadataFile, encoding)) + { + // Use XmlReader for best performance + using (var reader = XmlReader.Create(streamReader, settings)) + { + reader.MoveToContent(); + + // Loop through each element + while (reader.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + FetchDataFromXmlNode(reader, item); + } + } + } + } + } + + protected virtual void FetchDataFromXmlNode(XmlReader reader, T item) + { + switch (reader.Name) + { + // DateCreated + case "dateadded": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime added; + if (DateTime.TryParse(val, out added)) + { + item.DateCreated = added.ToUniversalTime(); + } + else + { + Logger.Warn("Invalid Added value found: " + val); + } + } + break; + } + + case "title": + case "localtitle": + item.Name = reader.ReadElementContentAsString(); + break; + + case "criticrating": + { + var text = reader.ReadElementContentAsString(); + + var hasCriticRating = item as IHasCriticRating; + + if (hasCriticRating != null && !string.IsNullOrEmpty(text)) + { + float value; + if (float.TryParse(text, NumberStyles.Any, _usCulture, out value)) + { + hasCriticRating.CriticRating = value; + } + } + + break; + } + + case "budget": + { + var text = reader.ReadElementContentAsString(); + var hasBudget = item as IHasBudget; + if (hasBudget != null) + { + double value; + if (double.TryParse(text, NumberStyles.Any, _usCulture, out value)) + { + hasBudget.Budget = value; + } + } + + break; + } + + case "revenue": + { + var text = reader.ReadElementContentAsString(); + var hasBudget = item as IHasBudget; + if (hasBudget != null) + { + double value; + if (double.TryParse(text, NumberStyles.Any, _usCulture, out value)) + { + hasBudget.Revenue = value; + } + } + + break; + } + + case "metascore": + { + var text = reader.ReadElementContentAsString(); + var hasMetascore = item as IHasMetascore; + if (hasMetascore != null) + { + float value; + if (float.TryParse(text, NumberStyles.Any, _usCulture, out value)) + { + hasMetascore.Metascore = value; + } + } + + break; + } + + case "awardsummary": + { + var text = reader.ReadElementContentAsString(); + var hasAwards = item as IHasAwards; + if (hasAwards != null) + { + if (!string.IsNullOrWhiteSpace(text)) + { + hasAwards.AwardSummary = text; + } + } + + break; + } + + case "sorttitle": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.ForcedSortName = val; + } + break; + } + + case "outline": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + var hasShortOverview = item as IHasShortOverview; + + if (hasShortOverview != null) + { + hasShortOverview.ShortOverview = val; + } + } + break; + } + + case "biography": + case "plot": + case "review": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.Overview = val; + } + + break; + } + + case "criticratingsummary": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + var hasCriticRating = item as IHasCriticRating; + + if (hasCriticRating != null) + { + hasCriticRating.CriticRatingSummary = val; + } + } + + break; + } + + case "language": + { + var val = reader.ReadElementContentAsString(); + + var hasLanguage = item as IHasPreferredMetadataLanguage; + if (hasLanguage != null) + { + hasLanguage.PreferredMetadataLanguage = val; + } + + break; + } + + case "website": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.HomePageUrl = val; + } + + break; + } + + case "lockedfields": + { + var fields = new List(); + + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + var list = val.Split('|').Select(i => + { + MetadataFields field; + + if (Enum.TryParse(i, true, out field)) + { + return (MetadataFields?)field; + } + + return null; + + }).Where(i => i.HasValue).Select(i => i.Value); + + fields.AddRange(list); + } + + item.LockedFields = fields; + + break; + } + + case "tagline": + { + var val = reader.ReadElementContentAsString(); + + var hasTagline = item as IHasTaglines; + if (hasTagline != null) + { + if (!string.IsNullOrWhiteSpace(val)) + { + hasTagline.AddTagline(val); + } + } + break; + } + + case "country": + { + var val = reader.ReadElementContentAsString(); + + var hasProductionLocations = item as IHasProductionLocations; + if (hasProductionLocations != null) + { + if (!string.IsNullOrWhiteSpace(val)) + { + hasProductionLocations.AddProductionLocation(val); + } + } + break; + } + + case "mpaa": + { + var rating = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(rating)) + { + item.OfficialRating = rating; + } + break; + } + + case "mpaadescription": + { + var rating = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(rating)) + { + item.OfficialRatingDescription = rating; + } + break; + } + + case "customrating": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.CustomRating = val; + } + break; + } + + case "runtime": + { + var text = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(text)) + { + int runtime; + if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out runtime)) + { + item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; + } + } + break; + } + + case "aspectratio": + { + var val = reader.ReadElementContentAsString(); + + var hasAspectRatio = item as IHasAspectRatio; + if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null) + { + hasAspectRatio.AspectRatio = val; + } + break; + } + + case "lockdata": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + break; + } + + case "studio": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + item.AddStudio(val); + } + break; + } + + case "director": + { + foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director })) + { + if (string.IsNullOrWhiteSpace(p.Name)) + { + continue; + } + item.AddPerson(p); + } + break; + } + case "credits": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + var parts = val.Split('/').Select(i => i.Trim()) + .Where(i => !string.IsNullOrEmpty(i)); + + foreach (var p in parts.Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer })) + { + 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.Trim(), Type = PersonType.Writer })) + { + if (string.IsNullOrWhiteSpace(p.Name)) + { + continue; + } + item.AddPerson(p); + } + break; + } + + case "actor": + { + using (var subtree = reader.ReadSubtree()) + { + var person = GetPersonFromXmlNode(subtree); + + item.AddPerson(person); + } + break; + } + + case "trailer": + { + var val = reader.ReadElementContentAsString(); + + var hasTrailer = item as IHasTrailers; + if (hasTrailer != null) + { + if (!string.IsNullOrWhiteSpace(val)) + { + hasTrailer.AddTrailerUrl(val, false); + } + } + break; + } + + case "displayorder": + { + var val = reader.ReadElementContentAsString(); + + var hasDisplayOrder = item as IHasDisplayOrder; + if (hasDisplayOrder != null) + { + if (!string.IsNullOrWhiteSpace(val)) + { + hasDisplayOrder.DisplayOrder = val; + } + } + break; + } + + case "year": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int productionYear; + if (int.TryParse(val, out productionYear) && productionYear > 1850) + { + item.ProductionYear = productionYear; + } + } + + break; + } + + case "rating": + { + + var rating = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(rating)) + { + float val; + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out val)) + { + item.CommunityRating = val; + } + } + break; + } + + case "aired": + case "formed": + case "premiered": + case "releasedate": + { + var formatString = _config.GetNfoConfiguration().ReleaseDateFormat; + + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime date; + + if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date) && date.Year > 1850) + { + item.PremiereDate = date.ToUniversalTime(); + item.ProductionYear = date.Year; + } + } + + break; + } + + case "enddate": + { + var formatString = _config.GetNfoConfiguration().ReleaseDateFormat; + + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + DateTime date; + + if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out date) && date.Year > 1850) + { + item.EndDate = date.ToUniversalTime(); + } + } + + break; + } + + case "tvdbid": + var tvdbId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(tvdbId)) + { + item.SetProviderId(MetadataProviders.Tvdb, tvdbId); + } + break; + + case "votes": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + int num; + + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out num)) + { + item.VoteCount = num; + } + } + break; + } + case "musicbrainzalbumid": + { + var mbz = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(mbz)) + { + item.SetProviderId(MetadataProviders.MusicBrainzAlbum, mbz); + } + break; + } + case "musicbrainzalbumartistid": + { + var mbz = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(mbz)) + { + item.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, mbz); + } + break; + } + case "musicbrainzartistid": + { + var mbz = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(mbz)) + { + item.SetProviderId(MetadataProviders.MusicBrainzArtist, mbz); + } + break; + } + case "musicbrainzreleasegroupid": + { + var mbz = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(mbz)) + { + item.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, mbz); + } + break; + } + case "tvrageid": + { + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(MetadataProviders.TvRage, id); + } + break; + } + case "audiodbartistid": + { + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(MetadataProviders.AudioDbArtist, id); + } + break; + } + case "audiodbalbumid": + { + var id = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(id)) + { + item.SetProviderId(MetadataProviders.AudioDbAlbum, id); + } + break; + } + case "rottentomatoesid": + var rtId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(rtId)) + { + item.SetProviderId(MetadataProviders.RottenTomatoes, rtId); + } + break; + + case "tmdbid": + var tmdb = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(tmdb)) + { + item.SetProviderId(MetadataProviders.Tmdb, tmdb); + } + break; + + case "collectionnumber": + var tmdbCollection = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(tmdbCollection)) + { + item.SetProviderId(MetadataProviders.TmdbCollection, tmdbCollection); + } + break; + + case "tvcomid": + var TVcomId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(TVcomId)) + { + item.SetProviderId(MetadataProviders.Tvcom, TVcomId); + } + break; + + case "zap2itid": + var zap2ItId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(zap2ItId)) + { + item.SetProviderId(MetadataProviders.Zap2It, zap2ItId); + } + break; + + case "imdb_id": + case "imdbid": + var imDbId = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(imDbId)) + { + item.SetProviderId(MetadataProviders.Imdb, imDbId); + } + break; + + case "genre": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + item.AddGenre(val); + } + break; + } + + case "style": + case "tag": + { + var val = reader.ReadElementContentAsString(); + if (!string.IsNullOrWhiteSpace(val)) + { + var hasTags = item as IHasTags; + if (hasTags != null) + { + hasTags.AddTag(val); + } + } + break; + } + + case "plotkeyword": + { + var val = reader.ReadElementContentAsString(); + + var hasKeywords = item as IHasKeywords; + if (hasKeywords != null) + { + if (!string.IsNullOrWhiteSpace(val)) + { + hasKeywords.AddKeyword(val); + } + } + break; + } + + case "fileinfo": + { + using (var subtree = reader.ReadSubtree()) + { + FetchFromFileInfoNode(subtree, item); + } + break; + } + + default: + reader.Skip(); + break; + } + } + + private void FetchFromFileInfoNode(XmlReader reader, T item) + { + reader.MoveToContent(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "streamdetails": + { + using (var subtree = reader.ReadSubtree()) + { + FetchFromStreamDetailsNode(subtree, item); + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + + private void FetchFromStreamDetailsNode(XmlReader reader, T item) + { + reader.MoveToContent(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "video": + { + using (var subtree = reader.ReadSubtree()) + { + FetchFromVideoNode(subtree, item); + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + + private void FetchFromVideoNode(XmlReader reader, T item) + { + reader.MoveToContent(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "format3d": + { + var video = item as Video; + + if (video != null) + { + var val = reader.ReadElementContentAsString(); + + if (string.Equals("HSBS", val, StringComparison.CurrentCulture)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", val, StringComparison.CurrentCulture)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", val, StringComparison.CurrentCulture)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", val, StringComparison.CurrentCulture)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + } + + /// + /// Gets the persons from XML node. + /// + /// The reader. + /// IEnumerable{PersonInfo}. + private PersonInfo GetPersonFromXmlNode(XmlReader reader) + { + var name = string.Empty; + var type = PersonType.Actor; // If type is not specified assume actor + var role = string.Empty; + int? sortOrder = null; + + reader.MoveToContent(); + + while (reader.Read()) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.Name) + { + case "name": + name = reader.ReadElementContentAsString() ?? string.Empty; + 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; + } + case "sortorder": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int intVal; + if (int.TryParse(val, NumberStyles.Integer, _usCulture, out intVal)) + { + sortOrder = intVal; + } + } + break; + } + + default: + reader.Skip(); + break; + } + } + } + + return new PersonInfo + { + Name = name.Trim(), + Role = role, + Type = type, + SortOrder = sortOrder + }; + } + + /// + /// Used to split names of comma or pipe delimeted genres and people + /// + /// The value. + /// IEnumerable{System.String}. + private IEnumerable 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.IndexOf(';') == -1 ? new[] { ',' } : new[] { '|', ';' }; + + value = value.Trim().Trim(separator); + + return string.IsNullOrWhiteSpace(value) ? new string[] { } : Split(value, separator, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Provides an additional overload for string.split + /// + /// The val. + /// The separators. + /// The options. + /// System.String[][]. + private static string[] Split(string val, char[] separators, StringSplitOptions options) + { + return val.Split(separators, options); + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs new file mode 100644 index 0000000000..ba7b48b1fd --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -0,0 +1,211 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Xml; + +namespace MediaBrowser.XbmcMetadata.Parsers +{ + public class EpisodeNfoParser : BaseNfoParser + { + private List _imagesFound; + private List _chaptersFound; + private string _xmlPath; + + public EpisodeNfoParser(ILogger logger, IConfigurationManager config) : base(logger, config) + { + } + + public void Fetch(Episode item, + List images, + List chapters, + string metadataFile, + CancellationToken cancellationToken) + { + _imagesFound = images; + _chaptersFound = chapters; + _xmlPath = metadataFile; + + Fetch(item, metadataFile, cancellationToken); + } + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Fetches the data from XML node. + /// + /// The reader. + /// The item. + protected override void FetchDataFromXmlNode(XmlReader reader, Episode item) + { + switch (reader.Name) + { + //case "Chapters": + + // _chaptersFound.AddRange(FetchChaptersFromXmlNode(item, reader.ReadSubtree())); + // break; + + case "season": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + int num; + + if (int.TryParse(number, out num)) + { + item.ParentIndexNumber = num; + } + } + break; + } + + case "episode": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + int num; + + if (int.TryParse(number, out num)) + { + item.IndexNumber = num; + } + } + break; + } + + case "episodenumberend": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + int num; + + if (int.TryParse(number, out num)) + { + item.IndexNumberEnd = num; + } + } + break; + } + + case "absolute_number": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AbsoluteEpisodeNumber = rval; + } + } + + break; + } + case "DVD_episodenumber": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdEpisodeNumber = num; + } + } + break; + } + + case "DVD_season": + { + var number = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(number)) + { + float num; + + if (float.TryParse(number, NumberStyles.Any, UsCulture, out num)) + { + item.DvdSeasonNumber = Convert.ToInt32(num); + } + } + break; + } + + case "airsbefore_episode": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsBeforeEpisodeNumber = rval; + } + } + + break; + } + + case "airsafter_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsAfterSeasonNumber = rval; + } + } + + break; + } + + case "airsbefore_season": + { + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) + { + int rval; + + // int.TryParse is local aware, so it can be probamatic, force us culture + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval)) + { + item.AirsBeforeSeasonNumber = rval; + } + } + + break; + } + + + default: + base.FetchDataFromXmlNode(reader, item); + break; + } + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs new file mode 100644 index 0000000000..9b90b2bb34 --- /dev/null +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -0,0 +1,97 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; +using System.Collections.Generic; +using System.Threading; +using System.Xml; + +namespace MediaBrowser.XbmcMetadata.Parsers +{ + class MovieNfoParser : BaseNfoParser