Pārlūkot izejas kodu

add new chapter provider feature

Luke Pulverenti 11 gadi atpakaļ
vecāks
revīzija
945e843270
29 mainītis faili ar 534 papildinājumiem un 74 dzēšanām
  1. 28 0
      MediaBrowser.Api/Library/ChapterService.cs
  2. 1 0
      MediaBrowser.Api/MediaBrowser.Api.csproj
  3. 2 2
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  4. 1 1
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  5. 5 1
      MediaBrowser.Common.Implementations/Security/UsageReporter.cs
  6. 2 0
      MediaBrowser.Controller/Chapters/ChapterSearchRequest.cs
  7. 57 0
      MediaBrowser.Controller/Chapters/IChapterManager.cs
  8. 1 0
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  9. 8 1
      MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs
  10. 6 0
      MediaBrowser.Model/Chapters/RemoteChapterInfo.cs
  11. 6 0
      MediaBrowser.Model/Chapters/RemoteChapterResult.cs
  12. 27 7
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  13. 4 0
      MediaBrowser.Model/LiveTv/ChannelInfoDto.cs
  14. 240 0
      MediaBrowser.Providers/Chapters/ChapterManager.cs
  15. 4 4
      MediaBrowser.Providers/Manager/MetadataService.cs
  16. 1 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  17. 27 23
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  18. 80 24
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  19. 1 1
      MediaBrowser.Providers/Photos/PhotoProvider.cs
  20. 3 0
      MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
  21. 3 1
      MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json
  22. 4 0
      MediaBrowser.Server.Implementations/Localization/Server/server.json
  23. 3 3
      MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs
  24. 8 1
      MediaBrowser.ServerApplication/ApplicationHost.cs
  25. 1 0
      MediaBrowser.WebDashboard/Api/DashboardService.cs
  26. 6 0
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  27. 2 2
      Nuget/MediaBrowser.Common.Internal.nuspec
  28. 1 1
      Nuget/MediaBrowser.Common.nuspec
  29. 2 2
      Nuget/MediaBrowser.Server.Core.nuspec

+ 28 - 0
MediaBrowser.Api/Library/ChapterService.cs

@@ -0,0 +1,28 @@
+using MediaBrowser.Controller.Chapters;
+using ServiceStack;
+using System.Linq;
+
+namespace MediaBrowser.Api.Library
+{
+    [Route("/Providers/Chapters", "GET")]
+    public class GetChapterProviders : IReturnVoid
+    {
+    }
+
+    public class ChapterService : BaseApiService
+    {
+        private readonly IChapterManager _chapterManager;
+
+        public ChapterService(IChapterManager chapterManager)
+        {
+            _chapterManager = chapterManager;
+        }
+
+        public object Get(GetChapterProviders request)
+        {
+            var result = _chapterManager.GetProviders().ToList();
+
+            return ToOptimizedResult(result);
+        }
+    }
+}

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

@@ -68,6 +68,7 @@
     <Compile Include="ChannelService.cs" />
     <Compile Include="Dlna\DlnaServerService.cs" />
     <Compile Include="Dlna\DlnaService.cs" />
+    <Compile Include="Library\ChapterService.cs" />
     <Compile Include="Library\SubtitleService.cs" />
     <Compile Include="Movies\CollectionService.cs" />
     <Compile Include="Music\AlbumsService.cs" />

+ 2 - 2
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -363,11 +363,11 @@ namespace MediaBrowser.Api.Playback
                 switch (qualitySetting)
                 {
                     case EncodingQuality.HighSpeed:
-                        crf = "16";
+                        crf = "12";
                         profileScore = 2;
                         break;
                     case EncodingQuality.HighQuality:
-                        crf = "10";
+                        crf = "8";
                         profileScore = 1;
                         break;
                     case EncodingQuality.MaxQuality:

+ 1 - 1
MediaBrowser.Api/UserLibrary/UserLibraryService.cs

@@ -561,7 +561,7 @@ namespace MediaBrowser.Api.UserLibrary
                 return dtos.ToList();
             }
 
-            throw new ArgumentException("The item does not support special features");
+            return new List<BaseItemDto>();
         }
 
         /// <summary>

+ 5 - 1
MediaBrowser.Common.Implementations/Security/UsageReporter.cs

@@ -1,6 +1,7 @@
 using MediaBrowser.Common.Net;
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -25,13 +26,16 @@ namespace MediaBrowser.Common.Implementations.Security
 
             var mac = _networkManager.GetMacAddress();
 
+            var plugins = string.Join("|", _applicationHost.Plugins.Select(i => i.Name).ToArray());
+
             var data = new Dictionary<string, string>
             {
                 { "feature", _applicationHost.Name }, 
                 { "mac", mac }, 
                 { "ver", _applicationHost.ApplicationVersion.ToString() }, 
                 { "platform", Environment.OSVersion.VersionString }, 
-                { "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}
+                { "isservice", _applicationHost.IsRunningAsService.ToString().ToLower()}, 
+                { "plugins", plugins}
             };
 
             return _httpClient.Post(Constants.Constants.MbAdminUrl + "service/registration/ping", data, cancellationToken);

+ 2 - 0
MediaBrowser.Controller/Chapters/ChapterSearchRequest.cs

@@ -21,6 +21,8 @@ namespace MediaBrowser.Controller.Chapters
         public long? RuntimeTicks { get; set; }
         public Dictionary<string, string> ProviderIds { get; set; }
 
+        public bool SearchAllProviders { get; set; }
+
         public ChapterSearchRequest()
         {
             ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

+ 57 - 0
MediaBrowser.Controller/Chapters/IChapterManager.cs

@@ -0,0 +1,57 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Model.Chapters;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Controller.Chapters
+{
+    /// <summary>
+    /// Interface IChapterManager
+    /// </summary>
+    public interface IChapterManager
+    {
+        /// <summary>
+        /// Adds the parts.
+        /// </summary>
+        /// <param name="chapterProviders">The chapter providers.</param>
+        void AddParts(IEnumerable<IChapterProvider> chapterProviders);
+
+        /// <summary>
+        /// Searches the specified video.
+        /// </summary>
+        /// <param name="video">The video.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
+        Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Searches the specified request.
+        /// </summary>
+        /// <param name="request">The request.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{IEnumerable{RemoteChapterResult}}.</returns>
+        Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the chapters.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{ChapterResponse}.</returns>
+        Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken);
+
+        /// <summary>
+        /// Gets the providers.
+        /// </summary>
+        /// <param name="itemId">The item identifier.</param>
+        /// <returns>IEnumerable{ChapterProviderInfo}.</returns>
+        IEnumerable<ChapterProviderInfo> GetProviders(string itemId);
+
+        /// <summary>
+        /// Gets the providers.
+        /// </summary>
+        /// <returns>IEnumerable{ChapterProviderInfo}.</returns>
+        IEnumerable<ChapterProviderInfo> GetProviders();
+    }
+}

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

@@ -91,6 +91,7 @@
     <Compile Include="Channels\IRequiresMediaInfoCallback.cs" />
     <Compile Include="Channels\ISearchableChannel.cs" />
     <Compile Include="Chapters\ChapterSearchRequest.cs" />
+    <Compile Include="Chapters\IChapterManager.cs" />
     <Compile Include="Chapters\IChapterProvider.cs" />
     <Compile Include="Chapters\ChapterResponse.cs" />
     <Compile Include="Collections\CollectionCreationOptions.cs" />

+ 8 - 1
MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs

@@ -12,7 +12,14 @@ namespace MediaBrowser.Controller.Providers
     public interface ICustomMetadataProvider<TItemType> : IMetadataProvider<TItemType>, ICustomMetadataProvider
         where TItemType : IHasMetadata
     {
-        Task<ItemUpdateType> FetchAsync(TItemType item, IDirectoryService directoryService, CancellationToken cancellationToken);
+        /// <summary>
+        /// Fetches the asynchronous.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="options">The options.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task{ItemUpdateType}.</returns>
+        Task<ItemUpdateType> FetchAsync(TItemType item, MetadataRefreshOptions options, CancellationToken cancellationToken);
     }
 
     public interface IPreRefreshProvider : ICustomMetadataProvider

+ 6 - 0
MediaBrowser.Model/Chapters/RemoteChapterInfo.cs

@@ -15,4 +15,10 @@ namespace MediaBrowser.Model.Chapters
         /// <value>The name.</value>
         public string Name { get; set; }
     }
+
+    public class ChapterProviderInfo
+    {
+        public string Name { get; set; }
+        public string Id { get; set; }
+    }
 }

+ 6 - 0
MediaBrowser.Model/Chapters/RemoteChapterResult.cs

@@ -21,6 +21,12 @@ namespace MediaBrowser.Model.Chapters
         /// <value>The name.</value>
         public string Name { get; set; }
 
+        /// <summary>
+        /// Gets or sets the name of the provider.
+        /// </summary>
+        /// <value>The name of the provider.</value>
+        public string ProviderName { get; set; }
+        
         /// <summary>
         /// Gets or sets the community rating.
         /// </summary>

+ 27 - 7
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -193,10 +193,6 @@ namespace MediaBrowser.Model.Configuration
 
         public bool AllowVideoUpscaling { get; set; }
 
-        public bool EnableMovieChapterImageExtraction { get; set; }
-        public bool EnableEpisodeChapterImageExtraction { get; set; }
-        public bool EnableOtherVideoChapterImageExtraction { get; set; }
-
         public MetadataOptions[] MetadataOptions { get; set; }
 
         public bool EnableDebugEncodingLogging { get; set; }
@@ -227,6 +223,7 @@ namespace MediaBrowser.Model.Configuration
         public string[] ManualLoginClients { get; set; }
 
         public ChannelOptions ChannelOptions { get; set; }
+        public ChapterOptions ChapterOptions { get; set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
@@ -241,9 +238,6 @@ namespace MediaBrowser.Model.Configuration
             EnableHttpLevelLogging = true;
             EnableDashboardResponseCaching = true;
 
-            EnableMovieChapterImageExtraction = true;
-            EnableEpisodeChapterImageExtraction = false;
-            EnableOtherVideoChapterImageExtraction = false;
             EnableAutomaticRestart = true;
             EnablePeoplePrefixSubFolders = true;
 
@@ -297,6 +291,7 @@ namespace MediaBrowser.Model.Configuration
             SubtitleOptions = new SubtitleOptions();
 
             ChannelOptions = new ChannelOptions();
+            ChapterOptions = new ChapterOptions();
         }
     }
 
@@ -315,4 +310,29 @@ namespace MediaBrowser.Model.Configuration
             MaxDownloadAge = 30;
         }
     }
+
+    public class ChapterOptions
+    {
+        public bool EnableMovieChapterImageExtraction { get; set; }
+        public bool EnableEpisodeChapterImageExtraction { get; set; }
+        public bool EnableOtherVideoChapterImageExtraction { get; set; }
+
+        public bool DownloadMovieChapters { get; set; }
+        public bool DownloadEpisodeChapters { get; set; }
+
+        public string[] FetcherOrder { get; set; }
+        public string[] DisabledFetchers { get; set; }
+
+        public ChapterOptions()
+        {
+            EnableMovieChapterImageExtraction = true;
+            EnableEpisodeChapterImageExtraction = false;
+            EnableOtherVideoChapterImageExtraction = false;
+
+            DownloadMovieChapters = true;
+
+            DisabledFetchers = new string[] { };
+            FetcherOrder = new string[] { };
+        }
+    }
 }

+ 4 - 0
MediaBrowser.Model/LiveTv/ChannelInfoDto.cs

@@ -32,6 +32,10 @@ namespace MediaBrowser.Model.LiveTv
         /// <value>The external identifier.</value>
         public string ExternalId { get; set; }
 
+        /// <summary>
+        /// Gets or sets the media sources.
+        /// </summary>
+        /// <value>The media sources.</value>
         public List<MediaSourceInfo> MediaSources { get; set; }
         
         /// <summary>

+ 240 - 0
MediaBrowser.Providers/Chapters/ChapterManager.cs

@@ -0,0 +1,240 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Chapters;
+using MediaBrowser.Model.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Providers.Chapters
+{
+    public class ChapterManager : IChapterManager
+    {
+        private IChapterProvider[] _providers;
+        private readonly ILibraryManager _libraryManager;
+        private readonly ILogger _logger;
+        private readonly IServerConfigurationManager _config;
+
+        public ChapterManager(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config)
+        {
+            _libraryManager = libraryManager;
+            _logger = logger;
+            _config = config;
+        }
+
+        public void AddParts(IEnumerable<IChapterProvider> chapterProviders)
+        {
+            _providers = chapterProviders.ToArray();
+        }
+
+        public Task<IEnumerable<RemoteChapterResult>> Search(Video video, CancellationToken cancellationToken)
+        {
+            VideoContentType mediaType;
+
+            if (video is Episode)
+            {
+                mediaType = VideoContentType.Episode;
+            }
+            else if (video is Movie)
+            {
+                mediaType = VideoContentType.Movie;
+            }
+            else
+            {
+                // These are the only supported types
+                return Task.FromResult<IEnumerable<RemoteChapterResult>>(new List<RemoteChapterResult>());
+            }
+
+            var request = new ChapterSearchRequest
+            {
+                ContentType = mediaType,
+                IndexNumber = video.IndexNumber,
+                //Language = language,
+                MediaPath = video.Path,
+                Name = video.Name,
+                ParentIndexNumber = video.ParentIndexNumber,
+                ProductionYear = video.ProductionYear,
+                ProviderIds = video.ProviderIds,
+                RuntimeTicks = video.RunTimeTicks
+            };
+
+            var episode = video as Episode;
+
+            if (episode != null)
+            {
+                request.IndexNumberEnd = episode.IndexNumberEnd;
+                request.SeriesName = episode.SeriesName;
+            }
+
+            return Search(request, cancellationToken);
+        }
+
+        public async Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request, CancellationToken cancellationToken)
+        {
+            var contentType = request.ContentType;
+            var providers = GetInternalProviders(false)
+                .Where(i => i.SupportedMediaTypes.Contains(contentType))
+                .ToList();
+
+            // If not searching all, search one at a time until something is found
+            if (!request.SearchAllProviders)
+            {
+                foreach (var provider in providers)
+                {
+                    try
+                    {
+                        return await Search(request, provider, cancellationToken).ConfigureAwait(false);
+
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.ErrorException("Error downloading subtitles from {0}", ex, provider.Name);
+                    }
+                }
+                return new List<RemoteChapterResult>();
+            }
+
+            var tasks = providers.Select(async i =>
+            {
+                try
+                {
+                    return await Search(request, i, cancellationToken).ConfigureAwait(false);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error downloading subtitles from {0}", ex, i.Name);
+                    return new List<RemoteChapterResult>();
+                }
+            });
+
+            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+            return results.SelectMany(i => i);
+        }
+
+        private async Task<IEnumerable<RemoteChapterResult>> Search(ChapterSearchRequest request,
+            IChapterProvider provider,
+            CancellationToken cancellationToken)
+        {
+            var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
+
+            foreach (var result in searchResults)
+            {
+                result.Id = GetProviderId(provider.Name) + "_" + result.Id;
+                result.ProviderName = provider.Name;
+            }
+
+            return searchResults;
+        }
+
+        public Task<ChapterResponse> GetChapters(string id, CancellationToken cancellationToken)
+        {
+            var parts = id.Split(new[] { '_' }, 2);
+
+            var provider = GetProvider(parts.First());
+            id = parts.Last();
+
+            return provider.GetChapters(id, cancellationToken);
+        }
+
+        public IEnumerable<ChapterProviderInfo> GetProviders(string itemId)
+        {
+            var video = _libraryManager.GetItemById(itemId) as Video;
+            VideoContentType mediaType;
+
+            if (video is Episode)
+            {
+                mediaType = VideoContentType.Episode;
+            }
+            else if (video is Movie)
+            {
+                mediaType = VideoContentType.Movie;
+            }
+            else
+            {
+                // These are the only supported types
+                return new List<ChapterProviderInfo>();
+            }
+
+            var providers = GetInternalProviders(false)
+                .Where(i => i.SupportedMediaTypes.Contains(mediaType));
+
+            return GetInfos(providers);
+        }
+
+        public IEnumerable<ChapterProviderInfo> GetProviders()
+        {
+            return GetInfos(GetInternalProviders(true));
+        }
+
+        private IEnumerable<IChapterProvider> GetInternalProviders(bool includeDisabledProviders)
+        {
+            var providers = _providers;
+
+            if (!includeDisabledProviders)
+            {
+                providers = providers
+                    .Where(i => _config.Configuration.ChapterOptions.DisabledFetchers.Contains(i.Name))
+                    .ToArray();
+            }
+
+            return providers
+                .OrderBy(GetConfiguredOrder)
+                .ThenBy(GetDefaultOrder)
+                .ToArray();
+        }
+
+        private IEnumerable<ChapterProviderInfo> GetInfos(IEnumerable<IChapterProvider> providers)
+        {
+            return providers.Select(i => new ChapterProviderInfo
+            {
+                Name = i.Name,
+                Id = GetProviderId(i.Name)
+            });
+        }
+
+        private string GetProviderId(string name)
+        {
+            return name.ToLower().GetMD5().ToString("N");
+        }
+
+        private IChapterProvider GetProvider(string id)
+        {
+            return _providers.First(i => string.Equals(id, GetProviderId(i.Name)));
+        }
+
+        private int GetConfiguredOrder(IChapterProvider provider)
+        {
+            // See if there's a user-defined order
+            var index = Array.IndexOf(_config.Configuration.ChapterOptions.FetcherOrder, provider.Name);
+
+            if (index != -1)
+            {
+                return index;
+            }
+
+            // Not configured. Just return some high number to put it at the end.
+            return 100;
+        }
+
+        private int GetDefaultOrder(IChapterProvider provider)
+        {
+            var hasOrder = provider as IHasOrder;
+
+            if (hasOrder != null)
+            {
+                return hasOrder.Order;
+            }
+
+            return 0;
+        }
+    }
+}

+ 4 - 4
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -271,7 +271,7 @@ namespace MediaBrowser.Providers.Manager
 
             foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
             {
-                await RunCustomProvider(provider, item, options.DirectoryService, refreshResult, cancellationToken).ConfigureAwait(false);
+                await RunCustomProvider(provider, item, options, refreshResult, cancellationToken).ConfigureAwait(false);
             }
 
             var temp = CreateNew();
@@ -343,19 +343,19 @@ namespace MediaBrowser.Providers.Manager
 
             foreach (var provider in customProviders.Where(i => !(i is IPreRefreshProvider)))
             {
-                await RunCustomProvider(provider, item, options.DirectoryService, refreshResult, cancellationToken).ConfigureAwait(false);
+                await RunCustomProvider(provider, item, options, refreshResult, cancellationToken).ConfigureAwait(false);
             }
 
             return refreshResult;
         }
 
-        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, IDirectoryService directoryService, RefreshResult refreshResult, CancellationToken cancellationToken)
+        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
         {
             Logger.Debug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
 
             try
             {
-                refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, directoryService, cancellationToken).ConfigureAwait(false);
+                refreshResult.UpdateType = refreshResult.UpdateType | await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(false);
             }
             catch (OperationCanceledException)
             {

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

@@ -79,6 +79,7 @@
     <Compile Include="BoxSets\MovieDbBoxSetImageProvider.cs" />
     <Compile Include="BoxSets\MovieDbBoxSetProvider.cs" />
     <Compile Include="Channels\ChannelMetadataService.cs" />
+    <Compile Include="Chapters\ChapterManager.cs" />
     <Compile Include="Folders\CollectionFolderImageProvider.cs" />
     <Compile Include="Folders\FolderMetadataService.cs" />
     <Compile Include="Folders\ImagesByNameImageProvider.cs" />

+ 27 - 23
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -1,5 +1,6 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -49,58 +50,59 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _config;
         private readonly ISubtitleManager _subtitleManager;
+        private readonly IChapterManager _chapterManager;
 
         public string Name
         {
             get { return "ffprobe"; }
         }
 
-        public Task<ItemUpdateType> FetchAsync(Episode item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(MusicVideo item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(Movie item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(AdultVideo item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(AdultVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(LiveTvVideoRecording item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(LiveTvVideoRecording item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(Trailer item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(Video item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
-            return FetchVideoInfo(item, directoryService, cancellationToken);
+            return FetchVideoInfo(item, options, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(Audio item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Audio item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchAudioInfo(item, cancellationToken);
         }
 
-        public Task<ItemUpdateType> FetchAsync(LiveTvAudioRecording item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(LiveTvAudioRecording item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             return FetchAudioInfo(item, cancellationToken);
         }
 
-        public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
+        public FFProbeProvider(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager)
         {
             _logger = logger;
             _isoManager = isoManager;
@@ -114,10 +116,11 @@ namespace MediaBrowser.Providers.MediaInfo
             _fileSystem = fileSystem;
             _config = config;
             _subtitleManager = subtitleManager;
+            _chapterManager = chapterManager;
         }
 
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
-        public Task<ItemUpdateType> FetchVideoInfo<T>(T item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchVideoInfo<T>(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
             where T : Video
         {
             if (item.LocationType != LocationType.FileSystem)
@@ -140,9 +143,9 @@ namespace MediaBrowser.Providers.MediaInfo
                 return _cachedTask;
             }
 
-            var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
+            var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager, _chapterManager);
 
-            return prober.ProbeVideo(item, directoryService, true, cancellationToken);
+            return prober.ProbeVideo(item, options, cancellationToken);
         }
 
         public Task<ItemUpdateType> FetchAudioInfo<T>(T item, CancellationToken cancellationToken)
@@ -171,9 +174,10 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 if (video != null && !video.IsPlaceHolder)
                 {
-                    var prober = new FFProbeVideoInfo(_logger, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager);
-
-                    return !video.SubtitleFiles.SequenceEqual(SubtitleResolver.GetSubtitleFiles(video, directoryService, false).Select(i => i.FullName).OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
+                    return !video.SubtitleFiles
+                        .SequenceEqual(SubtitleResolver.GetSubtitleFiles(video, directoryService, false)
+                        .Select(i => i.FullName)
+                        .OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
                 }
             }
 

+ 80 - 24
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -2,6 +2,7 @@
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.IO;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
@@ -41,10 +42,11 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IFileSystem _fileSystem;
         private readonly IServerConfigurationManager _config;
         private readonly ISubtitleManager _subtitleManager;
+        private readonly IChapterManager _chapterManager;
 
         private readonly CultureInfo _usCulture = new CultureInfo("en-US");
 
-        public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager)
+        public FFProbeVideoInfo(ILogger logger, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager)
         {
             _logger = logger;
             _isoManager = isoManager;
@@ -58,9 +60,12 @@ namespace MediaBrowser.Providers.MediaInfo
             _fileSystem = fileSystem;
             _config = config;
             _subtitleManager = subtitleManager;
+            _chapterManager = chapterManager;
         }
 
-        public async Task<ItemUpdateType> ProbeVideo<T>(T item, IDirectoryService directoryService, bool enableSubtitleDownloading, CancellationToken cancellationToken)
+        public async Task<ItemUpdateType> ProbeVideo<T>(T item,
+            MetadataRefreshOptions options,
+            CancellationToken cancellationToken)
             where T : Video
         {
             var isoMount = await MountIsoIfNeeded(item, cancellationToken).ConfigureAwait(false);
@@ -105,7 +110,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
                 cancellationToken.ThrowIfCancellationRequested();
 
-                await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, directoryService, enableSubtitleDownloading).ConfigureAwait(false);
+                await Fetch(item, cancellationToken, result, isoMount, blurayDiscInfo, options).ConfigureAwait(false);
 
             }
             finally
@@ -121,7 +126,9 @@ namespace MediaBrowser.Providers.MediaInfo
 
         private const string SchemaVersion = "1";
 
-        private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken)
+        private async Task<InternalMediaInfoResult> GetMediaInfo(BaseItem item,
+            IIsoMount isoMount,
+            CancellationToken cancellationToken)
         {
             cancellationToken.ThrowIfCancellationRequested();
 
@@ -160,7 +167,12 @@ namespace MediaBrowser.Providers.MediaInfo
             return result;
         }
 
-        protected async Task Fetch(Video video, CancellationToken cancellationToken, InternalMediaInfoResult data, IIsoMount isoMount, BlurayDiscInfo blurayInfo, IDirectoryService directoryService, bool enableSubtitleDownloading)
+        protected async Task Fetch(Video video,
+            CancellationToken cancellationToken,
+            InternalMediaInfoResult data,
+            IIsoMount isoMount,
+            BlurayDiscInfo blurayInfo,
+            MetadataRefreshOptions options)
         {
             var mediaInfo = MediaEncoderHelpers.GetMediaInfo(data);
             var mediaStreams = mediaInfo.MediaStreams;
@@ -208,17 +220,12 @@ namespace MediaBrowser.Providers.MediaInfo
                 FetchBdInfo(video, chapters, mediaStreams, blurayInfo);
             }
 
-            await AddExternalSubtitles(video, mediaStreams, directoryService, enableSubtitleDownloading, cancellationToken).ConfigureAwait(false);
+            await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
 
             FetchWtvInfo(video, data);
 
             video.IsHD = mediaStreams.Any(i => i.Type == MediaStreamType.Video && i.Width.HasValue && i.Width.Value >= 1270);
 
-            if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
-            {
-                AddDummyChapters(video, chapters);
-            }
-
             var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
 
             video.VideoBitRate = videoStream == null ? null : videoStream.BitRate;
@@ -228,18 +235,34 @@ namespace MediaBrowser.Providers.MediaInfo
 
             ExtractTimestamp(video);
 
-            await _encodingManager.RefreshChapterImages(new ChapterImageRefreshOptions
+            await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false);
+
+            if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
+                options.MetadataRefreshMode == MetadataRefreshMode.EnsureMetadata)
             {
-                Chapters = chapters,
-                Video = video,
-                ExtractImages = false,
-                SaveChapters = false
+                var remoteChapters = await DownloadChapters(video, cancellationToken).ConfigureAwait(false);
 
-            }, cancellationToken).ConfigureAwait(false);
+                if (remoteChapters.Count > 0)
+                {
+                    chapters = remoteChapters;
+                }
 
-            await _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken).ConfigureAwait(false);
+                if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video))
+                {
+                    AddDummyChapters(video, chapters);
+                }
+
+                await _encodingManager.RefreshChapterImages(new ChapterImageRefreshOptions
+                {
+                    Chapters = chapters,
+                    Video = video,
+                    ExtractImages = false,
+                    SaveChapters = false
 
-            await _itemRepo.SaveChapters(video.Id, chapters, cancellationToken).ConfigureAwait(false);
+                }, cancellationToken).ConfigureAwait(false);
+
+                await _itemRepo.SaveChapters(video.Id, chapters, cancellationToken).ConfigureAwait(false);
+            }
         }
 
         private ChapterInfo GetChapterInfo(MediaChapter chapter)
@@ -416,15 +439,20 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// <param name="video">The video.</param>
         /// <param name="currentStreams">The current streams.</param>
-        /// <param name="directoryService">The directory service.</param>
-        /// <param name="enableSubtitleDownloading">if set to <c>true</c> [enable subtitle downloading].</param>
+        /// <param name="options">The options.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        private async Task AddExternalSubtitles(Video video, List<MediaStream> currentStreams, IDirectoryService directoryService, bool enableSubtitleDownloading, CancellationToken cancellationToken)
+        private async Task AddExternalSubtitles(Video video,
+            List<MediaStream> currentStreams,
+            MetadataRefreshOptions options,
+            CancellationToken cancellationToken)
         {
             var subtitleResolver = new SubtitleResolver(_localization);
 
-            var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, currentStreams.Count, directoryService, false).ToList();
+            var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, currentStreams.Count, options.DirectoryService, false).ToList();
+
+            var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.EnsureMetadata ||
+                                            options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
 
             if (enableSubtitleDownloading && (_config.Configuration.SubtitleOptions.DownloadEpisodeSubtitles &&
                 video is Episode) ||
@@ -444,7 +472,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 // Rescan
                 if (downloadedLanguages.Count > 0)
                 {
-                    externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, currentStreams.Count, directoryService, true).ToList();
+                    externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, currentStreams.Count, options.DirectoryService, true).ToList();
                 }
             }
 
@@ -453,6 +481,33 @@ namespace MediaBrowser.Providers.MediaInfo
             currentStreams.AddRange(externalSubtitleStreams);
         }
 
+        private async Task<List<ChapterInfo>> DownloadChapters(Video video, CancellationToken cancellationToken)
+        {
+            if ((_config.Configuration.ChapterOptions.DownloadEpisodeChapters &&
+                 video is Episode) ||
+                (_config.Configuration.ChapterOptions.DownloadMovieChapters &&
+                 video is Movie))
+            {
+                var results = await _chapterManager.Search(video, cancellationToken).ConfigureAwait(false);
+
+                var result = results.FirstOrDefault();
+
+                if (result != null)
+                {
+                    var chapters = await _chapterManager.GetChapters(result.Id, cancellationToken).ConfigureAwait(false);
+
+                    return chapters.Chapters.Select(i => new ChapterInfo
+                    {
+                        Name = i.Name,
+                        StartPositionTicks = i.StartPositionTicks
+
+                    }).ToList();
+                }
+            }
+
+            return new List<ChapterInfo>();
+        }
+
         /// <summary>
         /// The dummy chapter duration
         /// </summary>
@@ -499,6 +554,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// </summary>
         /// <param name="item">The item.</param>
         /// <param name="mount">The mount.</param>
+        /// <param name="blurayDiscInfo">The bluray disc information.</param>
         private void OnPreFetch(Video item, IIsoMount mount, BlurayDiscInfo blurayDiscInfo)
         {
             if (item.VideoType == VideoType.Iso)

+ 1 - 1
MediaBrowser.Providers/Photos/PhotoProvider.cs

@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Photos
             _imageProcessor = imageProcessor;
         }
 
-        public Task<ItemUpdateType> FetchAsync(Photo item, IDirectoryService directoryService, CancellationToken cancellationToken)
+        public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
         {
             item.SetImagePath(ImageType.Primary, item.Path);
             item.SetImagePath(ImageType.Backdrop, item.Path);

+ 3 - 0
MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -410,6 +410,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
                 {
                     stream.PacketLength = null;
                 }
+
+                // Don't trust the provider values
+                stream.Index = -1;
             }
         }
 

+ 3 - 1
MediaBrowser.Server.Implementations/Localization/JavaScript/javascript.json

@@ -143,5 +143,7 @@
 	"HeaderSelectChannelDownloadPathHelp": "Browse or enter the path to use for storing channel cache files. The folder must be writeable.",
 	"OptionNewCollection": "New...",
 	"ButtonAdd": "Add",
-	"ButtonRemove": "Remove"
+	"ButtonRemove": "Remove",
+	"LabelChapterDownloaders": "Chapter downloaders:",
+	"LabelChapterDownloadersHelp": "Enable and rank your preferred chapter downloaders in order of priority. Lower priority downloaders will only be used to fill in missing information."
 }

+ 4 - 0
MediaBrowser.Server.Implementations/Localization/Server/server.json

@@ -733,11 +733,15 @@
 	"OptionReportByteRangeSeekingWhenTranscodingHelp": "This is required for some devices that don't time seek very well.",
 	"HeaderSubtitleDownloadingHelp": "When Media Browser scans your video files it can search for missing subtitles, and download them using a subtitle provider such as OpenSubtitles.org.",
 	"HeaderDownloadSubtitlesFor": "Download subtitles for:",
+	"MessageNoChapterProviders": "Install a chapter provider plugin such as ChapterDb or tagChimp to enable additional chapter metadata options.",
 	"LabelSkipIfGraphicalSubsPresent": "Skip if the video already contains graphical subtitles",
 	"LabelSkipIfGraphicalSubsPresentHelp": "Keeping text versions of subtitles will result in more efficient delivery to mobile clients.",
 	"TabSubtitles": "Subtitles",
+	"TabChapters": "Chapters",
+	"HeaderDownloadChaptersFor": "Download chapter names for:",
 	"LabelOpenSubtitlesUsername": "Open Subtitles username:",
 	"LabelOpenSubtitlesPassword": "Open Subtitles password:",
+	"HeaderChapterDownloadingHelp": "When Media Browser scans your video files it can download friendly chapter names from the internet using chapter plugins such as ChapterDb and tagChimp.",
 	"LabelPlayDefaultAudioTrack": "Play default audio track regardless of language",
 	"LabelSubtitlePlaybackMode": "Subtitle mode:",
 	"LabelDownloadLanguages": "Download languages:",

+ 3 - 3
MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -91,21 +91,21 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
 
             if (video is Movie)
             {
-                if (!_config.Configuration.EnableMovieChapterImageExtraction)
+                if (!_config.Configuration.ChapterOptions.EnableMovieChapterImageExtraction)
                 {
                     return false;
                 }
             }
             else if (video is Episode)
             {
-                if (!_config.Configuration.EnableEpisodeChapterImageExtraction)
+                if (!_config.Configuration.ChapterOptions.EnableEpisodeChapterImageExtraction)
                 {
                     return false;
                 }
             }
             else
             {
-                if (!_config.Configuration.EnableOtherVideoChapterImageExtraction)
+                if (!_config.Configuration.ChapterOptions.EnableOtherVideoChapterImageExtraction)
                 {
                     return false;
                 }

+ 8 - 1
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
@@ -43,6 +44,7 @@ using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Updates;
+using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.Server.Implementations;
@@ -196,6 +198,7 @@ namespace MediaBrowser.ServerApplication
 
         private INotificationManager NotificationManager { get; set; }
         private ISubtitleManager SubtitleManager { get; set; }
+        private IChapterManager ChapterManager { get; set; }
 
         private IUserViewManager UserViewManager { get; set; }
 
@@ -544,6 +547,9 @@ namespace MediaBrowser.ServerApplication
             SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, ItemRepository);
             RegisterSingleInstance(SubtitleManager);
 
+            ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager);
+            RegisterSingleInstance(ChapterManager);
+
             var displayPreferencesTask = Task.Run(async () => await ConfigureDisplayPreferencesRepositories().ConfigureAwait(false));
             var itemsTask = Task.Run(async () => await ConfigureItemRepositories().ConfigureAwait(false));
             var userdataTask = Task.Run(async () => await ConfigureUserDataRepositories().ConfigureAwait(false));
@@ -725,7 +731,8 @@ namespace MediaBrowser.ServerApplication
             LiveTvManager.AddParts(GetExports<ILiveTvService>());
 
             SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
-            
+            ChapterManager.AddParts(GetExports<IChapterProvider>());
+       
             SessionManager.AddParts(GetExports<ISessionControllerFactory>());
 
             ChannelManager.AddParts(GetExports<IChannel>(), GetExports<IChannelFactory>());

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

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

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

@@ -331,6 +331,9 @@
     <Content Include="dashboard-ui\librarypathmapping.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\metadatachapters.html">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\mypreferencesdisplay.html">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -670,6 +673,9 @@
     <Content Include="dashboard-ui\scripts\localsettings.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="dashboard-ui\scripts\metadatachapters.js">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="dashboard-ui\scripts\mypreferencesdisplay.js">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>

+ 2 - 2
Nuget/MediaBrowser.Common.Internal.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common.Internal</id>
-        <version>3.0.399</version>
+        <version>3.0.400</version>
         <title>MediaBrowser.Common.Internal</title>
         <authors>Luke</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains common components shared by Media Browser Theater and Media Browser Server. Not intended for plugin developer consumption.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.399" />
+            <dependency id="MediaBrowser.Common" version="3.0.400" />
             <dependency id="NLog" version="2.1.0" />
             <dependency id="SimpleInjector" version="2.5.0" />
             <dependency id="sharpcompress" version="0.10.2" />

+ 1 - 1
Nuget/MediaBrowser.Common.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Common</id>
-        <version>3.0.399</version>
+        <version>3.0.400</version>
         <title>MediaBrowser.Common</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>

+ 2 - 2
Nuget/MediaBrowser.Server.Core.nuspec

@@ -2,7 +2,7 @@
 <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
     <metadata>
         <id>MediaBrowser.Server.Core</id>
-        <version>3.0.399</version>
+        <version>3.0.400</version>
         <title>Media Browser.Server.Core</title>
         <authors>Media Browser Team</authors>
         <owners>ebr,Luke,scottisafool</owners>
@@ -12,7 +12,7 @@
         <description>Contains core components required to build plugins for Media Browser Server.</description>
         <copyright>Copyright © Media Browser 2013</copyright>
         <dependencies>
-            <dependency id="MediaBrowser.Common" version="3.0.399" />
+            <dependency id="MediaBrowser.Common" version="3.0.400" />
         </dependencies>
     </metadata>
     <files>