浏览代码

fixes #795 - Support reading Xbmc nfo's

Luke Pulverenti 11 年之前
父节点
当前提交
3d47b495a9
共有 89 个文件被更改,包括 4349 次插入306 次删除
  1. 13 3
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  2. 5 0
      MediaBrowser.Api/Playback/BifService.cs
  3. 2 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  4. 0 5
      MediaBrowser.Controller/Entities/Folder.cs
  5. 1 1
      MediaBrowser.Dlna/PlayTo/PlayToController.cs
  6. 6 6
      MediaBrowser.LocalMetadata/BaseXmlProvider.cs
  7. 3 5
      MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs
  8. 6 6
      MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs
  9. 4 5
      MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs
  10. 4 4
      MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs
  11. 7 7
      MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs
  12. 110 0
      MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
  13. 5 5
      MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs
  14. 6 6
      MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs
  15. 5 5
      MediaBrowser.LocalMetadata/Parsers/GameSystemXmlParser.cs
  16. 6 6
      MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs
  17. 5 5
      MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs
  18. 3 4
      MediaBrowser.LocalMetadata/Parsers/MusicVideoXmlParser.cs
  19. 3 3
      MediaBrowser.LocalMetadata/Parsers/SeasonXmlParser.cs
  20. 4 4
      MediaBrowser.LocalMetadata/Parsers/SeriesXmlParser.cs
  21. 36 0
      MediaBrowser.LocalMetadata/Properties/AssemblyInfo.cs
  22. 6 6
      MediaBrowser.LocalMetadata/Providers/AdultVideoXmlProvider.cs
  23. 5 5
      MediaBrowser.LocalMetadata/Providers/AlbumXmlProvider.cs
  24. 4 4
      MediaBrowser.LocalMetadata/Providers/ArtistXmlProvider.cs
  25. 5 4
      MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs
  26. 4 4
      MediaBrowser.LocalMetadata/Providers/ChannelXmlProvider.cs
  27. 16 6
      MediaBrowser.LocalMetadata/Providers/EpisodeXmlProvider.cs
  28. 4 4
      MediaBrowser.LocalMetadata/Providers/FolderXmlProvider.cs
  29. 5 4
      MediaBrowser.LocalMetadata/Providers/GameSystemXmlProvider.cs
  30. 5 4
      MediaBrowser.LocalMetadata/Providers/GameXmlProvider.cs
  31. 6 5
      MediaBrowser.LocalMetadata/Providers/MovieXmlProvider.cs
  32. 5 6
      MediaBrowser.LocalMetadata/Providers/MusicVideoXmlProvider.cs
  33. 4 4
      MediaBrowser.LocalMetadata/Providers/PersonXmlProvider.cs
  34. 15 5
      MediaBrowser.LocalMetadata/Providers/SeasonXmlProvider.cs
  35. 15 5
      MediaBrowser.LocalMetadata/Providers/SeriesXmlProvider.cs
  36. 6 5
      MediaBrowser.LocalMetadata/Providers/TrailerXmlProvider.cs
  37. 6 6
      MediaBrowser.LocalMetadata/Providers/VideoXmlProvider.cs
  38. 5 5
      MediaBrowser.LocalMetadata/Savers/AlbumXmlSaver.cs
  39. 5 5
      MediaBrowser.LocalMetadata/Savers/ArtistXmlSaver.cs
  40. 5 6
      MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs
  41. 5 6
      MediaBrowser.LocalMetadata/Savers/ChannelXmlSaver.cs
  42. 6 6
      MediaBrowser.LocalMetadata/Savers/EpisodeXmlSaver.cs
  43. 6 7
      MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs
  44. 4 4
      MediaBrowser.LocalMetadata/Savers/GameSystemXmlSaver.cs
  45. 5 5
      MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs
  46. 7 8
      MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs
  47. 4 6
      MediaBrowser.LocalMetadata/Savers/PersonXmlSaver.cs
  48. 5 5
      MediaBrowser.LocalMetadata/Savers/SeasonXmlSaver.cs
  49. 7 7
      MediaBrowser.LocalMetadata/Savers/SeriesXmlSaver.cs
  50. 7 7
      MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs
  51. 3 0
      MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj
  52. 3 0
      MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj
  53. 5 14
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  54. 21 0
      MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs
  55. 1 0
      MediaBrowser.Model/MediaBrowser.Model.csproj
  56. 3 3
      MediaBrowser.Providers/Channels/AudioChannelItemMetadataService.cs
  57. 3 3
      MediaBrowser.Providers/Channels/VideoChannelItemMetadataService.cs
  58. 3 46
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  59. 12 1
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  60. 84 0
      MediaBrowser.ServerApplication/ApplicationHost.cs
  61. 8 0
      MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj
  62. 1 0
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  63. 6 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  64. 29 0
      MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs
  65. 99 0
      MediaBrowser.XbmcMetadata/EntryPoint.cs
  66. 1 1
      MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs
  67. 89 0
      MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
  68. 992 0
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
  69. 211 0
      MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
  70. 97 0
      MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
  71. 45 0
      MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
  72. 93 0
      MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
  73. 36 0
      MediaBrowser.XbmcMetadata/Properties/AssemblyInfo.cs
  74. 34 0
      MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs
  75. 34 0
      MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs
  76. 89 0
      MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs
  77. 55 0
      MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
  78. 44 0
      MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs
  79. 45 0
      MediaBrowser.XbmcMetadata/Providers/MovieNfoProvider.cs
  80. 35 0
      MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs
  81. 34 0
      MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs
  82. 143 0
      MediaBrowser.XbmcMetadata/Savers/AlbumXmlSaver.cs
  83. 124 0
      MediaBrowser.XbmcMetadata/Savers/ArtistXmlSaver.cs
  84. 149 0
      MediaBrowser.XbmcMetadata/Savers/EpisodeXmlSaver.cs
  85. 143 0
      MediaBrowser.XbmcMetadata/Savers/MovieXmlSaver.cs
  86. 90 0
      MediaBrowser.XbmcMetadata/Savers/SeasonXmlSaver.cs
  87. 130 0
      MediaBrowser.XbmcMetadata/Savers/SeriesXmlSaver.cs
  88. 906 0
      MediaBrowser.XbmcMetadata/Savers/XmlSaverHelpers.cs
  89. 33 3
      MediaBrowser.sln

+ 13 - 3
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -1447,6 +1447,16 @@ namespace MediaBrowser.Api.Playback
                     state.MediaPath = mediaUrl;
                     state.InputProtocol = MediaProtocol.Http;
                 }
+                else
+                {
+                    // No media info, so this is probably needed
+                    state.DeInterlace = true;
+                }
+
+                if (recording.RecordingInfo.Status == RecordingStatus.InProgress)
+                {
+                    state.ReadInputAtNativeFramerate = true;
+                }
 
                 state.RunTimeTicks = recording.RunTimeTicks;
 
@@ -1455,9 +1465,7 @@ namespace MediaBrowser.Api.Playback
                     await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
                 }
 
-                state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress;
                 state.OutputAudioSync = "1000";
-                state.DeInterlace = true;
                 state.InputVideoSync = "-1";
                 state.InputAudioSync = "1";
                 state.InputContainer = recording.Container;
@@ -1524,7 +1532,9 @@ namespace MediaBrowser.Api.Playback
                 state.RunTimeTicks = mediaSource.RunTimeTicks;
             }
 
-            if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase))
+            // If it's a wtv and we don't have media info, we will probably need to deinterlace
+            if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) &&
+                mediaStreams.Count == 0)
             {
                 state.DeInterlace = true;
             }

+ 5 - 0
MediaBrowser.Api/Playback/BifService.cs

@@ -72,6 +72,11 @@ namespace MediaBrowser.Api.Playback
 
             try
             {
+                if (File.Exists(path))
+                {
+                    return path;
+                }
+                
                 await _mediaEncoder.ExtractVideoImagesOnInterval(inputPath, protocol, mediaSource.Video3DFormat,
                         TimeSpan.FromSeconds(10), Path.GetDirectoryName(path), "img_", request.MaxWidth, CancellationToken.None)
                         .ConfigureAwait(false);

+ 2 - 1
MediaBrowser.Api/Playback/Progressive/VideoService.cs

@@ -144,7 +144,8 @@ namespace MediaBrowser.Api.Playback.Progressive
                 return state.VideoStream != null && IsH264(state.VideoStream) ? args + " -bsf h264_mp4toannexb" : args;
             }
 
-            const string keyFrameArg = " -force_key_frames expr:if(isnan(prev_forced_t),gte(t,.1),gte(t,prev_forced_t+5))";
+            var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
+                5.ToString(UsCulture));
 
             args += keyFrameArg;
 

+ 0 - 5
MediaBrowser.Controller/Entities/Folder.cs

@@ -445,11 +445,6 @@ namespace MediaBrowser.Controller.Entities
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                if (this is UserRootFolder)
-                {
-                    var b = true;
-                }
-
                 foreach (var child in nonCachedChildren)
                 {
                     BaseItem currentChild;

+ 1 - 1
MediaBrowser.Dlna/PlayTo/PlayToController.cs

@@ -80,7 +80,7 @@ namespace MediaBrowser.Dlna.PlayTo
             _updateTimer = new Timer(updateTimer_Elapsed, null, 60000, 60000);
         }
 
-        private async void updateTimer_Elapsed(object state)
+        private void updateTimer_Elapsed(object state)
         {
             if (DateTime.UtcNow >= _device.DateLastActivity.AddSeconds(120))
             {

+ 6 - 6
MediaBrowser.Providers/BaseXmlProvider.cs → MediaBrowser.LocalMetadata/BaseXmlProvider.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Logging;
-using System;
+using System;
 using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Logging;
 
-namespace MediaBrowser.Providers
+namespace MediaBrowser.LocalMetadata
 {
     public abstract class BaseXmlProvider<T> : ILocalMetadataProvider<T>, IHasChangeMonitor
         where T : IHasMetadata, new()

+ 3 - 5
MediaBrowser.Providers/Folders/CollectionFolderImageProvider.cs → MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs

@@ -1,10 +1,8 @@
-using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Providers.All;
-using System.Collections.Generic;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.LocalMetadata.Images
 {
     public class CollectionFolderLocalImageProvider : ILocalImageFileProvider, IHasOrder
     {

+ 6 - 6
MediaBrowser.Providers/TV/EpisodeLocalImageProvider.cs → MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using System;
+using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Images
 {
     public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider
     {

+ 4 - 5
MediaBrowser.Providers/Folders/ImagesByNameImageProvider.cs → MediaBrowser.LocalMetadata/Images/ImagesByNameImageProvider.cs

@@ -1,12 +1,11 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
-using MediaBrowser.Providers.All;
-using System.Collections.Generic;
-using System.IO;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.LocalMetadata.Images
 {
     public class ImagesByNameImageProvider : ILocalImageFileProvider, IHasOrder
     {

+ 4 - 4
MediaBrowser.Providers/All/InternalMetadataFolderImageProvider.cs → MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs

@@ -1,11 +1,11 @@
-using MediaBrowser.Controller.Configuration;
+using System.Collections.Generic;
+using System.IO;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
-using System.Collections.Generic;
-using System.IO;
 
-namespace MediaBrowser.Providers.All
+namespace MediaBrowser.LocalMetadata.Images
 {
     public class InternalMetadataFolderImageProvider : ILocalImageFileProvider, IHasOrder
     {

+ 7 - 7
MediaBrowser.Providers/All/LocalImageProvider.cs → MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs

@@ -1,16 +1,16 @@
-using MediaBrowser.Controller.Entities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 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.Providers.All
+namespace MediaBrowser.LocalMetadata.Images
 {
     public class LocalImageProvider : ILocalImageFileProvider
     {

+ 110 - 0
MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj

@@ -0,0 +1,110 @@
+<?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>{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <AppDesignerFolder>Properties</AppDesignerFolder>
+    <RootNamespace>MediaBrowser.LocalMetadata</RootNamespace>
+    <AssemblyName>MediaBrowser.LocalMetadata</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="BaseXmlProvider.cs" />
+    <Compile Include="Images\CollectionFolderImageProvider.cs" />
+    <Compile Include="Images\EpisodeLocalImageProvider.cs" />
+    <Compile Include="Images\ImagesByNameImageProvider.cs" />
+    <Compile Include="Images\InternalMetadataFolderImageProvider.cs" />
+    <Compile Include="Images\LocalImageProvider.cs" />
+    <Compile Include="Parsers\BoxSetXmlParser.cs" />
+    <Compile Include="Parsers\EpisodeXmlParser.cs" />
+    <Compile Include="Parsers\GameSystemXmlParser.cs" />
+    <Compile Include="Parsers\GameXmlParser.cs" />
+    <Compile Include="Parsers\MovieXmlParser.cs" />
+    <Compile Include="Parsers\MusicVideoXmlParser.cs" />
+    <Compile Include="Parsers\SeasonXmlParser.cs" />
+    <Compile Include="Parsers\SeriesXmlParser.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Providers\AdultVideoXmlProvider.cs" />
+    <Compile Include="Providers\AlbumXmlProvider.cs" />
+    <Compile Include="Providers\ArtistXmlProvider.cs" />
+    <Compile Include="Providers\BoxSetXmlProvider.cs" />
+    <Compile Include="Providers\ChannelXmlProvider.cs" />
+    <Compile Include="Providers\EpisodeXmlProvider.cs" />
+    <Compile Include="Providers\FolderXmlProvider.cs" />
+    <Compile Include="Providers\GameSystemXmlProvider.cs" />
+    <Compile Include="Providers\GameXmlProvider.cs" />
+    <Compile Include="Providers\MovieXmlProvider.cs" />
+    <Compile Include="Providers\MusicVideoXmlProvider.cs" />
+    <Compile Include="Providers\PersonXmlProvider.cs" />
+    <Compile Include="Providers\SeasonXmlProvider.cs" />
+    <Compile Include="Providers\SeriesXmlProvider.cs" />
+    <Compile Include="Providers\TrailerXmlProvider.cs" />
+    <Compile Include="Providers\VideoXmlProvider.cs" />
+    <Compile Include="Savers\AlbumXmlSaver.cs" />
+    <Compile Include="Savers\ArtistXmlSaver.cs" />
+    <Compile Include="Savers\BoxSetXmlSaver.cs" />
+    <Compile Include="Savers\ChannelXmlSaver.cs" />
+    <Compile Include="Savers\EpisodeXmlSaver.cs" />
+    <Compile Include="Savers\FolderXmlSaver.cs" />
+    <Compile Include="Savers\GameSystemXmlSaver.cs" />
+    <Compile Include="Savers\GameXmlSaver.cs" />
+    <Compile Include="Savers\MovieXmlSaver.cs" />
+    <Compile Include="Savers\PersonXmlSaver.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>
+  <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>

+ 5 - 5
MediaBrowser.Providers/BoxSets/BoxSetXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Xml;
 
-namespace MediaBrowser.Providers.BoxSets
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     public class BoxSetXmlParser : BaseItemXmlParser<BoxSet>
     {

+ 6 - 6
MediaBrowser.Providers/TV/EpisodeXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/EpisodeXmlParser.cs

@@ -1,15 +1,15 @@
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Threading;
 using System.Xml;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     /// <summary>
     /// Class EpisodeXmlParser

+ 5 - 5
MediaBrowser.Providers/Games/GameSystemXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/GameSystemXmlParser.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml;
 
-namespace MediaBrowser.Providers.Games
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     public class GameSystemXmlParser : BaseItemXmlParser<GameSystem>
     {

+ 6 - 6
MediaBrowser.Providers/Games/GameXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/GameXmlParser.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using System.Globalization;
+using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
 
-namespace MediaBrowser.Providers.Games
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     /// <summary>
     /// Class EpisodeXmlParser

+ 5 - 5
MediaBrowser.Providers/Movies/MovieXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/MovieXmlParser.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.Threading;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System.Collections.Generic;
-using System.Threading;
-using System.Xml;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     /// <summary>
     /// Class EpisodeXmlParser

+ 3 - 4
MediaBrowser.Providers/Music/MusicVideoXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/MusicVideoXmlParser.cs

@@ -1,10 +1,9 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.Xml;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     public class MusicVideoXmlParser : BaseItemXmlParser<MusicVideo>
     {

+ 3 - 3
MediaBrowser.Providers/TV/SeasonXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/SeasonXmlParser.cs

@@ -1,9 +1,9 @@
-using MediaBrowser.Controller.Entities.TV;
+using System.Xml;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.Xml;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     public class SeasonXmlParser : BaseItemXmlParser<Season>
     {

+ 4 - 4
MediaBrowser.Providers/TV/SeriesXmlParser.cs → MediaBrowser.LocalMetadata/Parsers/SeriesXmlParser.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities.TV;
+using System;
+using System.Xml;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System;
-using System.Xml;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Parsers
 {
     /// <summary>
     /// Class SeriesXmlParser

+ 36 - 0
MediaBrowser.LocalMetadata/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.LocalMetadata")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("MediaBrowser.LocalMetadata")]
+[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("1c669501-2113-493a-b0ed-f8fd26311941")]
+
+// 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")]

+ 6 - 6
MediaBrowser.Providers/AdultVideos/AdultVideoXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/AdultVideoXmlProvider.cs

@@ -1,14 +1,14 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Providers.Movies;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.AdultVideos
+namespace MediaBrowser.LocalMetadata.Providers
 {
     class AdultVideoXmlProvider : BaseXmlProvider<AdultVideo>
     {

+ 5 - 5
MediaBrowser.Providers/Music/AlbumXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/AlbumXmlProvider.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.LocalMetadata.Providers
 {
-    class AlbumXmlProvider : BaseXmlProvider<MusicAlbum>
+    public class AlbumXmlProvider : BaseXmlProvider<MusicAlbum>
     {
         private readonly ILogger _logger;
 

+ 4 - 4
MediaBrowser.Providers/Music/ArtistXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/ArtistXmlProvider.cs

@@ -1,11 +1,11 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.LocalMetadata.Providers
 {
     class ArtistXmlProvider : BaseXmlProvider<MusicArtist>
     {

+ 5 - 4
MediaBrowser.Providers/BoxSets/BoxSetXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/BoxSetXmlProvider.cs

@@ -1,11 +1,12 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.BoxSets
+namespace MediaBrowser.LocalMetadata.Providers
 {
     /// <summary>
     /// Class BoxSetXmlProvider.

+ 4 - 4
MediaBrowser.Providers/LiveTv/ChannelXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/ChannelXmlProvider.cs

@@ -1,11 +1,11 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.LiveTv
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class ChannelXmlProvider : BaseXmlProvider<LiveTvChannel>
     {

+ 16 - 6
MediaBrowser.Providers/TV/EpisodeXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/EpisodeXmlProvider.cs

@@ -1,15 +1,16 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Providers
 {
-    public class EpisodeXmlProvider : BaseXmlProvider<Episode>
+    public class EpisodeXmlProvider : BaseXmlProvider<Episode>, IHasOrder
     {
         private readonly ILogger _logger;
 
@@ -39,5 +40,14 @@ namespace MediaBrowser.Providers.TV
 
             return directoryService.GetFile(metadataFile);
         }
+
+        public int Order
+        {
+            get
+            {
+                // After Xbmc
+                return 1;
+            }
+        }
     }
 }

+ 4 - 4
MediaBrowser.Providers/Folders/FolderXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/FolderXmlProvider.cs

@@ -1,11 +1,11 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Folders
+namespace MediaBrowser.LocalMetadata.Providers
 {
     /// <summary>
     /// Provides metadata for Folders and all subclasses by parsing folder.xml

+ 5 - 4
MediaBrowser.Providers/Games/GameSystemXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/GameSystemXmlProvider.cs

@@ -1,11 +1,12 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Games
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class GameSystemXmlProvider : BaseXmlProvider<GameSystem>
     {

+ 5 - 4
MediaBrowser.Providers/Games/GameXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/GameXmlProvider.cs

@@ -1,11 +1,12 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Games
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class GameXmlProvider : BaseXmlProvider<Game>
     {

+ 6 - 5
MediaBrowser.Providers/Movies/MovieXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/MovieXmlProvider.cs

@@ -1,13 +1,14 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class MovieXmlProvider : BaseXmlProvider<Movie>
     {

+ 5 - 6
MediaBrowser.Providers/Music/MusicVideoXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/MusicVideoXmlProvider.cs

@@ -1,13 +1,12 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Providers.Movies;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Music
+namespace MediaBrowser.LocalMetadata.Providers
 {
     class MusicVideoXmlProvider : BaseXmlProvider<MusicVideo>
     {

+ 4 - 4
MediaBrowser.Providers/People/PersonXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/PersonXmlProvider.cs

@@ -1,11 +1,11 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.People
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class PersonXmlProvider : BaseXmlProvider<Person>
     {

+ 15 - 5
MediaBrowser.Providers/TV/SeasonXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/SeasonXmlProvider.cs

@@ -1,16 +1,17 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Providers
 {
     /// <summary>
     /// Class SeriesProviderFromXml
     /// </summary>
-    public class SeasonXmlProvider : BaseXmlProvider<Season>
+    public class SeasonXmlProvider : BaseXmlProvider<Season>, IHasOrder
     {
         private readonly ILogger _logger;
 
@@ -29,6 +30,15 @@ namespace MediaBrowser.Providers.TV
         {
             return directoryService.GetFile(Path.Combine(info.Path, "season.xml"));
         }
+
+        public int Order
+        {
+            get
+            {
+                // After Xbmc
+                return 1;
+            }
+        }
     }
 }
 

+ 15 - 5
MediaBrowser.Providers/TV/SeriesXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/SeriesXmlProvider.cs

@@ -1,16 +1,17 @@
-using MediaBrowser.Common.IO;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Logging;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.TV
+namespace MediaBrowser.LocalMetadata.Providers
 {
     /// <summary>
     /// Class SeriesProviderFromXml
     /// </summary>
-    public class SeriesXmlProvider : BaseXmlProvider<Series>
+    public class SeriesXmlProvider : BaseXmlProvider<Series>, IHasOrder
     {
         private readonly ILogger _logger;
 
@@ -29,5 +30,14 @@ namespace MediaBrowser.Providers.TV
         {
             return directoryService.GetFile(Path.Combine(info.Path, "series.xml"));
         }
+
+        public int Order
+        {
+            get
+            {
+                // After Xbmc
+                return 1;
+            }
+        }
     }
 }

+ 6 - 5
MediaBrowser.Providers/Movies/TrailerXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/TrailerXmlProvider.cs

@@ -1,13 +1,14 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Movies
+namespace MediaBrowser.LocalMetadata.Providers
 {
     public class TrailerXmlProvider : BaseXmlProvider<Trailer>
     {

+ 6 - 6
MediaBrowser.Providers/Videos/VideoXmlProvider.cs → MediaBrowser.LocalMetadata/Providers/VideoXmlProvider.cs

@@ -1,14 +1,14 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.LocalMetadata.Parsers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
-using MediaBrowser.Providers.Movies;
-using System.Collections.Generic;
-using System.IO;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Videos
+namespace MediaBrowser.LocalMetadata.Providers
 {
     class VideoXmlProvider : BaseXmlProvider<Video>
     {

+ 5 - 5
MediaBrowser.Providers/Savers/AlbumXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/AlbumXmlSaver.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     class AlbumXmlSaver : IMetadataFileSaver
     {

+ 5 - 5
MediaBrowser.Providers/Savers/ArtistXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/ArtistXmlSaver.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     class ArtistXmlSaver : IMetadataFileSaver
     {

+ 5 - 6
MediaBrowser.Providers/Savers/BoxSetXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/BoxSetXmlSaver.cs

@@ -1,13 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class BoxSetXmlSaver : IMetadataFileSaver
     {

+ 5 - 6
MediaBrowser.Providers/Savers/ChannelXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/ChannelXmlSaver.cs

@@ -1,13 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     /// <summary>
     /// Class PersonXmlSaver

+ 6 - 6
MediaBrowser.Providers/Savers/EpisodeXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/EpisodeXmlSaver.cs

@@ -1,15 +1,15 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class EpisodeXmlSaver : IMetadataFileSaver
     {

+ 6 - 7
MediaBrowser.Providers/Savers/FolderXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/FolderXmlSaver.cs

@@ -1,15 +1,14 @@
-using MediaBrowser.Controller.Entities;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+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.Model.Entities;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using System.Threading;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class FolderXmlSaver : IMetadataFileSaver
     {

+ 4 - 4
MediaBrowser.Providers/Savers/GameSystemXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/GameSystemXmlSaver.cs

@@ -1,12 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class GameSystemXmlSaver : IMetadataFileSaver
     {

+ 5 - 5
MediaBrowser.Providers/Savers/GameXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/GameXmlSaver.cs

@@ -1,14 +1,14 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     /// <summary>
     /// Saves game.xml for games

+ 7 - 8
MediaBrowser.Providers/Savers/MovieXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/MovieXmlSaver.cs

@@ -1,16 +1,15 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using System.Collections.Generic;
-using System.Globalization;
+using System.Collections.Generic;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     /// <summary>
     /// Saves movie.xml for movies, trailers and music videos

+ 4 - 6
MediaBrowser.Providers/Savers/PersonXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/PersonXmlSaver.cs

@@ -1,14 +1,12 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
-using MediaBrowser.Model.Entities;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     /// <summary>
     /// Class PersonXmlSaver

+ 5 - 5
MediaBrowser.Providers/Savers/SeasonXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/SeasonXmlSaver.cs

@@ -1,14 +1,14 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using System.Collections.Generic;
+using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class SeasonXmlSaver : IMetadataFileSaver
     {

+ 7 - 7
MediaBrowser.Providers/Savers/SeriesXmlSaver.cs → MediaBrowser.LocalMetadata/Savers/SeriesXmlSaver.cs

@@ -1,15 +1,15 @@
-using System.Globalization;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Security;
 using System.Text;
 using System.Threading;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     public class SeriesXmlSaver : IMetadataFileSaver
     {

+ 7 - 7
MediaBrowser.Providers/Savers/XmlSaverHelpers.cs → MediaBrowser.LocalMetadata/Savers/XmlSaverHelpers.cs

@@ -1,9 +1,4 @@
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -11,8 +6,13 @@ using System.Linq;
 using System.Security;
 using System.Text;
 using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
 
-namespace MediaBrowser.Providers.Savers
+namespace MediaBrowser.LocalMetadata.Savers
 {
     /// <summary>
     /// Class XmlHelpers

+ 3 - 0
MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj

@@ -170,6 +170,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\UserConfiguration.cs">
       <Link>Configuration\UserConfiguration.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\XbmcMetadataOptions.cs">
+      <Link>Configuration\XbmcMetadataOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dlna\AudioOptions.cs">
       <Link>Dlna\AudioOptions.cs</Link>
     </Compile>

+ 3 - 0
MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj

@@ -157,6 +157,9 @@
     <Compile Include="..\MediaBrowser.Model\Configuration\UserConfiguration.cs">
       <Link>Configuration\UserConfiguration.cs</Link>
     </Compile>
+    <Compile Include="..\MediaBrowser.Model\Configuration\XbmcMetadataOptions.cs">
+      <Link>Configuration\XbmcMetadataOptions.cs</Link>
+    </Compile>
     <Compile Include="..\MediaBrowser.Model\Dlna\AudioOptions.cs">
       <Link>Dlna\AudioOptions.cs</Link>
     </Compile>

+ 5 - 14
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -1,4 +1,5 @@
-using MediaBrowser.Model.Weather;
+using System.Linq;
+using MediaBrowser.Model.Weather;
 using System;
 
 namespace MediaBrowser.Model.Configuration
@@ -68,24 +69,12 @@ namespace MediaBrowser.Model.Configuration
         /// <value>The display name of the season zero.</value>
         public string SeasonZeroDisplayName { get; set; }
 
-        /// <summary>
-        /// Gets or sets the metadata refresh days.
-        /// </summary>
-        /// <value>The metadata refresh days.</value>
-        public int MetadataRefreshDays { get; set; }
-
         /// <summary>
         /// Gets or sets a value indicating whether [save local meta].
         /// </summary>
         /// <value><c>true</c> if [save local meta]; otherwise, <c>false</c>.</value>
         public bool SaveLocalMeta { get; set; }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether [refresh item images].
-        /// </summary>
-        /// <value><c>true</c> if [refresh item images]; otherwise, <c>false</c>.</value>
-        public bool RefreshItemImages { get; set; }
-
         /// <summary>
         /// Gets or sets the preferred metadata language.
         /// </summary>
@@ -227,6 +216,9 @@ namespace MediaBrowser.Model.Configuration
         [Obsolete]
         public ChapterOptions ChapterOptions { get; set; }
 
+        [Obsolete]
+        public bool DefaultMetadataSettingsApplied { get; set; }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -258,7 +250,6 @@ namespace MediaBrowser.Model.Configuration
 
             PathSubstitutions = new PathSubstitution[] { };
 
-            MetadataRefreshDays = 30;
             PreferredMetadataLanguage = "en";
             MetadataCountryCode = "US";
 

+ 21 - 0
MediaBrowser.Model/Configuration/XbmcMetadataOptions.cs

@@ -0,0 +1,21 @@
+
+namespace MediaBrowser.Model.Configuration
+{
+    public class XbmcMetadataOptions
+    {
+        public string UserId { get; set; }
+
+        public string ReleaseDateFormat { get; set; }
+
+        public bool SaveImagePathsInNfo { get; set; }
+        public bool EnablePathSubstitution { get; set; }
+
+        public XbmcMetadataOptions()
+        {
+            ReleaseDateFormat = "yyyy-MM-dd";
+
+            SaveImagePathsInNfo = true;
+            EnablePathSubstitution = true;
+        }
+    }
+}

+ 1 - 0
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -75,6 +75,7 @@
     <Compile Include="Chapters\RemoteChapterResult.cs" />
     <Compile Include="Configuration\ChannelOptions.cs" />
     <Compile Include="Configuration\ChapterOptions.cs" />
+    <Compile Include="Configuration\XbmcMetadataOptions.cs" />
     <Compile Include="Configuration\SubtitlePlaybackMode.cs" />
     <Compile Include="Configuration\TvFileOrganizationOptions.cs" />
     <Compile Include="Configuration\BaseApplicationConfiguration.cs" />

+ 3 - 3
MediaBrowser.Providers/GameGenres/AudioChannelItemMetadataService.cs → MediaBrowser.Providers/Channels/AudioChannelItemMetadataService.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Providers.Manager;
-using System.Collections.Generic;
 
-namespace MediaBrowser.Providers.GameGenres
+namespace MediaBrowser.Providers.Channels
 {
     public class AudioChannelItemMetadataService : MetadataService<ChannelAudioItem, ItemLookupInfo>
     {

+ 3 - 3
MediaBrowser.Providers/GameGenres/VideoChannelItemMetadataService.cs → MediaBrowser.Providers/Channels/VideoChannelItemMetadataService.cs

@@ -1,13 +1,13 @@
-using MediaBrowser.Common.IO;
+using System.Collections.Generic;
+using MediaBrowser.Common.IO;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Providers.Manager;
-using System.Collections.Generic;
 
-namespace MediaBrowser.Providers.GameGenres
+namespace MediaBrowser.Providers.Channels
 {
     public class VideoChannelItemMetadataService : MetadataService<ChannelVideoItem, ItemLookupInfo>
     {

+ 3 - 46
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -70,41 +70,28 @@
       <Link>Properties\SharedVersion.cs</Link>
     </Compile>
     <Compile Include="AdultVideos\AdultVideoMetadataService.cs" />
-    <Compile Include="AdultVideos\AdultVideoXmlProvider.cs" />
-    <Compile Include="All\InternalMetadataFolderImageProvider.cs" />
-    <Compile Include="All\LocalImageProvider.cs" />
     <Compile Include="Books\BookMetadataService.cs" />
     <Compile Include="BoxSets\BoxSetMetadataService.cs" />
-    <Compile Include="BoxSets\BoxSetXmlParser.cs" />
     <Compile Include="BoxSets\MovieDbBoxSetImageProvider.cs" />
     <Compile Include="BoxSets\MovieDbBoxSetProvider.cs" />
     <Compile Include="Channels\ChannelMetadataService.cs" />
     <Compile Include="Chapters\ChapterManager.cs" />
     <Compile Include="FolderImages\DefaultImageProvider.cs" />
-    <Compile Include="Folders\CollectionFolderImageProvider.cs" />
     <Compile Include="Folders\FolderMetadataService.cs" />
-    <Compile Include="Folders\ImagesByNameImageProvider.cs" />
-    <Compile Include="GameGenres\AudioChannelItemMetadataService.cs" />
+    <Compile Include="Channels\AudioChannelItemMetadataService.cs" />
     <Compile Include="GameGenres\GameGenreMetadataService.cs" />
-    <Compile Include="GameGenres\VideoChannelItemMetadataService.cs" />
+    <Compile Include="Channels\VideoChannelItemMetadataService.cs" />
     <Compile Include="Games\GameMetadataService.cs" />
     <Compile Include="Games\GameSystemMetadataService.cs" />
-    <Compile Include="Games\GameSystemXmlParser.cs" />
     <Compile Include="Genres\GenreMetadataService.cs" />
     <Compile Include="LiveTv\AudioRecordingService.cs" />
     <Compile Include="LiveTv\ChannelMetadataService.cs" />
-    <Compile Include="LiveTv\ChannelXmlProvider.cs" />
     <Compile Include="LiveTv\ProgramMetadataService.cs" />
     <Compile Include="LiveTv\VideoRecordingService.cs" />
     <Compile Include="Manager\ImageSaver.cs" />
     <Compile Include="Manager\ItemImageProvider.cs" />
     <Compile Include="Manager\ProviderManager.cs" />
     <Compile Include="Manager\MetadataService.cs" />
-    <Compile Include="BaseXmlProvider.cs" />
-    <Compile Include="Folders\FolderXmlProvider.cs" />
-    <Compile Include="Games\GameXmlParser.cs" />
-    <Compile Include="Games\GameXmlProvider.cs" />
-    <Compile Include="Games\GameSystemXmlProvider.cs" />
     <Compile Include="Manager\SeriesOrderManager.cs" />
     <Compile Include="MediaInfo\FFProbeAudioInfo.cs" />
     <Compile Include="MediaInfo\FFProbeHelpers.cs" />
@@ -118,16 +105,13 @@
     <Compile Include="Movies\GenericMovieDbInfo.cs" />
     <Compile Include="Movies\MovieDbSearch.cs" />
     <Compile Include="Movies\MovieMetadataService.cs" />
-    <Compile Include="Movies\MovieXmlProvider.cs" />
     <Compile Include="Movies\TmdbSettings.cs" />
-    <Compile Include="Movies\TrailerXmlProvider.cs" />
     <Compile Include="MusicGenres\MusicGenreImageProvider.cs" />
     <Compile Include="GameGenres\GameGenreImageProvider.cs" />
     <Compile Include="Genres\GenreImageProvider.cs" />
     <Compile Include="ImagesByName\ImageUtils.cs" />
     <Compile Include="MediaInfo\AudioImageProvider.cs" />
     <Compile Include="MediaInfo\VideoImageProvider.cs" />
-    <Compile Include="BoxSets\BoxSetXmlProvider.cs" />
     <Compile Include="Movies\MovieDbImageProvider.cs" />
     <Compile Include="Movies\FanartMovieImageProvider.cs" />
     <Compile Include="MusicGenres\MusicGenreMetadataService.cs" />
@@ -146,16 +130,12 @@
     <Compile Include="Music\MusicBrainzArtistProvider.cs" />
     <Compile Include="Music\MusicExternalIds.cs" />
     <Compile Include="Music\MusicVideoMetadataService.cs" />
-    <Compile Include="Music\MusicVideoXmlProvider.cs" />
     <Compile Include="Omdb\OmdbProvider.cs" />
     <Compile Include="Omdb\OmdbItemProvider.cs" />
     <Compile Include="People\MovieDbPersonImageProvider.cs" />
     <Compile Include="Movies\MovieUpdatesPrescanTask.cs" />
-    <Compile Include="Movies\MovieXmlParser.cs" />
     <Compile Include="Movies\FanArtMovieUpdatesPostScanTask.cs" />
     <Compile Include="Movies\MovieDbProvider.cs" />
-    <Compile Include="Music\AlbumXmlProvider.cs" />
-    <Compile Include="Music\ArtistXmlProvider.cs" />
     <Compile Include="Music\FanArtUpdatesPostScanTask.cs" />
     <Compile Include="Music\LastfmAlbumProvider.cs" />
     <Compile Include="Music\LastfmHelper.cs" />
@@ -163,10 +143,8 @@
     <Compile Include="Music\FanArtArtistProvider.cs" />
     <Compile Include="Music\LastFmImageProvider.cs" />
     <Compile Include="Music\MusicBrainzAlbumProvider.cs" />
-    <Compile Include="Music\MusicVideoXmlParser.cs" />
     <Compile Include="Music\SoundtrackPostScanTask.cs" />
     <Compile Include="People\PersonMetadataService.cs" />
-    <Compile Include="People\PersonXmlProvider.cs" />
     <Compile Include="People\MovieDbPersonProvider.cs" />
     <Compile Include="Photos\ExifReader.cs" />
     <Compile Include="Photos\ExifTags.cs" />
@@ -175,34 +153,17 @@
     <Compile Include="Photos\PhotoProvider.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Manager\ProviderUtils.cs" />
-    <Compile Include="Savers\AlbumXmlSaver.cs" />
-    <Compile Include="Savers\ArtistXmlSaver.cs" />
-    <Compile Include="Savers\BoxSetXmlSaver.cs" />
-    <Compile Include="Savers\ChannelXmlSaver.cs" />
-    <Compile Include="Savers\EpisodeXmlSaver.cs" />
-    <Compile Include="Savers\FolderXmlSaver.cs" />
-    <Compile Include="Savers\GameSystemXmlSaver.cs" />
-    <Compile Include="Savers\GameXmlSaver.cs" />
-    <Compile Include="Savers\MovieXmlSaver.cs" />
-    <Compile Include="Savers\PersonXmlSaver.cs" />
-    <Compile Include="Savers\SeasonXmlSaver.cs" />
-    <Compile Include="Savers\SeriesXmlSaver.cs" />
-    <Compile Include="Savers\XmlSaverHelpers.cs" />
     <Compile Include="Studios\StudiosImageProvider.cs" />
     <Compile Include="Studios\StudioMetadataService.cs" />
     <Compile Include="Subtitles\OpenSubtitleDownloader.cs" />
     <Compile Include="Subtitles\SubtitleManager.cs" />
-    <Compile Include="TV\EpisodeLocalImageProvider.cs" />
     <Compile Include="TV\EpisodeMetadataService.cs" />
-    <Compile Include="TV\EpisodeXmlProvider.cs" />
-    <Compile Include="TV\EpisodeXmlParser.cs" />
     <Compile Include="TV\FanArtTvUpdatesPostScanTask.cs" />
     <Compile Include="TV\FanArtSeasonProvider.cs" />
     <Compile Include="TV\FanartSeriesProvider.cs" />
     <Compile Include="TV\MissingEpisodeProvider.cs" />
     <Compile Include="TV\MovieDbSeriesImageProvider.cs" />
     <Compile Include="TV\MovieDbSeriesProvider.cs" />
-    <Compile Include="TV\SeasonXmlParser.cs" />
     <Compile Include="TV\SeriesMetadataService.cs" />
     <Compile Include="TV\TvdbEpisodeImageProvider.cs" />
     <Compile Include="People\TvdbPersonImageProvider.cs" />
@@ -212,16 +173,11 @@
     <Compile Include="TV\SeasonMetadataService.cs" />
     <Compile Include="TV\TvdbEpisodeProvider.cs" />
     <Compile Include="TV\TvdbSeriesProvider.cs" />
-    <Compile Include="TV\SeasonXmlProvider.cs" />
     <Compile Include="TV\SeriesPostScanTask.cs" />
-    <Compile Include="TV\SeriesXmlProvider.cs" />
-    <Compile Include="TV\SeriesXmlParser.cs" />
     <Compile Include="TV\TvdbPrescanTask.cs" />
     <Compile Include="TV\TvExternalIds.cs" />
     <Compile Include="Users\UserMetadataService.cs" />
     <Compile Include="Videos\VideoMetadataService.cs" />
-    <Compile Include="Videos\VideoXmlProvider.cs" />
-    <Compile Include="Xbmc\XbmcImageSaver.cs" />
     <Compile Include="Years\YearMetadataService.cs" />
   </ItemGroup>
   <ItemGroup>
@@ -248,6 +204,7 @@
   <ItemGroup>
     <EmbeddedResource Include="MediaInfo\whitelist.txt" />
   </ItemGroup>
+  <ItemGroup />
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition=" '$(ConfigurationName)' != 'Release Mono' " />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 12 - 1
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -835,5 +835,16 @@
 	"TitleRemoteControl": "Remote Control",
 	"OptionLatestTvRecordings": "Latest recordings",
 	"LabelProtocolInfo": "Protocol info:",
-	"LabelProtocolInfoHelp": "The value that will be used when responding to GetProtocolInfo requests from the device."
+	"LabelProtocolInfoHelp": "The value that will be used when responding to GetProtocolInfo requests from the device.",
+	"TabXbmcMetadata": "Xbmc",
+	"HeaderXbmcMetadataHelp": "Media Browser includes native support for Xbmc Nfo metadata and images. To enable or disable Xbmc metadata, use the Advanced tab to configure options for your media types.",
+	"LabelXbmcMetadataUser": "Add user watch data to nfo's for:",
+	"LabelXbmcMetadataUserHelp": "Enable this to keep watch data in sync between Media Browser and Xbmc.",
+	"LabelXbmcMetadataDateFormat": "Release date format:",
+	"LabelXbmcMetadataDateFormatHelp": "All dates within nfo's will be read and written to using this format.",
+	"LabelXbmcMetadataSaveImagePaths": "Save image paths within nfo files",
+	"LabelXbmcMetadataSaveImagePathsHelp": "This is recommended if you have image file names that don't conform to Xbmc guidelines.",
+	"LabelXbmcMetadataEnablePathSubstitution": "Enable path substitution",
+	"LabelXbmcMetadataEnablePathSubstitutionHelp": "Enables path substitution of image paths using the server's path substitution settings.",
+	"LabelXbmcMetadataEnablePathSubstitutionHelp2": "See path substitution."
 }

+ 84 - 0
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -17,6 +17,9 @@ using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.FileOrganization;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
@@ -38,9 +41,11 @@ using MediaBrowser.Dlna;
 using MediaBrowser.Dlna.ConnectionManager;
 using MediaBrowser.Dlna.ContentDirectory;
 using MediaBrowser.Dlna.Main;
+using MediaBrowser.LocalMetadata.Providers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Encoder;
 using MediaBrowser.MediaEncoding.Subtitles;
+using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
@@ -75,6 +80,7 @@ using MediaBrowser.ServerApplication.IO;
 using MediaBrowser.ServerApplication.Native;
 using MediaBrowser.ServerApplication.Networking;
 using MediaBrowser.WebDashboard.Api;
+using MediaBrowser.XbmcMetadata.Providers;
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -283,6 +289,7 @@ namespace MediaBrowser.ServerApplication
             DeleteDeprecatedModules();
 
             MigrateModularConfigurations();
+            ApplyDefaultXbmcSettings();
         }
 
         private void MigrateModularConfigurations()
@@ -309,6 +316,68 @@ namespace MediaBrowser.ServerApplication
             }
         }
 
+        private void ApplyDefaultXbmcSettings()
+        {
+            if (!ServerConfigurationManager.Configuration.DefaultMetadataSettingsApplied)
+            {
+                // Make sure xbmc metadata is disabled for existing users.
+                // New users can just take the defaults.
+                var service = ServerConfigurationManager.Configuration.IsStartupWizardCompleted
+                    ? "Xbmc Nfo"
+                    : "Media Browser Xml";
+
+                DisableMetadataService(typeof(Movie), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(MusicAlbum), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(MusicArtist), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(Episode), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(Season), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(Series), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(MusicVideo), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(Trailer), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(AdultVideo), ServerConfigurationManager.Configuration, service);
+                DisableMetadataService(typeof(Video), ServerConfigurationManager.Configuration, service);
+            }
+
+            ServerConfigurationManager.Configuration.DefaultMetadataSettingsApplied = true;
+            ServerConfigurationManager.SaveConfiguration();
+        }
+
+        private void DisableMetadataService(Type type, ServerConfiguration config, string service)
+        {
+            var options = GetMetadataOptions(type, config);
+
+            if (!options.DisabledMetadataSavers.Contains(service, StringComparer.OrdinalIgnoreCase))
+            {
+                var list = options.DisabledMetadataSavers.ToList();
+
+                list.Add(service);
+
+                options.DisabledMetadataSavers = list.ToArray();
+            }
+        }
+
+        private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config)
+        {
+            var options = config.MetadataOptions
+                .FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase));
+
+            if (options == null)
+            {
+                var list = config.MetadataOptions.ToList();
+
+                options = new MetadataOptions
+                {
+                    ItemType = type.Name
+                };
+
+                list.Add(options);
+
+                config.MetadataOptions = list.ToArray();
+            }
+
+            return options;
+        }
+
         private void DeleteDeprecatedModules()
         {
             try
@@ -328,6 +397,15 @@ namespace MediaBrowser.ServerApplication
                 // Not there, no big deal
             }
 
+            try
+            {
+                File.Delete(Path.Combine(ApplicationPaths.PluginsPath, "MediaBrowser.Plugins.XbmcMetadata.dll"));
+            }
+            catch (IOException)
+            {
+                // Not there, no big deal
+            }
+
             Task.Run(() =>
             {
                 try
@@ -912,6 +990,12 @@ namespace MediaBrowser.ServerApplication
             // Dlna 
             list.Add(typeof(DlnaEntryPoint).Assembly);
 
+            // Local metadata 
+            list.Add(typeof(AlbumXmlProvider).Assembly);
+
+            // Xbmc 
+            list.Add(typeof(ArtistNfoProvider).Assembly);
+
             list.AddRange(Assemblies.GetAssembliesWithParts());
 
             // Include composable parts in the running assembly

+ 8 - 0
MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj

@@ -198,6 +198,10 @@
       <Project>{734098eb-6dc1-4dd0-a1ca-3140dcd2737c}</Project>
       <Name>MediaBrowser.Dlna</Name>
     </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj">
+      <Project>{7ef9f3e0-697d-42f3-a08f-19deb5f84392}</Project>
+      <Name>MediaBrowser.LocalMetadata</Name>
+    </ProjectReference>
     <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj">
       <Project>{0bd82fa6-eb8a-4452-8af5-74f9c3849451}</Project>
       <Name>MediaBrowser.MediaEncoding</Name>
@@ -218,6 +222,10 @@
       <Project>{5624b7b5-b5a7-41d8-9f10-cc5611109619}</Project>
       <Name>MediaBrowser.WebDashboard</Name>
     </ProjectReference>
+    <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj">
+      <Project>{23499896-b135-4527-8574-c26e926ea99e}</Project>
+      <Name>MediaBrowser.XbmcMetadata</Name>
+    </ProjectReference>
   </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />

+ 1 - 0
MediaBrowser.WebDashboard/Api/DashboardService.cs

@@ -594,6 +594,7 @@ namespace MediaBrowser.WebDashboard.Api
                                 "metadataimagespage.js",
                                 "metadatasubtitles.js",
                                 "metadatachapters.js",
+                                "metadataxbmc.js",
                                 "moviegenres.js",
                                 "moviecollections.js",
                                 "movies.js",

+ 6 - 0
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

@@ -338,6 +338,9 @@
     <Content Include="dashboard-ui\metadatachapters.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\metadataxbmc.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\mypreferencesdisplay.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -683,6 +686,9 @@
     <Content Include="dashboard-ui\scripts\metadatachapters.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\metadataxbmc.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\mypreferencesdisplay.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 29 - 0
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");
+        }
+    }
+}

+ 99 - 0
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);
+            }
+        }
+    }
+}

+ 1 - 1
MediaBrowser.Providers/Xbmc/XbmcImageSaver.cs → MediaBrowser.XbmcMetadata/Images/XbmcImageSaver.cs

@@ -10,7 +10,7 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 
-namespace MediaBrowser.Providers.Xbmc
+namespace MediaBrowser.XbmcMetadata.Images
 {
     public class XbmcImageSaver : IImageFileSaver
     {

+ 89 - 0
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>

+ 992 - 0
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);
+        }
+    }
+}

+ 211 - 0
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;
+            }
+        }
+    }
+}

+ 97 - 0
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;
+            }
+        }
+    }
+}

+ 45 - 0
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;
+            }
+        }
+    }
+}

+ 93 - 0
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;
+            }
+        }
+    }
+}

+ 36 - 0
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")]

+ 34 - 0
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"));
+        }
+    }
+}

+ 34 - 0
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"));
+        }
+    }
+}

+ 89 - 0
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);
+    }
+}

+ 55 - 0
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");
+        }
+    }
+}

+ 44 - 0
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);
+        }
+    }
+}

+ 45 - 0
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)
+        {
+        }
+    }
+
+}

+ 35 - 0
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"));
+        }
+    }
+}
+

+ 34 - 0
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"));
+        }
+    }
+}

+ 143 - 0
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>");
+            }
+        }
+    }
+}

+ 124 - 0
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>");
+            }
+        }
+    }
+}

+ 149 - 0
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;
+        }
+    }
+}

+ 143 - 0
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;
+        }
+    }
+}

+ 90 - 0
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;
+        }
+    }
+}

+ 130 - 0
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;
+        }
+    }
+}

+ 906 - 0
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();
+        }
+    }
+}

+ 33 - 3
MediaBrowser.sln

@@ -1,8 +1,6 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 2013
-VisualStudioVersion = 12.0.21005.1
-MinimumVisualStudioVersion = 10.0.40219.1
+# Visual Studio 2012
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Controller", "MediaBrowser.Controller\MediaBrowser.Controller.csproj", "{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Api", "MediaBrowser.Api\MediaBrowser.Api.csproj", "{4FD51AC5-2C16-4308-A993-C3A84F3B4582}"
@@ -47,6 +45,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.MediaEncoding"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenSubtitlesHandler", "OpenSubtitlesHandler\OpenSubtitlesHandler.csproj", "{4A4402D4-E910-443B-B8FC-2C18286A2CA0}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.XbmcMetadata", "MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj", "{23499896-B135-4527-8574-C26E926EA99E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.LocalMetadata", "MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj", "{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -281,6 +283,34 @@ Global
 		{4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|Win32.ActiveCfg = Release|Any CPU
 		{4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|x64.ActiveCfg = Release|Any CPU
 		{4A4402D4-E910-443B-B8FC-2C18286A2CA0}.Release|x86.ActiveCfg = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|Win32.ActiveCfg = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|Any CPU.Build.0 = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|Win32.ActiveCfg = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|x64.ActiveCfg = Release|Any CPU
+		{23499896-B135-4527-8574-C26E926EA99E}.Release|x86.ActiveCfg = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|Win32.ActiveCfg = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|Win32.ActiveCfg = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|x64.ActiveCfg = Release|Any CPU
+		{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}.Release|x86.ActiveCfg = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE