aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.XbmcMetadata
diff options
context:
space:
mode:
authorLuke Pulverenti <luke.pulverenti@gmail.com>2014-06-29 23:04:50 -0400
committerLuke Pulverenti <luke.pulverenti@gmail.com>2014-06-29 23:04:50 -0400
commit3d47b495a96fce84c03d9f3177dc6dbc8a4afa3c (patch)
tree9d03bc06b30fec5695bb7a89094387ecbdd8e3e9 /MediaBrowser.XbmcMetadata
parent1a5a75854bd3ec4cdd771c9afdaefe0acb62c03c (diff)
fixes #795 - Support reading Xbmc nfo's
Diffstat (limited to 'MediaBrowser.XbmcMetadata')
-rw-r--r--MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/EntryPoint.cs99
-rw-r--r--MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs272
-rw-r--r--MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj89
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs992
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs211
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs97
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs45
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs93
-rw-r--r--MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs36
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs34
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs34
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs89
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs55
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs44
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs45
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs35
-rw-r--r--MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs34
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs143
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs124
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs149
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs143
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs90
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs130
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs906
25 files changed, 4018 insertions, 0 deletions
diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs
new file mode 100644
index 000000000..d0e2dbc6d
--- /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<ConfigurationStore> GetConfigurations()
+ {
+ return new[]
+ {
+ new ConfigurationStore
+ {
+ ConfigurationType = typeof(XbmcMetadataOptions),
+ Key = "xbmcmetadata"
+ }
+ };
+ }
+ }
+
+ public static class ConfigurationExtension
+ {
+ public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager)
+ {
+ return manager.GetConfiguration<XbmcMetadataOptions>("xbmcmetadata");
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs
new file mode 100644
index 000000000..2d978781f
--- /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 000000000..a45a44904
--- /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<string> 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<string> 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<ItemImageInfo> 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 000000000..a3928d774
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+ <PropertyGroup>
+ <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+ <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+ <ProjectGuid>{23499896-B135-4527-8574-C26E926EA99E}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>MediaBrowser.XbmcMetadata</RootNamespace>
+ <AssemblyName>MediaBrowser.XbmcMetadata</AssemblyName>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="System" />
+ <Reference Include="System.Core" />
+ <Reference Include="System.Xml.Linq" />
+ <Reference Include="System.Data.DataSetExtensions" />
+ <Reference Include="Microsoft.CSharp" />
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Configuration\NfoOptions.cs" />
+ <Compile Include="EntryPoint.cs" />
+ <Compile Include="Images\XbmcImageSaver.cs" />
+ <Compile Include="Parsers\BaseNfoParser.cs" />
+ <Compile Include="Parsers\EpisodeNfoParser.cs" />
+ <Compile Include="Parsers\MovieNfoParser.cs" />
+ <Compile Include="Parsers\SeasonNfoParser.cs" />
+ <Compile Include="Parsers\SeriesNfoParser.cs" />
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="Providers\AlbumNfoProvider.cs" />
+ <Compile Include="Providers\ArtistNfoProvider.cs" />
+ <Compile Include="Providers\BaseNfoProvider.cs" />
+ <Compile Include="Providers\BaseVideoNfoProvider.cs" />
+ <Compile Include="Providers\EpisodeNfoProvider.cs" />
+ <Compile Include="Providers\MovieNfoProvider.cs" />
+ <Compile Include="Providers\SeasonNfoProvider.cs" />
+ <Compile Include="Providers\SeriesNfoProvider.cs" />
+ <Compile Include="Savers\AlbumXmlSaver.cs" />
+ <Compile Include="Savers\ArtistXmlSaver.cs" />
+ <Compile Include="Savers\EpisodeXmlSaver.cs" />
+ <Compile Include="Savers\MovieXmlSaver.cs" />
+ <Compile Include="Savers\SeasonXmlSaver.cs" />
+ <Compile Include="Savers\SeriesXmlSaver.cs" />
+ <Compile Include="Savers\XmlSaverHelpers.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
+ <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
+ <Name>MediaBrowser.Common</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
+ <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
+ <Name>MediaBrowser.Controller</Name>
+ </ProjectReference>
+ <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
+ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
+ <Name>MediaBrowser.Model</Name>
+ </ProjectReference>
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
+ Other similar extension points exist, see Microsoft.Common.targets.
+ <Target Name="BeforeBuild">
+ </Target>
+ <Target Name="AfterBuild">
+ </Target>
+ -->
+</Project> \ 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 000000000..0515148f0
--- /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<T>
+ where T : BaseItem
+ {
+ /// <summary>
+ /// The logger
+ /// </summary>
+ protected ILogger Logger { get; private set; }
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly IConfigurationManager _config;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BaseNfoParser{T}" /> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="config">The configuration.</param>
+ public BaseNfoParser(ILogger logger, IConfigurationManager config)
+ {
+ Logger = logger;
+ _config = config;
+ }
+
+ /// <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();
+ }
+
+ 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);
+ }
+
+ /// <summary>
+ /// Fetches the specified item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="metadataFile">The metadata file.</param>
+ /// <param name="settings">The settings.</param>
+ /// <param name="encoding">The encoding.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ 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<MetadataFields>();
+
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ var list = val.Split('|').Select(i =>
+ {
+ MetadataFields field;
+
+ if (Enum.TryParse<MetadataFields>(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;
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the persons from XML node.
+ /// </summary>
+ /// <param name="reader">The reader.</param>
+ /// <returns>IEnumerable{PersonInfo}.</returns>
+ 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
+ };
+ }
+
+ /// <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.IndexOf(';') == -1 ? new[] { ',' } : new[] { '|', ';' };
+
+ value = value.Trim().Trim(separator);
+
+ return string.IsNullOrWhiteSpace(value) ? new string[] { } : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ /// <summary>
+ /// Provides an additional overload for string.split
+ /// </summary>
+ /// <param name="val">The val.</param>
+ /// <param name="separators">The separators.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>System.String[][].</returns>
+ 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 000000000..ba7b48b1f
--- /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<Episode>
+ {
+ private List<LocalImageInfo> _imagesFound;
+ private List<ChapterInfo> _chaptersFound;
+ private string _xmlPath;
+
+ public EpisodeNfoParser(ILogger logger, IConfigurationManager config) : base(logger, config)
+ {
+ }
+
+ public void Fetch(Episode item,
+ List<LocalImageInfo> images,
+ List<ChapterInfo> chapters,
+ string metadataFile,
+ CancellationToken cancellationToken)
+ {
+ _imagesFound = images;
+ _chaptersFound = chapters;
+ _xmlPath = metadataFile;
+
+ Fetch(item, metadataFile, cancellationToken);
+ }
+
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <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 "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 000000000..9b90b2bb3
--- /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<Video>
+ {
+ private List<ChapterInfo> _chaptersFound;
+
+ public MovieNfoParser(ILogger logger, IConfigurationManager config) : base(logger, config)
+ {
+ }
+
+ public void Fetch(Video item,
+ List<ChapterInfo> chapters,
+ string metadataFile,
+ CancellationToken cancellationToken)
+ {
+ _chaptersFound = chapters;
+
+ Fetch(item, metadataFile, cancellationToken);
+ }
+
+ /// <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, Video item)
+ {
+ switch (reader.Name)
+ {
+ case "id":
+ var id = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(id))
+ {
+ item.SetProviderId(MetadataProviders.Imdb, id);
+ }
+ break;
+
+ case "set":
+ {
+ var val = reader.ReadElementContentAsString();
+ var movie = item as Movie;
+
+ if (!string.IsNullOrWhiteSpace(val) && movie != null)
+ {
+ movie.TmdbCollectionName = val;
+ }
+
+ break;
+ }
+
+ case "artist":
+ {
+ var val = reader.ReadElementContentAsString();
+ var movie = item as MusicVideo;
+
+ if (!string.IsNullOrWhiteSpace(val) && movie != null)
+ {
+ movie.Artist = val;
+ }
+
+ break;
+ }
+
+ case "album":
+ {
+ var val = reader.ReadElementContentAsString();
+ var movie = item as MusicVideo;
+
+ if (!string.IsNullOrWhiteSpace(val) && movie != null)
+ {
+ movie.Album = val;
+ }
+
+ break;
+ }
+
+ //case "chapter":
+
+ // _chaptersFound.AddRange(FetchChaptersFromXmlNode(item, reader.ReadSubtree()));
+ // break;
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
new file mode 100644
index 000000000..14abb74c6
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Model.Logging;
+using System.Xml;
+
+namespace MediaBrowser.XbmcMetadata.Parsers
+{
+ public class SeasonNfoParser : BaseNfoParser<Season>
+ {
+ public SeasonNfoParser(ILogger logger, IConfigurationManager config) : base(logger, config)
+ {
+ }
+
+ /// <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, Season item)
+ {
+ switch (reader.Name)
+ {
+ case "seasonnumber":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.IndexNumber = num;
+ }
+ }
+ break;
+ }
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
new file mode 100644
index 000000000..d72a64b5b
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -0,0 +1,93 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Xml;
+
+namespace MediaBrowser.XbmcMetadata.Parsers
+{
+ public class SeriesNfoParser : BaseNfoParser<Series>
+ {
+ public SeriesNfoParser(ILogger logger, IConfigurationManager config) : base(logger, config)
+ {
+ }
+
+ /// <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 "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":
+ {
+ var val = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(val))
+ {
+ item.AirTime = val;
+ }
+ break;
+ }
+
+ case "animeseriesindex":
+ {
+ var number = reader.ReadElementContentAsString();
+
+ if (!string.IsNullOrWhiteSpace(number))
+ {
+ int num;
+
+ if (int.TryParse(number, out num))
+ {
+ item.AnimeSeriesIndex = num;
+ }
+ }
+ 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.Info("Unrecognized series status: " + status);
+ }
+ }
+
+ break;
+ }
+
+ default:
+ base.FetchDataFromXmlNode(reader, item);
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..03829798e
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("MediaBrowser.XbmcMetadata")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.XbmcMetadata")]
+[assembly: AssemblyCopyright("Copyright © 2014")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("bbdf434b-65f1-4178-8ddf-067447df3e20")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
new file mode 100644
index 000000000..535fa28b8
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class AlbumNfoProvider : BaseNfoProvider<MusicAlbum>
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public AlbumNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<MusicAlbum> result, string path, CancellationToken cancellationToken)
+ {
+ new BaseNfoParser<MusicAlbum>(_logger, _config).Fetch(result.Item, path, cancellationToken);
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ return directoryService.GetFile(Path.Combine(info.Path, "album.nfo"));
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
new file mode 100644
index 000000000..f281c1048
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class ArtistNfoProvider : BaseNfoProvider<MusicArtist>
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public ArtistNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<MusicArtist> result, string path, CancellationToken cancellationToken)
+ {
+ new BaseNfoParser<MusicArtist>(_logger, _config).Fetch(result.Item, path, cancellationToken);
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ return directoryService.GetFile(Path.Combine(info.Path, "artist.nfo"));
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
new file mode 100644
index 000000000..65648e1d0
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
@@ -0,0 +1,89 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasChangeMonitor
+ where T : IHasMetadata, new()
+ {
+ protected IFileSystem FileSystem;
+
+ public async Task<LocalMetadataResult<T>> GetMetadata(ItemInfo info, CancellationToken cancellationToken)
+ {
+ var result = new LocalMetadataResult<T>();
+
+ var file = GetXmlFile(info, new DirectoryService(new NullLogger()));
+
+ if (file == null)
+ {
+ return result;
+ }
+
+ var path = file.FullName;
+
+ await XmlProviderUtils.XmlParsingResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ result.Item = new T();
+
+ Fetch(result, path, cancellationToken);
+ result.HasMetadata = true;
+ }
+ catch (FileNotFoundException)
+ {
+ result.HasMetadata = false;
+ }
+ catch (DirectoryNotFoundException)
+ {
+ result.HasMetadata = false;
+ }
+ finally
+ {
+ XmlProviderUtils.XmlParsingResourcePool.Release();
+ }
+
+ return result;
+ }
+
+ protected abstract void Fetch(LocalMetadataResult<T> result, string path, CancellationToken cancellationToken);
+
+ protected BaseNfoProvider(IFileSystem fileSystem)
+ {
+ FileSystem = fileSystem;
+ }
+
+ protected abstract FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService);
+
+ public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
+ {
+ var file = GetXmlFile(new ItemInfo { IsInMixedFolder = item.IsInMixedFolder, Path = item.Path }, directoryService);
+
+ if (file == null)
+ {
+ return false;
+ }
+
+ return file.Exists && FileSystem.GetLastWriteTimeUtc(file) > date;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+ }
+
+ static class XmlProviderUtils
+ {
+ internal static readonly SemaphoreSlim XmlParsingResourcePool = new SemaphoreSlim(4, 4);
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
new file mode 100644
index 000000000..d51c44ad4
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -0,0 +1,55 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class BaseVideoNfoProvider<T> : BaseNfoProvider<T>
+ where T : Video, new ()
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public BaseVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<T> result, string path, CancellationToken cancellationToken)
+ {
+ var chapters = new List<ChapterInfo>();
+
+ new MovieNfoParser(_logger, _config).Fetch(result.Item, chapters, path, cancellationToken);
+
+ result.Chapters = chapters;
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ var path = GetMovieSavePath(info);
+
+ return directoryService.GetFile(path);
+ }
+
+ public static string GetMovieSavePath(ItemInfo item)
+ {
+ if (Directory.Exists(item.Path))
+ {
+ var path = item.Path;
+
+ return Path.Combine(path, Path.GetFileNameWithoutExtension(path) + ".nfo");
+ }
+
+ return Path.ChangeExtension(item.Path, ".nfo");
+ }
+ }
+} \ No newline at end of file
diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
new file mode 100644
index 000000000..7899305e3
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
@@ -0,0 +1,44 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class EpisodeNfoProvider : BaseNfoProvider<Episode>
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public EpisodeNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<Episode> result, string path, CancellationToken cancellationToken)
+ {
+ var images = new List<LocalImageInfo>();
+ var chapters = new List<ChapterInfo>();
+
+ new EpisodeNfoParser(_logger, _config).Fetch(result.Item, images, chapters, path, cancellationToken);
+
+ result.Images = images;
+ result.Chapters = chapters;
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ var path = Path.ChangeExtension(info.Path, ".nfo");
+
+ return directoryService.GetFile(path);
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
new file mode 100644
index 000000000..bd17c8539
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
@@ -0,0 +1,45 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class MovieNfoProvider : BaseVideoNfoProvider<Movie>
+ {
+ public MovieNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config) : base(fileSystem, logger, config)
+ {
+ }
+ }
+
+ public class MusicVideoNfoProvider : BaseVideoNfoProvider<MusicVideo>
+ {
+ public MusicVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config) : base(fileSystem, logger, config)
+ {
+ }
+ }
+
+ public class AdultVideoNfoProvider : BaseVideoNfoProvider<AdultVideo>
+ {
+ public AdultVideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config) : base(fileSystem, logger, config)
+ {
+ }
+ }
+
+ public class VideoNfoProvider : BaseVideoNfoProvider<Video>
+ {
+ public VideoNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config) : base(fileSystem, logger, config)
+ {
+ }
+ }
+
+ public class TrailerNfoProvider : BaseVideoNfoProvider<Trailer>
+ {
+ public TrailerNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem, logger, config)
+ {
+ }
+ }
+
+} \ No newline at end of file
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
new file mode 100644
index 000000000..7b68ed94b
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
@@ -0,0 +1,35 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class SeasonNfoProvider : BaseNfoProvider<Season>
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public SeasonNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<Season> result, string path, CancellationToken cancellationToken)
+ {
+ new SeasonNfoParser(_logger, _config).Fetch(result.Item, path, cancellationToken);
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ return directoryService.GetFile(Path.Combine(info.Path, "season.nfo"));
+ }
+ }
+}
+
diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
new file mode 100644
index 000000000..4a71d9fd3
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
@@ -0,0 +1,34 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.XbmcMetadata.Parsers;
+using System.IO;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Providers
+{
+ public class SeriesNfoProvider : BaseNfoProvider<Series>
+ {
+ private readonly ILogger _logger;
+ private readonly IConfigurationManager _config;
+
+ public SeriesNfoProvider(IFileSystem fileSystem, ILogger logger, IConfigurationManager config)
+ : base(fileSystem)
+ {
+ _logger = logger;
+ _config = config;
+ }
+
+ protected override void Fetch(LocalMetadataResult<Series> result, string path, CancellationToken cancellationToken)
+ {
+ new SeriesNfoParser(_logger, _config).Fetch(result.Item, path, cancellationToken);
+ }
+
+ protected override FileSystemInfo GetXmlFile(ItemInfo info, IDirectoryService directoryService)
+ {
+ return directoryService.GetFile(Path.Combine(info.Path, "series.nfo"));
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs
new file mode 100644
index 000000000..121514cdd
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs
@@ -0,0 +1,143 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class AlbumXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ public AlbumXmlSaver(ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ _config = config;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return Path.Combine(item.Path, "album.nfo");
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var album = (MusicAlbum)item;
+
+ var builder = new StringBuilder();
+
+ builder.Append("<album>");
+
+ XmlSaverHelpers.AddCommonNodes(album, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ var tracks = album.RecursiveChildren
+ .OfType<Audio>()
+ .ToList();
+
+ var artists = tracks
+ .SelectMany(i =>
+ {
+ var list = new List<string>();
+
+ if (!string.IsNullOrEmpty(i.AlbumArtist))
+ {
+ list.Add(i.AlbumArtist);
+ }
+ list.AddRange(i.Artists);
+
+ return list;
+ })
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var artist in artists)
+ {
+ builder.Append("<artist>" + SecurityElement.Escape(artist) + "</artist>");
+ }
+
+ AddTracks(tracks, builder);
+
+ builder.Append("</album>");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "track",
+ "artist"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ return item is MusicAlbum;
+ }
+
+ return false;
+ }
+
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ private void AddTracks(IEnumerable<Audio> tracks, StringBuilder builder)
+ {
+ foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0))
+ {
+ builder.Append("<track>");
+
+ if (track.IndexNumber.HasValue)
+ {
+ builder.Append("<position>" + SecurityElement.Escape(track.IndexNumber.Value.ToString(UsCulture)) + "</position>");
+ }
+
+ if (!string.IsNullOrEmpty(track.Name))
+ {
+ builder.Append("<title>" + SecurityElement.Escape(track.Name) + "</title>");
+ }
+
+ if (track.RunTimeTicks.HasValue)
+ {
+ var time = TimeSpan.FromTicks(track.RunTimeTicks.Value).ToString(@"mm\:ss");
+
+ builder.Append("<duration>" + SecurityElement.Escape(time) + "</duration>");
+ }
+
+ builder.Append("</track>");
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs
new file mode 100644
index 000000000..f47785155
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs
@@ -0,0 +1,124 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.XbmcMetadata.Configuration;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class ArtistXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ public ArtistXmlSaver(ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ _config = config;
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return Path.Combine(item.Path, "artist.nfo");
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var artist = (MusicArtist)item;
+
+ var builder = new StringBuilder();
+
+ builder.Append("<artist>");
+
+ XmlSaverHelpers.AddCommonNodes(artist, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ if (artist.EndDate.HasValue)
+ {
+ var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+
+ builder.Append("<disbanded>" + SecurityElement.Escape(artist.EndDate.Value.ToString(formatString)) + "</disbanded>");
+ }
+
+ var albums = artist
+ .RecursiveChildren
+ .OfType<MusicAlbum>()
+ .ToList();
+
+ AddAlbums(albums, builder);
+
+ builder.Append("</artist>");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "album",
+ "disbanded"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ return item is MusicArtist;
+ }
+
+ return false;
+ }
+
+ private void AddAlbums(IEnumerable<MusicAlbum> albums, StringBuilder builder)
+ {
+ foreach (var album in albums)
+ {
+ builder.Append("<album>");
+
+ if (!string.IsNullOrEmpty(album.Name))
+ {
+ builder.Append("<title>" + SecurityElement.Escape(album.Name) + "</title>");
+ }
+
+ if (album.ProductionYear.HasValue)
+ {
+ builder.Append("<year>" + SecurityElement.Escape(album.ProductionYear.Value.ToString(UsCulture)) + "</year>");
+ }
+
+ builder.Append("</album>");
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs
new file mode 100644
index 000000000..a03eacdc4
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs
@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security;
+using System.Text;
+using System.Threading;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.XbmcMetadata.Configuration;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class EpisodeXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ public EpisodeXmlSaver(ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ _config = config;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return Path.ChangeExtension(item.Path, ".nfo");
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var episode = (Episode)item;
+
+ var builder = new StringBuilder();
+
+ builder.Append("<episodedetails>");
+
+ XmlSaverHelpers.AddCommonNodes(episode, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ if (episode.IndexNumber.HasValue)
+ {
+ builder.Append("<episode>" + episode.IndexNumber.Value.ToString(_usCulture) + "</episode>");
+ }
+
+ if (episode.IndexNumberEnd.HasValue)
+ {
+ builder.Append("<episodenumberend>" + SecurityElement.Escape(episode.IndexNumberEnd.Value.ToString(_usCulture)) + "</episodenumberend>");
+ }
+
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ builder.Append("<season>" + episode.ParentIndexNumber.Value.ToString(_usCulture) + "</season>");
+ }
+
+ if (episode.PremiereDate.HasValue)
+ {
+ var formatString = _config.GetNfoConfiguration().ReleaseDateFormat;
+
+ builder.Append("<aired>" + SecurityElement.Escape(episode.PremiereDate.Value.ToString(formatString)) + "</aired>");
+ }
+
+ if (episode.AirsAfterSeasonNumber.HasValue)
+ {
+ builder.Append("<airsafter_season>" + SecurityElement.Escape(episode.AirsAfterSeasonNumber.Value.ToString(_usCulture)) + "</airsafter_season>");
+ }
+ if (episode.AirsBeforeEpisodeNumber.HasValue)
+ {
+ builder.Append("<airsbefore_episode>" + SecurityElement.Escape(episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture)) + "</airsbefore_episode>");
+ }
+ if (episode.AirsBeforeSeasonNumber.HasValue)
+ {
+ builder.Append("<airsbefore_season>" + SecurityElement.Escape(episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture)) + "</airsbefore_season>");
+ }
+
+ if (episode.DvdEpisodeNumber.HasValue)
+ {
+ builder.Append("<DVD_episodenumber>" + SecurityElement.Escape(episode.DvdEpisodeNumber.Value.ToString(_usCulture)) + "</DVD_episodenumber>");
+ }
+
+ if (episode.DvdSeasonNumber.HasValue)
+ {
+ builder.Append("<DVD_season>" + SecurityElement.Escape(episode.DvdSeasonNumber.Value.ToString(_usCulture)) + "</DVD_season>");
+ }
+
+ if (episode.AbsoluteEpisodeNumber.HasValue)
+ {
+ builder.Append("<absolute_number>" + SecurityElement.Escape(episode.AbsoluteEpisodeNumber.Value.ToString(_usCulture)) + "</absolute_number>");
+ }
+
+ XmlSaverHelpers.AddMediaInfo((Episode)item, builder);
+
+ builder.Append("</episodedetails>");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "aired",
+ "season",
+ "episode",
+ "episodenumberend",
+ "airsafter_season",
+ "airsbefore_episode",
+ "airsbefore_season",
+ "DVD_episodenumber",
+ "DVD_season",
+ "absolute_number"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ return item is Episode;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs
new file mode 100644
index 000000000..586cefe65
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs
@@ -0,0 +1,143 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Security;
+using System.Text;
+using System.Threading;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class MovieXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ public MovieXmlSaver(ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ _config = config;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return GetMovieSavePath(item);
+ }
+
+ public static string GetMovieSavePath(IHasMetadata item)
+ {
+ var video = (Video)item;
+
+ if (video.VideoType == VideoType.Dvd || video.VideoType == VideoType.BluRay || video.VideoType == VideoType.HdDvd)
+ {
+ var path = item.ContainingFolderPath;
+
+ return Path.Combine(path, Path.GetFileNameWithoutExtension(path) + ".nfo");
+ }
+
+ return Path.ChangeExtension(item.Path, ".nfo");
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ var builder = new StringBuilder();
+
+ var tag = item is MusicVideo ? "musicvideo" : "movie";
+
+ builder.Append("<" + tag + ">");
+
+ XmlSaverHelpers.AddCommonNodes(video, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ var imdb = item.GetProviderId(MetadataProviders.Imdb);
+
+ if (!string.IsNullOrEmpty(imdb))
+ {
+ builder.Append("<id>" + SecurityElement.Escape(imdb) + "</id>");
+ }
+
+ var musicVideo = item as MusicVideo;
+
+ if (musicVideo != null)
+ {
+ if (!string.IsNullOrEmpty(musicVideo.Artist))
+ {
+ builder.Append("<artist>" + SecurityElement.Escape(musicVideo.Artist) + "</artist>");
+ }
+ if (!string.IsNullOrEmpty(musicVideo.Album))
+ {
+ builder.Append("<album>" + SecurityElement.Escape(musicVideo.Album) + "</album>");
+ }
+ }
+
+ var movie = item as Movie;
+
+ if (movie != null)
+ {
+ if (!string.IsNullOrEmpty(movie.TmdbCollectionName))
+ {
+ builder.Append("<set>" + SecurityElement.Escape(movie.TmdbCollectionName) + "</set>");
+ }
+ }
+
+ XmlSaverHelpers.AddMediaInfo((Video)item, builder);
+
+ builder.Append("</" + tag + ">");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "album",
+ "artist",
+ "set",
+ "id"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ var video = item as Video;
+
+ // Check parent for null to avoid running this against things like video backdrops
+ if (video != null && !(item is Episode) && !video.IsOwnedItem)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs
new file mode 100644
index 000000000..9de589e55
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security;
+using System.Text;
+using System.Threading;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class SeasonXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ public SeasonXmlSaver(ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ _config = config;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return Path.Combine(item.Path, "season.nfo");
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var builder = new StringBuilder();
+
+ builder.Append("<season>");
+
+ var season = (Season)item;
+
+ if (season.IndexNumber.HasValue)
+ {
+ builder.Append("<seasonnumber>" + SecurityElement.Escape(season.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)) + "</seasonnumber>");
+ }
+
+ XmlSaverHelpers.AddCommonNodes((Season)item, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ builder.Append("</season>");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "seasonnumber"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ return item is Season;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs
new file mode 100644
index 000000000..9c99cc33d
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs
@@ -0,0 +1,130 @@
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Security;
+using System.Text;
+using System.Threading;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public class SeriesXmlSaver : IMetadataFileSaver
+ {
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepo;
+
+ private readonly IFileSystem _fileSystem;
+ private readonly IServerConfigurationManager _config;
+
+ public SeriesXmlSaver(IServerConfigurationManager config, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem)
+ {
+ _config = config;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _userDataRepo = userDataRepo;
+ _fileSystem = fileSystem;
+ }
+
+ public string Name
+ {
+ get
+ {
+ return "Xbmc Nfo";
+ }
+ }
+
+ public string GetSavePath(IHasMetadata item)
+ {
+ return Path.Combine(item.Path, "tvshow.nfo");
+ }
+
+ public void Save(IHasMetadata item, CancellationToken cancellationToken)
+ {
+ var series = (Series)item;
+
+ var builder = new StringBuilder();
+
+ builder.Append("<tvshow>");
+
+ XmlSaverHelpers.AddCommonNodes(series, builder, _libraryManager, _userManager, _userDataRepo, _fileSystem, _config);
+
+ var tvdb = item.GetProviderId(MetadataProviders.Tvdb);
+
+ if (!string.IsNullOrEmpty(tvdb))
+ {
+ builder.Append("<id>" + SecurityElement.Escape(tvdb) + "</id>");
+
+ builder.AppendFormat("<episodeguide><url cache=\"{0}.xml\">http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/{1}.zip</url></episodeguide>",
+ tvdb,
+ string.IsNullOrEmpty(_config.Configuration.PreferredMetadataLanguage) ? "en" : _config.Configuration.PreferredMetadataLanguage);
+ }
+
+ builder.Append("<season>-1</season>");
+ builder.Append("<episode>-1</episode>");
+
+ if (series.Status.HasValue)
+ {
+ builder.Append("<status>" + SecurityElement.Escape(series.Status.Value.ToString()) + "</status>");
+ }
+
+ if (!string.IsNullOrEmpty(series.AirTime))
+ {
+ builder.Append("<airs_time>" + SecurityElement.Escape(series.AirTime) + "</airs_time>");
+ }
+
+ if (series.AirDays.Count == 7)
+ {
+ builder.Append("<airs_dayofweek>" + SecurityElement.Escape("Daily") + "</airs_dayofweek>");
+ }
+ else if (series.AirDays.Count > 0)
+ {
+ builder.Append("<airs_dayofweek>" + SecurityElement.Escape(series.AirDays[0].ToString()) + "</airs_dayofweek>");
+ }
+
+ if (series.AnimeSeriesIndex.HasValue)
+ {
+ builder.Append("<animeseriesindex>" + SecurityElement.Escape(series.AnimeSeriesIndex.Value.ToString(CultureInfo.InvariantCulture)) + "</animeseriesindex>");
+ }
+
+ builder.Append("</tvshow>");
+
+ var xmlFilePath = GetSavePath(item);
+
+ XmlSaverHelpers.Save(builder, xmlFilePath, new List<string>
+ {
+ "id",
+ "episodeguide",
+ "season",
+ "episode",
+ "status",
+ "airs_time",
+ "airs_dayofweek",
+ "animeseriesindex"
+ });
+ }
+
+ public bool IsEnabledFor(IHasMetadata item, ItemUpdateType updateType)
+ {
+ var locationType = item.LocationType;
+ if (locationType == LocationType.Remote || locationType == LocationType.Virtual)
+ {
+ return false;
+ }
+
+ // If new metadata has been downloaded or metadata was manually edited, proceed
+ if ((updateType & ItemUpdateType.MetadataDownload) == ItemUpdateType.MetadataDownload
+ || (updateType & ItemUpdateType.MetadataEdit) == ItemUpdateType.MetadataEdit)
+ {
+ return item is Series;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs b/MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs
new file mode 100644
index 000000000..85829b155
--- /dev/null
+++ b/MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs
@@ -0,0 +1,906 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Security;
+using System.Text;
+using System.Xml;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.XbmcMetadata.Configuration;
+
+namespace MediaBrowser.XbmcMetadata.Savers
+{
+ public static class XmlSaverHelpers
+ {
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ private static readonly Dictionary<string, string> CommonTags = new[] {
+
+ "plot",
+ "customrating",
+ "lockdata",
+ "type",
+ "dateadded",
+ "title",
+ "rating",
+ "year",
+ "sorttitle",
+ "mpaa",
+ "mpaadescription",
+ "aspectratio",
+ "website",
+ "collectionnumber",
+ "tmdbid",
+ "rottentomatoesid",
+ "language",
+ "tvcomid",
+ "budget",
+ "revenue",
+ "tagline",
+ "studio",
+ "genre",
+ "tag",
+ "runtime",
+ "actor",
+ "criticratingsummary",
+ "criticrating",
+ "fileinfo",
+ "director",
+ "writer",
+ "trailer",
+ "premiered",
+ "releasedate",
+ "outline",
+ "id",
+ "votes",
+ "credits",
+ "originaltitle",
+ "watched",
+ "playcount",
+ "lastplayed",
+ "art",
+ "resume",
+ "biography",
+ "formed",
+ "review",
+ "style",
+ "imdbid",
+ "imdb_id",
+ "plotkeyword",
+ "country",
+ "audiodbalbumid",
+ "audiodbartistid",
+ "awardsummary",
+ "enddate",
+ "lockedfields",
+ "metascore",
+ "zap2itid",
+ "tvrageid",
+ "gamesdbid",
+
+ "musicbrainzartistid",
+ "musicbrainzalbumartistid",
+ "musicbrainzalbumid",
+ "musicbrainzreleasegroupid",
+ "tvdbid",
+ "collectionitem"
+
+ }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// Saves the specified XML.
+ /// </summary>
+ /// <param name="xml">The XML.</param>
+ /// <param name="path">The path.</param>
+ /// <param name="xmlTagsUsed">The XML tags used.</param>
+ public static void Save(StringBuilder xml, string path, List<string> xmlTagsUsed)
+ {
+ if (File.Exists(path))
+ {
+ var tags = xmlTagsUsed.ToList();
+
+ var position = xml.ToString().LastIndexOf("</", StringComparison.OrdinalIgnoreCase);
+ xml.Insert(position, GetCustomTags(path, tags));
+ }
+
+ var xmlDocument = new XmlDocument();
+ xmlDocument.LoadXml(xml.ToString());
+
+ //Add the new node to the document.
+ xmlDocument.InsertBefore(xmlDocument.CreateXmlDeclaration("1.0", "UTF-8", "yes"), xmlDocument.DocumentElement);
+
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ var wasHidden = false;
+
+ var file = new FileInfo(path);
+
+ // This will fail if the file is hidden
+ if (file.Exists)
+ {
+ if ((file.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden)
+ {
+ file.Attributes &= ~FileAttributes.Hidden;
+
+ wasHidden = true;
+ }
+ }
+
+ using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ using (var streamWriter = new StreamWriter(filestream, Encoding.UTF8))
+ {
+ xmlDocument.Save(streamWriter);
+ }
+ }
+
+ if (wasHidden)
+ {
+ file.Refresh();
+
+ // Add back the attribute
+ file.Attributes |= FileAttributes.Hidden;
+ }
+ }
+
+ /// <summary>
+ /// Gets the custom tags.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="xmlTagsUsed">The XML tags used.</param>
+ /// <returns>System.String.</returns>
+ private static string GetCustomTags(string path, List<string> xmlTagsUsed)
+ {
+ var settings = new XmlReaderSettings
+ {
+ CheckCharacters = false,
+ IgnoreProcessingInstructions = true,
+ IgnoreComments = true,
+ ValidationType = ValidationType.None
+ };
+
+ var builder = new StringBuilder();
+
+ using (var streamReader = new StreamReader(path, Encoding.UTF8))
+ {
+ // Use XmlReader for best performance
+ using (var reader = XmlReader.Create(streamReader, settings))
+ {
+ reader.MoveToContent();
+
+ // Loop through each element
+ while (reader.Read())
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ var name = reader.Name;
+
+ if (!CommonTags.ContainsKey(name) && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase))
+ {
+ builder.AppendLine(reader.ReadOuterXml());
+ }
+ else
+ {
+ reader.Skip();
+ }
+ }
+ }
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ public static void AddMediaInfo<T>(T item, StringBuilder builder)
+ where T : BaseItem, IHasMediaSources
+ {
+ builder.Append("<fileinfo>");
+ builder.Append("<streamdetails>");
+
+ foreach (var stream in item.GetMediaSources(false).First().MediaStreams)
+ {
+ builder.Append("<" + stream.Type.ToString().ToLower() + ">");
+
+ if (!string.IsNullOrEmpty(stream.Codec))
+ {
+ builder.Append("<codec>" + SecurityElement.Escape(stream.Codec) + "</codec>");
+ builder.Append("<micodec>" + SecurityElement.Escape(stream.Codec) + "</micodec>");
+ }
+
+ if (stream.BitRate.HasValue)
+ {
+ builder.Append("<bitrate>" + stream.BitRate.Value.ToString(UsCulture) + "</bitrate>");
+ }
+
+ if (stream.Width.HasValue)
+ {
+ builder.Append("<width>" + stream.Width.Value.ToString(UsCulture) + "</width>");
+ }
+
+ if (stream.Height.HasValue)
+ {
+ builder.Append("<height>" + stream.Height.Value.ToString(UsCulture) + "</height>");
+ }
+
+ if (!string.IsNullOrEmpty(stream.AspectRatio))
+ {
+ builder.Append("<aspect>" + SecurityElement.Escape(stream.AspectRatio) + "</aspect>");
+ builder.Append("<aspectratio>" + SecurityElement.Escape(stream.AspectRatio) + "</aspectratio>");
+ }
+
+ var framerate = stream.AverageFrameRate ?? stream.RealFrameRate;
+
+ if (framerate.HasValue)
+ {
+ builder.Append("<framerate>" + framerate.Value.ToString(UsCulture) + "</framerate>");
+ }
+
+ if (!string.IsNullOrEmpty(stream.Language))
+ {
+ builder.Append("<language>" + SecurityElement.Escape(stream.Language) + "</language>");
+ }
+
+ var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
+ if (!string.IsNullOrEmpty(scanType))
+ {
+ builder.Append("<scantype>" + SecurityElement.Escape(scanType) + "</scantype>");
+ }
+
+ if (stream.Channels.HasValue)
+ {
+ builder.Append("<channels>" + stream.Channels.Value.ToString(UsCulture) + "</channels>");
+ }
+
+ if (stream.SampleRate.HasValue)
+ {
+ builder.Append("<samplingrate>" + stream.SampleRate.Value.ToString(UsCulture) + "</samplingrate>");
+ }
+
+ builder.Append("<default>" + SecurityElement.Escape(stream.IsDefault.ToString()) + "</default>");
+ builder.Append("<forced>" + SecurityElement.Escape(stream.IsForced.ToString()) + "</forced>");
+
+ if (stream.Type == MediaStreamType.Video)
+ {
+ if (item.RunTimeTicks.HasValue)
+ {
+ var timespan = TimeSpan.FromTicks(item.RunTimeTicks.Value);
+
+ builder.Append("<duration>" + Convert.ToInt32(timespan.TotalMinutes).ToString(UsCulture) + "</duration>");
+ builder.Append("<durationinseconds>" + Convert.ToInt32(timespan.TotalSeconds).ToString(UsCulture) + "</durationinseconds>");
+ }
+
+ var video = item as Video;
+
+ if (video != null)
+ {
+ //AddChapters(video, builder, itemRepository);
+
+ if (video.Video3DFormat.HasValue)
+ {
+ switch (video.Video3DFormat.Value)
+ {
+ case Video3DFormat.FullSideBySide:
+ builder.Append("<format3d>FSBS</format3d>");
+ break;
+ case Video3DFormat.FullTopAndBottom:
+ builder.Append("<format3d>FTAB</format3d>");
+ break;
+ case Video3DFormat.HalfSideBySide:
+ builder.Append("<format3d>HSBS</format3d>");
+ break;
+ case Video3DFormat.HalfTopAndBottom:
+ builder.Append("<format3d>HTAB</format3d>");
+ break;
+ }
+ }
+ }
+ }
+
+ builder.Append("</" + stream.Type.ToString().ToLower() + ">");
+ }
+
+ builder.Append("</streamdetails>");
+ builder.Append("</fileinfo>");
+ }
+
+ /// <summary>
+ /// Adds the common nodes.
+ /// </summary>
+ /// <returns>Task.</returns>
+ public static void AddCommonNodes(BaseItem item, StringBuilder builder, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ var overview = (item.Overview ?? string.Empty)
+ .StripHtml()
+ .Replace("&quot;", "'");
+
+ var options = config.GetNfoConfiguration();
+
+ if (item is MusicArtist)
+ {
+ builder.Append("<biography><![CDATA[" + overview + "]]></biography>");
+ }
+ else if (item is MusicAlbum)
+ {
+ builder.Append("<review><![CDATA[" + overview + "]]></review>");
+ }
+ else
+ {
+ builder.Append("<plot><![CDATA[" + overview + "]]></plot>");
+ }
+
+ var hasShortOverview = item as IHasShortOverview;
+ if (hasShortOverview != null)
+ {
+ var outline = (hasShortOverview.ShortOverview ?? string.Empty)
+ .StripHtml()
+ .Replace("&quot;", "'");
+
+ builder.Append("<outline><![CDATA[" + outline + "]]></outline>");
+ }
+ else
+ {
+ builder.Append("<outline><![CDATA[" + overview + "]]></outline>");
+ }
+
+ builder.Append("<customrating>" + SecurityElement.Escape(item.CustomRating ?? string.Empty) + "</customrating>");
+ builder.Append("<lockdata>" + item.IsLocked.ToString().ToLower() + "</lockdata>");
+
+ if (item.LockedFields.Count > 0)
+ {
+ builder.Append("<lockedfields>" + string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()) + "</lockedfields>");
+ }
+
+ if (!string.IsNullOrEmpty(item.DisplayMediaType))
+ {
+ builder.Append("<type>" + SecurityElement.Escape(item.DisplayMediaType) + "</type>");
+ }
+
+ builder.Append("<dateadded>" + SecurityElement.Escape(item.DateCreated.ToString("yyyy-MM-dd HH:mm:ss")) + "</dateadded>");
+
+ builder.Append("<title>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</title>");
+ builder.Append("<originaltitle>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</originaltitle>");
+
+ var directors = item.People
+ .Where(i => IsPersonType(i, PersonType.Director))
+ .Select(i => i.Name)
+ .ToList();
+
+ foreach (var person in directors)
+ {
+ builder.Append("<director>" + SecurityElement.Escape(person) + "</director>");
+ }
+
+ var writers = item.People
+ .Where(i => IsPersonType(i, PersonType.Director))
+ .Select(i => i.Name)
+ .ToList();
+
+ foreach (var person in writers)
+ {
+ builder.Append("<writer>" + SecurityElement.Escape(person) + "</writer>");
+ }
+
+ var credits = writers.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
+
+ if (credits.Count > 0)
+ {
+ builder.Append("<credits>" + SecurityElement.Escape(string.Join(" / ", credits.ToArray())) + "</credits>");
+ }
+
+ var hasTrailer = item as IHasTrailers;
+ if (hasTrailer != null)
+ {
+ foreach (var trailer in hasTrailer.RemoteTrailers)
+ {
+ builder.Append("<trailer>" + SecurityElement.Escape(GetOutputTrailerUrl(trailer.Url)) + "</trailer>");
+ }
+ }
+
+ if (item.CommunityRating.HasValue)
+ {
+ builder.Append("<rating>" + SecurityElement.Escape(item.CommunityRating.Value.ToString(UsCulture)) + "</rating>");
+ }
+
+ if (item.ProductionYear.HasValue)
+ {
+ builder.Append("<year>" + SecurityElement.Escape(item.ProductionYear.Value.ToString(UsCulture)) + "</year>");
+ }
+
+ if (!string.IsNullOrEmpty(item.ForcedSortName))
+ {
+ builder.Append("<sorttitle>" + SecurityElement.Escape(item.ForcedSortName) + "</sorttitle>");
+ }
+
+ if (!string.IsNullOrEmpty(item.OfficialRating))
+ {
+ builder.Append("<mpaa>" + SecurityElement.Escape(item.OfficialRating) + "</mpaa>");
+ }
+
+ if (!string.IsNullOrEmpty(item.OfficialRatingDescription))
+ {
+ builder.Append("<mpaadescription>" + SecurityElement.Escape(item.OfficialRatingDescription) + "</mpaadescription>");
+ }
+
+ var hasAspectRatio = item as IHasAspectRatio;
+ if (hasAspectRatio != null)
+ {
+ if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
+ {
+ builder.Append("<aspectratio>" + SecurityElement.Escape(hasAspectRatio.AspectRatio) + "</aspectratio>");
+ }
+ }
+
+ if (!string.IsNullOrEmpty(item.HomePageUrl))
+ {
+ builder.Append("<website>" + SecurityElement.Escape(item.HomePageUrl) + "</website>");
+ }
+
+ var rt = item.GetProviderId(MetadataProviders.RottenTomatoes);
+
+ if (!string.IsNullOrEmpty(rt))
+ {
+ builder.Append("<rottentomatoesid>" + SecurityElement.Escape(rt) + "</rottentomatoesid>");
+ }
+
+ var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection);
+
+ if (!string.IsNullOrEmpty(tmdbCollection))
+ {
+ builder.Append("<collectionnumber>" + SecurityElement.Escape(tmdbCollection) + "</collectionnumber>");
+ }
+
+ var imdb = item.GetProviderId(MetadataProviders.Imdb);
+ if (!string.IsNullOrEmpty(imdb))
+ {
+ if (item is Series)
+ {
+ builder.Append("<imdb_id>" + SecurityElement.Escape(imdb) + "</imdb_id>");
+ }
+ else
+ {
+ builder.Append("<imdbid>" + SecurityElement.Escape(imdb) + "</imdbid>");
+ }
+ }
+
+ // Series xml saver already saves this
+ if (!(item is Series))
+ {
+ var tvdb = item.GetProviderId(MetadataProviders.Tvdb);
+ if (!string.IsNullOrEmpty(tvdb))
+ {
+ builder.Append("<tvdbid>" + SecurityElement.Escape(tvdb) + "</tvdbid>");
+ }
+ }
+
+ var tmdb = item.GetProviderId(MetadataProviders.Tmdb);
+ if (!string.IsNullOrEmpty(tmdb))
+ {
+ builder.Append("<tmdbid>" + SecurityElement.Escape(tmdb) + "</tmdbid>");
+ }
+
+ var tvcom = item.GetProviderId(MetadataProviders.Tvcom);
+ if (!string.IsNullOrEmpty(tvcom))
+ {
+ builder.Append("<tvcomid>" + SecurityElement.Escape(tvcom) + "</tvcomid>");
+ }
+
+ var hasLanguage = item as IHasPreferredMetadataLanguage;
+ if (hasLanguage != null)
+ {
+ if (!string.IsNullOrEmpty(hasLanguage.PreferredMetadataLanguage))
+ {
+ builder.Append("<language>" + SecurityElement.Escape(hasLanguage.PreferredMetadataLanguage) + "</language>");
+ }
+ }
+
+ if (item.PremiereDate.HasValue && !(item is Episode))
+ {
+ var formatString = options.ReleaseDateFormat;
+
+ if (item is MusicArtist)
+ {
+ builder.Append("<formed>" + SecurityElement.Escape(item.PremiereDate.Value.ToString(formatString)) + "</formed>");
+ }
+ else
+ {
+ builder.Append("<premiered>" + SecurityElement.Escape(item.PremiereDate.Value.ToString(formatString)) + "</premiered>");
+ builder.Append("<releasedate>" + SecurityElement.Escape(item.PremiereDate.Value.ToString(formatString)) + "</releasedate>");
+ }
+ }
+
+ if (item.EndDate.HasValue)
+ {
+ if (!(item is Episode))
+ {
+ var formatString = options.ReleaseDateFormat;
+
+ builder.Append("<enddate>" + SecurityElement.Escape(item.EndDate.Value.ToString(formatString)) + "</enddate>");
+ }
+ }
+
+ var hasCriticRating = item as IHasCriticRating;
+
+ if (hasCriticRating != null)
+ {
+ if (hasCriticRating.CriticRating.HasValue)
+ {
+ builder.Append("<criticrating>" + SecurityElement.Escape(hasCriticRating.CriticRating.Value.ToString(UsCulture)) + "</criticrating>");
+ }
+
+ if (!string.IsNullOrEmpty(hasCriticRating.CriticRatingSummary))
+ {
+ builder.Append("<criticratingsummary><![CDATA[" + hasCriticRating.CriticRatingSummary + "]]></criticratingsummary>");
+ }
+ }
+
+ var hasDisplayOrder = item as IHasDisplayOrder;
+
+ if (hasDisplayOrder != null)
+ {
+ if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
+ {
+ builder.Append("<displayorder>" + SecurityElement.Escape(hasDisplayOrder.DisplayOrder) + "</displayorder>");
+ }
+ }
+
+ if (item.VoteCount.HasValue)
+ {
+ builder.Append("<votes>" + SecurityElement.Escape(item.VoteCount.Value.ToString(UsCulture)) + "</votes>");
+ }
+
+ var hasBudget = item as IHasBudget;
+ if (hasBudget != null)
+ {
+ if (hasBudget.Budget.HasValue)
+ {
+ builder.Append("<budget>" + SecurityElement.Escape(hasBudget.Budget.Value.ToString(UsCulture)) + "</budget>");
+ }
+
+ if (hasBudget.Revenue.HasValue)
+ {
+ builder.Append("<revenue>" + SecurityElement.Escape(hasBudget.Revenue.Value.ToString(UsCulture)) + "</revenue>");
+ }
+ }
+
+ var hasMetascore = item as IHasMetascore;
+ if (hasMetascore != null && hasMetascore.Metascore.HasValue)
+ {
+ builder.Append("<metascore>" + SecurityElement.Escape(hasMetascore.Metascore.Value.ToString(UsCulture)) + "</metascore>");
+ }
+
+ // Use original runtime here, actual file runtime later in MediaInfo
+ var runTimeTicks = item.RunTimeTicks;
+
+ if (runTimeTicks.HasValue)
+ {
+ var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
+
+ builder.Append("<runtime>" + Convert.ToInt32(timespan.TotalMinutes).ToString(UsCulture) + "</runtime>");
+ }
+
+ var hasTaglines = item as IHasTaglines;
+ if (hasTaglines != null)
+ {
+ foreach (var tagline in hasTaglines.Taglines)
+ {
+ builder.Append("<tagline>" + SecurityElement.Escape(tagline) + "</tagline>");
+ }
+ }
+
+ var hasProductionLocations = item as IHasProductionLocations;
+ if (hasProductionLocations != null)
+ {
+ foreach (var country in hasProductionLocations.ProductionLocations)
+ {
+ builder.Append("<country>" + SecurityElement.Escape(country) + "</country>");
+ }
+ }
+
+ foreach (var genre in item.Genres)
+ {
+ builder.Append("<genre>" + SecurityElement.Escape(genre) + "</genre>");
+ }
+
+ foreach (var studio in item.Studios)
+ {
+ builder.Append("<studio>" + SecurityElement.Escape(studio) + "</studio>");
+ }
+
+ var hasTags = item as IHasTags;
+ if (hasTags != null)
+ {
+ foreach (var tag in hasTags.Tags)
+ {
+ if (item is MusicAlbum || item is MusicArtist)
+ {
+ builder.Append("<style>" + SecurityElement.Escape(tag) + "</style>");
+ }
+ else
+ {
+ builder.Append("<tag>" + SecurityElement.Escape(tag) + "</tag>");
+ }
+ }
+ }
+
+ var hasKeywords = item as IHasKeywords;
+ if (hasKeywords != null)
+ {
+ foreach (var tag in hasKeywords.Keywords)
+ {
+ builder.Append("<plotkeyword>" + SecurityElement.Escape(tag) + "</plotkeyword>");
+ }
+ }
+
+ var hasAwards = item as IHasAwards;
+ if (hasAwards != null && !string.IsNullOrEmpty(hasAwards.AwardSummary))
+ {
+ builder.Append("<awardsummary>" + SecurityElement.Escape(hasAwards.AwardSummary) + "</awardsummary>");
+ }
+
+ var externalId = item.GetProviderId(MetadataProviders.AudioDbArtist);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<audiodbartistid>" + SecurityElement.Escape(externalId) + "</audiodbartistid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.AudioDbAlbum);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<audiodbalbumid>" + SecurityElement.Escape(externalId) + "</audiodbalbumid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.Zap2It);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<zap2itid>" + SecurityElement.Escape(externalId) + "</zap2itid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.MusicBrainzAlbum);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<musicbrainzalbumid>" + SecurityElement.Escape(externalId) + "</musicbrainzalbumid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.MusicBrainzAlbumArtist);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<musicbrainzalbumartistid>" + SecurityElement.Escape(externalId) + "</musicbrainzalbumartistid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.MusicBrainzArtist);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<musicbrainzartistid>" + SecurityElement.Escape(externalId) + "</musicbrainzartistid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
+
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<musicbrainzreleasegroupid>" + SecurityElement.Escape(externalId) + "</musicbrainzreleasegroupid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.Gamesdb);
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<gamesdbid>" + SecurityElement.Escape(externalId) + "</gamesdbid>");
+ }
+
+ externalId = item.GetProviderId(MetadataProviders.TvRage);
+ if (!string.IsNullOrEmpty(externalId))
+ {
+ builder.Append("<tvrageid>" + SecurityElement.Escape(externalId) + "</tvrageid>");
+ }
+
+ if (options.SaveImagePathsInNfo)
+ {
+ AddImages(item, builder, fileSystem, config);
+ }
+
+ AddUserData(item, builder, userManager, userDataRepo, options);
+
+ AddActors(item, builder, libraryManager, fileSystem, config);
+
+ var folder = item as BoxSet;
+ if (folder != null)
+ {
+ AddCollectionItems(folder, builder);
+ }
+ }
+
+ public static void AddChapters(Video item, StringBuilder builder, IItemRepository repository)
+ {
+ var chapters = repository.GetChapters(item.Id);
+
+ foreach (var chapter in chapters)
+ {
+ builder.Append("<chapter>");
+ builder.Append("<name>" + SecurityElement.Escape(chapter.Name) + "</name>");
+
+ var time = TimeSpan.FromTicks(chapter.StartPositionTicks);
+ var ms = Convert.ToInt64(time.TotalMilliseconds);
+
+ builder.Append("<startpositionms>" + SecurityElement.Escape(ms.ToString(UsCulture)) + "</startpositionms>");
+ builder.Append("</chapter>");
+ }
+ }
+
+ public static void AddCollectionItems(Folder item, StringBuilder builder)
+ {
+ var items = item.LinkedChildren
+ .Where(i => i.Type == LinkedChildType.Manual && !string.IsNullOrWhiteSpace(i.ItemName))
+ .ToList();
+
+ foreach (var link in items)
+ {
+ builder.Append("<collectionitem>");
+
+ builder.Append("<name>" + SecurityElement.Escape(link.ItemName) + "</name>");
+ builder.Append("<type>" + SecurityElement.Escape(link.ItemType) + "</type>");
+
+ if (link.ItemYear.HasValue)
+ {
+ builder.Append("<year>" + SecurityElement.Escape(link.ItemYear.Value.ToString(UsCulture)) + "</year>");
+ }
+
+ builder.Append("</collectionitem>");
+ }
+ }
+
+ /// <summary>
+ /// Gets the output trailer URL.
+ /// </summary>
+ /// <param name="url">The URL.</param>
+ /// <returns>System.String.</returns>
+ private static string GetOutputTrailerUrl(string url)
+ {
+ // This is what xbmc expects
+
+ return url.Replace("http://www.youtube.com/watch?v=",
+ "plugin://plugin.video.youtube/?action=play_video&videoid=",
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static void AddImages(BaseItem item, StringBuilder builder, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ builder.Append("<art>");
+
+ var poster = item.PrimaryImagePath;
+
+ if (!string.IsNullOrEmpty(poster))
+ {
+ builder.Append("<poster>" + SecurityElement.Escape(GetPathToSave(item.PrimaryImagePath, fileSystem, config)) + "</poster>");
+ }
+
+ foreach (var backdrop in item.GetImages(ImageType.Backdrop))
+ {
+ builder.Append("<fanart>" + SecurityElement.Escape(GetPathToSave(backdrop.Path, fileSystem, config)) + "</fanart>");
+ }
+
+ builder.Append("</art>");
+ }
+
+ private static void AddUserData(BaseItem item, StringBuilder builder, IUserManager userManager, IUserDataManager userDataRepo, XbmcMetadataOptions options)
+ {
+ var userId = options.UserId;
+ if (string.IsNullOrWhiteSpace(userId))
+ {
+ return;
+ }
+
+ var user = userManager.GetUserById(new Guid(userId));
+
+ if (user == null)
+ {
+ return;
+ }
+
+ if (item.IsFolder)
+ {
+ return;
+ }
+
+ var userdata = userDataRepo.GetUserData(user.Id, item.GetUserDataKey());
+
+ builder.Append("<playcount>" + userdata.PlayCount.ToString(UsCulture) + "</playcount>");
+ builder.Append("<watched>" + userdata.Played.ToString().ToLower() + "</watched>");
+
+ if (userdata.LastPlayedDate.HasValue)
+ {
+ builder.Append("<lastplayed>" + SecurityElement.Escape(userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss")) + "</lastplayed>");
+ }
+
+ builder.Append("<resume>");
+
+ var runTimeTicks = item.RunTimeTicks ?? 0;
+
+ builder.Append("<position>" + TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(UsCulture) + "</position>");
+ builder.Append("<total>" + TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(UsCulture) + "</total>");
+
+ builder.Append("</resume>");
+ }
+
+ public static void AddActors(BaseItem item, StringBuilder builder, ILibraryManager libraryManager, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ var actors = item.People
+ .Where(i => !IsPersonType(i, PersonType.Director) && !IsPersonType(i, PersonType.Writer))
+ .ToList();
+
+ foreach (var person in actors)
+ {
+ builder.Append("<actor>");
+ builder.Append("<name>" + SecurityElement.Escape(person.Name ?? string.Empty) + "</name>");
+ builder.Append("<role>" + SecurityElement.Escape(person.Role ?? string.Empty) + "</role>");
+ builder.Append("<type>" + SecurityElement.Escape(person.Type ?? string.Empty) + "</type>");
+
+ try
+ {
+ var personEntity = libraryManager.GetPerson(person.Name);
+
+ if (!string.IsNullOrEmpty(personEntity.PrimaryImagePath))
+ {
+ builder.Append("<thumb>" + SecurityElement.Escape(GetPathToSave(personEntity.PrimaryImagePath, fileSystem, config)) + "</thumb>");
+ }
+ }
+ catch (Exception)
+ {
+ // Already logged in core
+ }
+
+ builder.Append("</actor>");
+ }
+ }
+
+ private static bool IsPersonType(PersonInfo person, string type)
+ {
+ return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string GetPathToSave(string path, IFileSystem fileSystem, IServerConfigurationManager config)
+ {
+ foreach (var map in config.Configuration.PathSubstitutions)
+ {
+ path = fileSystem.SubstitutePath(path, map.From, map.To);
+ }
+
+ return path;
+ }
+
+ public static string ReplaceString(string str, string oldValue, string newValue, StringComparison comparison)
+ {
+ var sb = new StringBuilder();
+
+ int previousIndex = 0;
+ int index = str.IndexOf(oldValue, comparison);
+ while (index != -1)
+ {
+ sb.Append(str.Substring(previousIndex, index - previousIndex));
+ sb.Append(newValue);
+ index += oldValue.Length;
+
+ previousIndex = index;
+ index = str.IndexOf(oldValue, index, comparison);
+ }
+ sb.Append(str.Substring(previousIndex));
+
+ return sb.ToString();
+ }
+ }
+}