diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs index b3191cd4b9..39fcc50d8f 100644 --- a/MediaBrowser.Api/ConfigurationService.cs +++ b/MediaBrowser.Api/ConfigurationService.cs @@ -8,8 +8,11 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; using ServiceStack; +using ServiceStack.Text.Controller; +using ServiceStack.Web; using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace MediaBrowser.Api @@ -23,6 +26,13 @@ namespace MediaBrowser.Api } + [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")] + public class GetNamedConfiguration + { + [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Key { get; set; } + } + /// /// Class UpdateConfiguration /// @@ -31,6 +41,15 @@ namespace MediaBrowser.Api { } + [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")] + public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream + { + [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] + public string Key { get; set; } + + public Stream RequestStream { get; set; } + } + [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")] public class GetDefaultMetadataOptions : IReturn { @@ -88,6 +107,13 @@ namespace MediaBrowser.Api return ToOptimizedResultUsingCache(cacheKey, dateModified, null, () => _configurationManager.Configuration); } + public object Get(GetNamedConfiguration request) + { + var result = _configurationManager.GetConfiguration(request.Key); + + return ToOptimizedResult(result); + } + /// /// Posts the specified configuraiton. /// @@ -95,7 +121,6 @@ namespace MediaBrowser.Api public void Post(UpdateConfiguration request) { // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration - var json = _jsonSerializer.SerializeToString(request); var config = _jsonSerializer.DeserializeFromString(json); @@ -103,6 +128,17 @@ namespace MediaBrowser.Api _configurationManager.ReplaceConfiguration(config); } + public void Post(UpdateNamedConfiguration request) + { + var pathInfo = PathInfo.Parse(Request.PathInfo); + var key = pathInfo.GetArgumentValue(2); + + var configurationType = _configurationManager.GetConfigurationType(key); + var configuration = _jsonSerializer.DeserializeFromStream(request.RequestStream, configurationType); + + _configurationManager.SaveConfiguration(key, configuration); + } + public object Get(GetDefaultMetadataOptions request) { return ToOptimizedSerializedResultUsingCache(new MetadataOptions()); diff --git a/MediaBrowser.Api/Dlna/DlnaServerService.cs b/MediaBrowser.Api/Dlna/DlnaServerService.cs index 28de8ee174..82bd394f0c 100644 --- a/MediaBrowser.Api/Dlna/DlnaServerService.cs +++ b/MediaBrowser.Api/Dlna/DlnaServerService.cs @@ -1,4 +1,6 @@ -using MediaBrowser.Controller.Dlna; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Model.Configuration; using ServiceStack; using ServiceStack.Text.Controller; using ServiceStack.Web; @@ -76,11 +78,14 @@ namespace MediaBrowser.Api.Dlna private readonly IContentDirectory _contentDirectory; private readonly IConnectionManager _connectionManager; - public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager) + private readonly IConfigurationManager _config; + + public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IConfigurationManager config) { _dlnaManager = dlnaManager; _contentDirectory = contentDirectory; _connectionManager = connectionManager; + _config = config; } public object Get(GetDescriptionXml request) diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 1e9ff9199e..3f1d9fe67d 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -101,6 +101,7 @@ + diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs index 7bd0ca7482..ebd6c6b59f 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/MediaBrowser.Common.Implementations/BaseApplicationHost.cs @@ -342,6 +342,7 @@ namespace MediaBrowser.Common.Implementations /// protected virtual void FindParts() { + ConfigurationManager.AddParts(GetExports()); Plugins = GetExports(); } diff --git a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs b/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs index 8c4840ea71..60abc14f1a 100644 --- a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs +++ b/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs @@ -1,10 +1,13 @@ -using System.IO; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; namespace MediaBrowser.Common.Implementations.Configuration @@ -25,6 +28,11 @@ namespace MediaBrowser.Common.Implementations.Configuration /// public event EventHandler ConfigurationUpdated; + /// + /// Occurs when [named configuration updated]. + /// + public event EventHandler NamedConfigurationUpdated; + /// /// Gets the logger. /// @@ -74,6 +82,9 @@ namespace MediaBrowser.Common.Implementations.Configuration } } + private ConfigurationStore[] _configurationStores = {}; + private IConfigurationFactory[] _configurationFactories; + /// /// Initializes a new instance of the class. /// @@ -89,10 +100,14 @@ namespace MediaBrowser.Common.Implementations.Configuration UpdateCachePath(); } - /// - /// The _save lock - /// - private readonly object _configurationSaveLock = new object(); + public void AddParts(IEnumerable factories) + { + _configurationFactories = factories.ToArray(); + + _configurationStores = _configurationFactories + .SelectMany(i => i.GetConfigurations()) + .ToArray(); + } /// /// Saves the configuration. @@ -103,7 +118,7 @@ namespace MediaBrowser.Common.Implementations.Configuration Directory.CreateDirectory(Path.GetDirectoryName(path)); - lock (_configurationSaveLock) + lock (_configurationSyncLock) { XmlSerializer.SerializeToFile(CommonConfiguration, path); } @@ -144,8 +159,8 @@ namespace MediaBrowser.Common.Implementations.Configuration /// private void UpdateCachePath() { - ((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ? - null : + ((BaseApplicationPaths)CommonApplicationPaths).CachePath = string.IsNullOrEmpty(CommonConfiguration.CachePath) ? + null : CommonConfiguration.CachePath; } @@ -168,5 +183,63 @@ namespace MediaBrowser.Common.Implementations.Configuration } } } + + private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary(); + + private string GetConfigurationFile(string key) + { + return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLower() + ".xml"); + } + + public object GetConfiguration(string key) + { + return _configurations.GetOrAdd(key, k => + { + var file = GetConfigurationFile(key); + + var configurationType = _configurationStores + .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)) + .ConfigurationType; + + lock (_configurationSyncLock) + { + return ConfigurationHelper.GetXmlConfiguration(configurationType, file, XmlSerializer); + } + }); + } + + public void SaveConfiguration(string key, object configuration) + { + var configurationType = GetConfigurationType(key); + + if (configuration.GetType() != configurationType) + { + throw new ArgumentException("Expected configuration type is " + configurationType.Name); + } + + _configurations.AddOrUpdate(key, configuration, (k, v) => configuration); + + var path = GetConfigurationFile(key); + Directory.CreateDirectory(Path.GetDirectoryName(path)); + + lock (_configurationSyncLock) + { + XmlSerializer.SerializeToFile(configuration, path); + } + + EventHelper.FireEventIfNotNull(NamedConfigurationUpdated, this, new ConfigurationUpdateEventArgs + { + Key = key, + NewConfiguration = configuration + + }, Logger); + } + + public Type GetConfigurationType(string key) + { + return _configurationStores + .First(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase)) + .ConfigurationType; + } } } diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs new file mode 100644 index 0000000000..310e2aa638 --- /dev/null +++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace MediaBrowser.Common.Configuration +{ + public class ConfigurationUpdateEventArgs : EventArgs + { + /// + /// Gets or sets the key. + /// + /// The key. + public string Key { get; set; } + /// + /// Gets or sets the new configuration. + /// + /// The new configuration. + public object NewConfiguration { get; set; } + } +} diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs new file mode 100644 index 0000000000..d418d0a423 --- /dev/null +++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace MediaBrowser.Common.Configuration +{ + public interface IConfigurationFactory + { + IEnumerable GetConfigurations(); + } + + public class ConfigurationStore + { + public string Key { get; set; } + + public Type ConfigurationType { get; set; } + } +} diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 0d0759b666..25698d9729 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -1,5 +1,6 @@ using MediaBrowser.Model.Configuration; using System; +using System.Collections.Generic; namespace MediaBrowser.Common.Configuration { @@ -9,7 +10,12 @@ namespace MediaBrowser.Common.Configuration /// Occurs when [configuration updated]. /// event EventHandler ConfigurationUpdated; - + + /// + /// Occurs when [named configuration updated]. + /// + event EventHandler NamedConfigurationUpdated; + /// /// Gets or sets the application paths. /// @@ -32,5 +38,40 @@ namespace MediaBrowser.Common.Configuration /// /// The new configuration. void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration); + + /// + /// Gets the configuration. + /// + /// The key. + /// System.Object. + object GetConfiguration(string key); + + /// + /// Gets the type of the configuration. + /// + /// The key. + /// Type. + Type GetConfigurationType(string key); + + /// + /// Saves the configuration. + /// + /// The key. + /// The configuration. + void SaveConfiguration(string key, object configuration); + + /// + /// Adds the parts. + /// + /// The factories. + void AddParts(IEnumerable factories); + } + + public static class ConfigurationManagerExtensions + { + public static T GetConfiguration(this IConfigurationManager manager, string key) + { + return (T)manager.GetConfiguration(key); + } } } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 2e7db5c351..9d59843171 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -55,7 +55,9 @@ Properties\SharedVersion.cs + + diff --git a/MediaBrowser.Common/Net/MimeTypes.cs b/MediaBrowser.Common/Net/MimeTypes.cs index d85a2fd1e0..0cc4fc6b4d 100644 --- a/MediaBrowser.Common/Net/MimeTypes.cs +++ b/MediaBrowser.Common/Net/MimeTypes.cs @@ -228,6 +228,11 @@ namespace MediaBrowser.Common.Net return "text/vtt"; } + if (ext.Equals(".bif", StringComparison.OrdinalIgnoreCase)) + { + return "application/octet-stream"; + } + throw new ArgumentException("Argument not supported: " + path); } } diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index b8f29d1ceb..676ef9c561 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -3,6 +3,7 @@ using MediaBrowser.Model.Chapters; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters @@ -70,5 +71,11 @@ namespace MediaBrowser.Controller.Chapters /// /// IEnumerable{ChapterProviderInfo}. IEnumerable GetProviders(); + + /// + /// Gets the configuration. + /// + /// ChapterOptions. + ChapterOptions GetConfiguration(); } } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 9468fd9875..38c2c83c47 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -43,6 +43,27 @@ namespace MediaBrowser.Controller.MediaEncoding /// Task{Stream}. Task ExtractVideoImage(string[] inputFiles, MediaProtocol protocol, Video3DFormat? threedFormat, TimeSpan? offset, CancellationToken cancellationToken); + /// + /// Extracts the video images on interval. + /// + /// The input files. + /// The protocol. + /// The threed format. + /// The interval. + /// The target directory. + /// The filename prefix. + /// The maximum width. + /// The cancellation token. + /// Task. + Task ExtractVideoImagesOnInterval(string[] inputFiles, + MediaProtocol protocol, + Video3DFormat? threedFormat, + TimeSpan interval, + string targetDirectory, + string filenamePrefix, + int? maxWidth, + CancellationToken cancellationToken); + /// /// Gets the media info. /// diff --git a/MediaBrowser.Dlna/ConfigurationExtension.cs b/MediaBrowser.Dlna/ConfigurationExtension.cs new file mode 100644 index 0000000000..821e21ccfb --- /dev/null +++ b/MediaBrowser.Dlna/ConfigurationExtension.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Configuration; +using System.Collections.Generic; + +namespace MediaBrowser.Dlna +{ + public static class ConfigurationExtension + { + public static DlnaOptions GetDlnaConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration("dlna"); + } + } + + public class DlnaConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "dlna", + ConfigurationType = typeof (DlnaOptions) + } + }; + } + } +} diff --git a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs b/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs index bb65f422c5..f594b4471d 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs +++ b/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs @@ -89,9 +89,11 @@ namespace MediaBrowser.Dlna.ContentDirectory } } - if (!string.IsNullOrEmpty(_config.Configuration.DlnaOptions.DefaultUserId)) + var userId = _config.GetDlnaConfiguration().DefaultUserId; + + if (!string.IsNullOrEmpty(userId)) { - var user = _userManager.GetUserById(new Guid(_config.Configuration.DlnaOptions.DefaultUserId)); + var user = _userManager.GetUserById(new Guid(userId)); if (user != null) { diff --git a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs index 0485255881..5f2c1c49ae 100644 --- a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs +++ b/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; @@ -58,34 +59,39 @@ namespace MediaBrowser.Dlna.Main StartSsdpHandler(); ReloadComponents(); - _config.ConfigurationUpdated += ConfigurationUpdated; + _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; } - void ConfigurationUpdated(object sender, EventArgs e) + void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { - ReloadComponents(); + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadComponents(); + } } private void ReloadComponents() { var isServerStarted = _dlnaServerStarted; - if (_config.Configuration.DlnaOptions.EnableServer && !isServerStarted) + var options = _config.GetDlnaConfiguration(); + + if (options.EnableServer && !isServerStarted) { StartDlnaServer(); } - else if (!_config.Configuration.DlnaOptions.EnableServer && isServerStarted) + else if (!options.EnableServer && isServerStarted) { DisposeDlnaServer(); } var isPlayToStarted = _manager != null; - if (_config.Configuration.DlnaOptions.EnablePlayTo && !isPlayToStarted) + if (options.EnablePlayTo && !isPlayToStarted) { StartPlayToManager(); } - else if (!_config.Configuration.DlnaOptions.EnablePlayTo && isPlayToStarted) + else if (!options.EnablePlayTo && isPlayToStarted) { DisposePlayToManager(); } diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj index 10da21e52c..1d51fc60ff 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj @@ -51,6 +51,7 @@ Properties\SharedVersion.cs + diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs index 095c6a893c..1f8c33ee7d 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/MediaBrowser.Dlna/PlayTo/PlayToManager.cs @@ -181,7 +181,7 @@ namespace MediaBrowser.Dlna.PlayTo return; } - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); var headerText = string.Join(",", headerTexts.ToArray()); @@ -220,7 +220,7 @@ namespace MediaBrowser.Dlna.PlayTo { _ssdpHandler.SendRendererSearchMessage(new IPEndPoint(localIp, 1900)); - var delay = _config.Configuration.DlnaOptions.ClientDiscoveryIntervalSeconds * 1000; + var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000; await Task.Delay(delay, _tokenSource.Token).ConfigureAwait(false); } @@ -250,7 +250,7 @@ namespace MediaBrowser.Dlna.PlayTo { socket.SendTo(request, new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900)); - var delay = _config.Configuration.DlnaOptions.ClientDiscoveryIntervalSeconds * 1000; + var delay = _config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds * 1000; await Task.Delay(delay).ConfigureAwait(false); } diff --git a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs b/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs index 22d4797a3e..ccc7d46e6e 100644 --- a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs +++ b/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs @@ -58,7 +58,7 @@ namespace MediaBrowser.Dlna.PlayTo { Url = url, UserAgent = USERAGENT, - LogRequest = _config.Configuration.DlnaOptions.EnableDebugLogging, + LogRequest = _config.GetDlnaConfiguration().EnableDebugLogging, LogErrorResponseBody = true }; @@ -76,7 +76,7 @@ namespace MediaBrowser.Dlna.PlayTo { Url = url, UserAgent = USERAGENT, - LogRequest = _config.Configuration.DlnaOptions.EnableDebugLogging, + LogRequest = _config.GetDlnaConfiguration().EnableDebugLogging, LogErrorResponseBody = true }; @@ -103,7 +103,7 @@ namespace MediaBrowser.Dlna.PlayTo { Url = url, UserAgent = USERAGENT, - LogRequest = _config.Configuration.DlnaOptions.EnableDebugLogging, + LogRequest = _config.GetDlnaConfiguration().EnableDebugLogging, LogErrorResponseBody = true }; diff --git a/MediaBrowser.Dlna/Service/BaseControlHandler.cs b/MediaBrowser.Dlna/Service/BaseControlHandler.cs index 9f7e87088a..c2af1f5a12 100644 --- a/MediaBrowser.Dlna/Service/BaseControlHandler.cs +++ b/MediaBrowser.Dlna/Service/BaseControlHandler.cs @@ -28,7 +28,7 @@ namespace MediaBrowser.Dlna.Service { try { - if (Config.Configuration.DlnaOptions.EnableDebugLogging) + if (Config.GetDlnaConfiguration().EnableDebugLogging) { LogRequest(request); } diff --git a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs index 0fc7c579b5..0455dd674d 100644 --- a/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs +++ b/MediaBrowser.Dlna/Ssdp/SsdpHandler.cs @@ -1,4 +1,4 @@ -using System.Text; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Controller.Configuration; using MediaBrowser.Dlna.Server; @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; +using System.Text; using System.Threading; namespace MediaBrowser.Dlna.Ssdp @@ -42,12 +43,15 @@ namespace MediaBrowser.Dlna.Ssdp _config = config; _serverSignature = serverSignature; - _config.ConfigurationUpdated += _config_ConfigurationUpdated; + _config.NamedConfigurationUpdated += _config_ConfigurationUpdated; } - void _config_ConfigurationUpdated(object sender, EventArgs e) + void _config_ConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { - ReloadAliveNotifier(); + if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase)) + { + ReloadAliveNotifier(); + } } public event EventHandler MessageReceived; @@ -142,7 +146,7 @@ namespace MediaBrowser.Dlna.Ssdp private void RespondToSearch(IPEndPoint endpoint, string deviceType) { - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug("RespondToSearch"); } @@ -166,7 +170,7 @@ namespace MediaBrowser.Dlna.Ssdp SendDatagram(header, values, endpoint, null); - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug("{1} - Responded to a {0} request to {2}", d.Type, endpoint, d.Address.ToString()); } @@ -255,14 +259,14 @@ namespace MediaBrowser.Dlna.Ssdp var received = (byte[])result.AsyncState; - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug(Encoding.ASCII.GetString(received)); } var args = SsdpHelper.ParseSsdpResponse(received, (IPEndPoint)endpoint); - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)); var headerText = string.Join(",", headerTexts.ToArray()); @@ -285,7 +289,7 @@ namespace MediaBrowser.Dlna.Ssdp public void Dispose() { - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; + _config.NamedConfigurationUpdated -= _config_ConfigurationUpdated; _isDisposed = true; while (_messageQueue.Count != 0) @@ -337,7 +341,7 @@ namespace MediaBrowser.Dlna.Ssdp private void NotifyAll() { - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug("Sending alive notifications"); } @@ -362,7 +366,7 @@ namespace MediaBrowser.Dlna.Ssdp values["NT"] = dev.Type; values["USN"] = dev.USN; - if (_config.Configuration.DlnaOptions.EnableDebugLogging) + if (_config.GetDlnaConfiguration().EnableDebugLogging) { _logger.Debug("{0} said {1}", dev.USN, type); } @@ -406,13 +410,13 @@ namespace MediaBrowser.Dlna.Ssdp private int _aliveNotifierIntervalMs; private void ReloadAliveNotifier() { - if (!_config.Configuration.DlnaOptions.BlastAliveMessages) + if (!_config.GetDlnaConfiguration().BlastAliveMessages) { DisposeNotificationTimer(); return; } - var intervalMs = _config.Configuration.DlnaOptions.BlastAliveMessageIntervalSeconds * 1000; + var intervalMs = _config.GetDlnaConfiguration().BlastAliveMessageIntervalSeconds * 1000; if (_notificationTimer == null || _aliveNotifierIntervalMs != intervalMs) { diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 1087c905c8..ce761ff9c1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -152,7 +152,7 @@ namespace MediaBrowser.MediaEncoding.Encoder RedirectStandardError = true, FileName = FFProbePath, Arguments = string.Format(args, - probeSizeArgument, inputPath).Trim(), + probeSizeArgument, inputPath).Trim(), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -186,8 +186,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.BeginErrorReadLine(); - result = - _jsonSerializer.DeserializeFromStream(process.StandardOutput.BaseStream); + result = _jsonSerializer.DeserializeFromStream(process.StandardOutput.BaseStream); } catch { @@ -292,7 +291,6 @@ namespace MediaBrowser.MediaEncoding.Encoder // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar var vf = "scale=600:trunc(600/dar/2)*2"; - //crop=min(iw\,ih*dar):min(ih\,iw/dar):(iw-min(iw\,iw*sar))/2:(ih - min (ih\,ih/sar))/2,scale=600:(600/dar),thumbnail" -f image2 if (threedFormat.HasValue) { @@ -344,7 +342,8 @@ namespace MediaBrowser.MediaEncoding.Encoder WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + RedirectStandardInput = true } }; @@ -370,7 +369,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { _logger.Info("Killing ffmpeg process"); - process.Kill(); + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); } @@ -437,5 +436,93 @@ namespace MediaBrowser.MediaEncoding.Encoder { return time.ToString(@"hh\:mm\:ss\.fff", UsCulture); } + + public async Task ExtractVideoImagesOnInterval(string[] inputFiles, + MediaProtocol protocol, + Video3DFormat? threedFormat, + TimeSpan interval, + string targetDirectory, + string filenamePrefix, + int? maxWidth, + CancellationToken cancellationToken) + { + var resourcePool = _videoImageResourcePool; + + var inputArgument = GetInputArgument(inputFiles, protocol); + + var vf = "fps=fps=1/" + interval.TotalSeconds.ToString(UsCulture); + + if (maxWidth.HasValue) + { + var maxWidthParam = maxWidth.Value.ToString(UsCulture); + + vf += string.Format(",scale=min(iw\\,{0}):trunc(ow/dar/2)*2", maxWidthParam); + } + + Directory.CreateDirectory(targetDirectory); + var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); + + var args = string.Format("-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); + + var probeSize = GetProbeSizeArgument(new[] { inputArgument }, protocol); + + if (!string.IsNullOrEmpty(probeSize)) + { + args = probeSize + " " + args; + } + + var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = FFMpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + RedirectStandardInput = true + } + }; + + _logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + + await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); + + process.Start(); + + var ranToCompletion = process.WaitForExit(120000); + + if (!ranToCompletion) + { + try + { + _logger.Info("Killing ffmpeg process"); + + process.StandardInput.WriteLine("q"); + + process.WaitForExit(1000); + } + catch (Exception ex) + { + _logger.ErrorException("Error killing process", ex); + } + } + + resourcePool.Release(); + + var exitCode = ranToCompletion ? process.ExitCode : -1; + + process.Dispose(); + + if (exitCode == -1) + { + var msg = string.Format("ffmpeg image extraction failed for {0}", inputArgument); + + _logger.Error(msg); + + throw new ApplicationException(msg); + } + } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 154541316d..ab9cd546aa 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -342,12 +342,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles { RedirectStandardOutput = false, RedirectStandardError = true, + RedirectStandardInput = true, CreateNoWindow = true, UseShellExecute = false, FileName = _mediaEncoder.EncoderPath, - Arguments = - string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath), + Arguments = string.Format("{0} -i \"{1}\" -c:s ass \"{2}\"", encodingParam, inputPath, outputPath), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -385,8 +385,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle conversion process"); - process.Kill(); - + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); await logTask.ConfigureAwait(false); @@ -520,6 +519,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles RedirectStandardOutput = false, RedirectStandardError = true, + RedirectStandardInput = true, FileName = _mediaEncoder.EncoderPath, Arguments = processArgs, @@ -559,8 +559,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { _logger.Info("Killing ffmpeg subtitle extraction process"); - process.Kill(); - + process.StandardInput.WriteLine("q"); process.WaitForExit(1000); } catch (Exception ex) diff --git a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj index ee6f05cc1c..3c34652c32 100644 --- a/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj +++ b/MediaBrowser.Model.Portable/MediaBrowser.Model.Portable.csproj @@ -104,6 +104,12 @@ Configuration\BaseApplicationConfiguration.cs + + Configuration\ChannelOptions.cs + + + Configuration\ChapterOptions.cs + Configuration\DlnaOptions.cs @@ -152,6 +158,9 @@ Configuration\SubtitleOptions.cs + + Configuration\SubtitlePlaybackMode.cs + Configuration\TvFileOrganizationOptions.cs diff --git a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj index a2fa1aa949..9e3df382ef 100644 --- a/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj +++ b/MediaBrowser.Model.net35/MediaBrowser.Model.net35.csproj @@ -91,6 +91,12 @@ Configuration\BaseApplicationConfiguration.cs + + Configuration\ChannelOptions.cs + + + Configuration\ChapterOptions.cs + Configuration\DlnaOptions.cs @@ -139,6 +145,9 @@ Configuration\SubtitleOptions.cs + + Configuration\SubtitlePlaybackMode.cs + Configuration\TvFileOrganizationOptions.cs diff --git a/MediaBrowser.Model/Configuration/ChannelOptions.cs b/MediaBrowser.Model/Configuration/ChannelOptions.cs new file mode 100644 index 0000000000..f0fc4d47c4 --- /dev/null +++ b/MediaBrowser.Model/Configuration/ChannelOptions.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Model.Configuration +{ + public class ChannelOptions + { + public int? PreferredStreamingWidth { get; set; } + + public string DownloadPath { get; set; } + public int? MaxDownloadAge { get; set; } + + public string[] DownloadingChannels { get; set; } + + public ChannelOptions() + { + DownloadingChannels = new string[] { }; + MaxDownloadAge = 30; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/ChapterOptions.cs b/MediaBrowser.Model/Configuration/ChapterOptions.cs new file mode 100644 index 0000000000..8a059a0a4a --- /dev/null +++ b/MediaBrowser.Model/Configuration/ChapterOptions.cs @@ -0,0 +1,27 @@ +namespace MediaBrowser.Model.Configuration +{ + 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[] { }; + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 0d728ec75e..3d5e0a9c92 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -211,6 +211,7 @@ namespace MediaBrowser.Model.Configuration public string UICulture { get; set; } + [Obsolete] public DlnaOptions DlnaOptions { get; set; } public double DownMixAudioBoost { get; set; } @@ -223,6 +224,8 @@ namespace MediaBrowser.Model.Configuration public string[] ManualLoginClients { get; set; } public ChannelOptions ChannelOptions { get; set; } + + [Obsolete] public ChapterOptions ChapterOptions { get; set; } /// @@ -268,73 +271,27 @@ namespace MediaBrowser.Model.Configuration SeasonZeroDisplayName = "Specials"; - LiveTvOptions = new LiveTvOptions(); - - TvFileOrganizationOptions = new TvFileOrganizationOptions(); - EnableRealtimeMonitor = true; - List options = new List + UICulture = "en-us"; + + MetadataOptions = new List { new MetadataOptions(1, 1280) {ItemType = "Book"}, new MetadataOptions(1, 1280) {ItemType = "MusicAlbum"}, new MetadataOptions(1, 1280) {ItemType = "MusicArtist"}, new MetadataOptions(0, 1280) {ItemType = "Season"} - }; - - MetadataOptions = options.ToArray(); - DlnaOptions = new DlnaOptions(); - - UICulture = "en-us"; + }.ToArray(); NotificationOptions = new NotificationOptions(); SubtitleOptions = new SubtitleOptions(); ChannelOptions = new ChannelOptions(); - ChapterOptions = new ChapterOptions(); - } - } - - public class ChannelOptions - { - public int? PreferredStreamingWidth { get; set; } - public string DownloadPath { get; set; } - public int? MaxDownloadAge { get; set; } - - public string[] DownloadingChannels { get; set; } - - public ChannelOptions() - { - DownloadingChannels = new string[] { }; - 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[] { }; + LiveTvOptions = new LiveTvOptions(); + TvFileOrganizationOptions = new TvFileOrganizationOptions(); } } } diff --git a/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs new file mode 100644 index 0000000000..e6a3c3091e --- /dev/null +++ b/MediaBrowser.Model/Configuration/SubtitlePlaybackMode.cs @@ -0,0 +1,10 @@ +namespace MediaBrowser.Model.Configuration +{ + public enum SubtitlePlaybackMode + { + Default = 0, + Always = 1, + OnlyForced = 2, + None = 3 + } +} \ No newline at end of file diff --git a/MediaBrowser.Model/Configuration/UserConfiguration.cs b/MediaBrowser.Model/Configuration/UserConfiguration.cs index 885172bed7..94a41bdda0 100644 --- a/MediaBrowser.Model/Configuration/UserConfiguration.cs +++ b/MediaBrowser.Model/Configuration/UserConfiguration.cs @@ -91,12 +91,4 @@ namespace MediaBrowser.Model.Configuration ExcludeFoldersFromGrouping = new string[] { }; } } - - public enum SubtitlePlaybackMode - { - Default = 0, - Always = 1, - OnlyForced = 2, - None = 3 - } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 9efa63283d..5d753f9c29 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -73,6 +73,9 @@ + + + diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs index 5f8664bec3..6e2cd77eb3 100644 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ b/MediaBrowser.Providers/Chapters/ChapterManager.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -8,6 +9,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Chapters; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; @@ -192,8 +194,10 @@ namespace MediaBrowser.Providers.Chapters if (!includeDisabledProviders) { + var options = GetConfiguration(); + providers = providers - .Where(i => !_config.Configuration.ChapterOptions.DisabledFetchers.Contains(i.Name)) + .Where(i => !options.DisabledFetchers.Contains(i.Name)) .ToArray(); } @@ -224,8 +228,10 @@ namespace MediaBrowser.Providers.Chapters private int GetConfiguredOrder(IChapterProvider provider) { + var options = GetConfiguration(); + // See if there's a user-defined order - var index = Array.IndexOf(_config.Configuration.ChapterOptions.FetcherOrder, provider.Name); + var index = Array.IndexOf(options.FetcherOrder, provider.Name); if (index != -1) { @@ -257,5 +263,25 @@ namespace MediaBrowser.Providers.Chapters { return _itemRepo.SaveChapters(new Guid(itemId), chapters, cancellationToken); } + + public ChapterOptions GetConfiguration() + { + return _config.GetConfiguration("chapters"); + } + } + + public class ChapterConfigurationStore : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new List + { + new ConfigurationStore + { + Key = "chapters", + ConfigurationType = typeof (ChapterOptions) + } + }; + } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 65faae3274..92b4616e71 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -492,9 +492,11 @@ namespace MediaBrowser.Providers.MediaInfo private async Task> DownloadChapters(Video video, List currentChapters, CancellationToken cancellationToken) { - if ((_config.Configuration.ChapterOptions.DownloadEpisodeChapters && + var options = _chapterManager.GetConfiguration(); + + if ((options.DownloadEpisodeChapters && video is Episode) || - (_config.Configuration.ChapterOptions.DownloadMovieChapters && + (options.DownloadMovieChapters && video is Movie)) { var results = await _chapterManager.Search(video, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index 056a526f6c..d35282e55c 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -1,12 +1,15 @@ -using MediaBrowser.Common.IO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; 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.MediaEncoding; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; using System; using System.Collections.Generic; using System.Globalization; @@ -14,7 +17,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Server.Implementations.MediaEncoder { @@ -61,23 +63,25 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder return false; } + var options = _chapterManager.GetConfiguration(); + if (video is Movie) { - if (!_config.Configuration.ChapterOptions.EnableMovieChapterImageExtraction) + if (!options.EnableMovieChapterImageExtraction) { return false; } } else if (video is Episode) { - if (!_config.Configuration.ChapterOptions.EnableEpisodeChapterImageExtraction) + if (!options.EnableEpisodeChapterImageExtraction) { return false; } } else { - if (!_config.Configuration.ChapterOptions.EnableOtherVideoChapterImageExtraction) + if (!options.EnableOtherVideoChapterImageExtraction) { return false; } diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index d03c5fe3dc..8007e05062 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -41,6 +41,7 @@ using MediaBrowser.Dlna.Main; 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; @@ -273,11 +274,35 @@ namespace MediaBrowser.ServerApplication public override Task Init(IProgress progress) { - DeleteDeprecatedModules(); + PerformVersionMigration(); return base.Init(progress); } + private void PerformVersionMigration() + { + DeleteDeprecatedModules(); + + MigrateModularConfigurations(); + } + + private void MigrateModularConfigurations() + { + if (ServerConfigurationManager.Configuration.DlnaOptions != null) + { + ServerConfigurationManager.SaveConfiguration("dlna", ServerConfigurationManager.Configuration.DlnaOptions); + ServerConfigurationManager.Configuration.DlnaOptions = null; + ServerConfigurationManager.SaveConfiguration(); + } + + if (ServerConfigurationManager.Configuration.ChapterOptions != null) + { + ServerConfigurationManager.SaveConfiguration("chapters", ServerConfigurationManager.Configuration.ChapterOptions); + ServerConfigurationManager.Configuration.ChapterOptions = null; + ServerConfigurationManager.SaveConfiguration(); + } + } + private void DeleteDeprecatedModules() { try