diff options
Diffstat (limited to 'MediaBrowser.Server.Startup.Common')
6 files changed, 647 insertions, 9 deletions
diff --git a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs index e5a17f8fc..b14378eb8 100644 --- a/MediaBrowser.Server.Startup.Common/ApplicationHost.cs +++ b/MediaBrowser.Server.Startup.Common/ApplicationHost.cs @@ -50,9 +50,7 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Subtitles; using MediaBrowser.Server.Implementations; using MediaBrowser.Server.Implementations.Activity; -using MediaBrowser.Server.Implementations.Configuration; using MediaBrowser.Server.Implementations.Devices; -using MediaBrowser.Server.Implementations.HttpServer; using MediaBrowser.Server.Implementations.IO; using MediaBrowser.Server.Implementations.Notifications; using MediaBrowser.Server.Implementations.Persistence; @@ -72,6 +70,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Emby.Common.Implementations; @@ -133,8 +132,10 @@ using MediaBrowser.Model.Social; using MediaBrowser.Model.Text; using MediaBrowser.Model.Xml; using MediaBrowser.Server.Implementations.Archiving; +using MediaBrowser.Server.Startup.Common.Configuration; using OpenSubtitlesHandler; using ServiceStack; +using SocketHttpListener.Primitives; using StringExtensions = MediaBrowser.Controller.Extensions.StringExtensions; namespace MediaBrowser.Server.Startup.Common @@ -271,7 +272,7 @@ namespace MediaBrowser.Server.Startup.Common ILogManager logManager, StartupOptions options, IFileSystem fileSystem, - INativeApp nativeApp, + INativeApp nativeApp, IPowerManagement powerManagement, string releaseAssetFilename) : base(applicationPaths, logManager, fileSystem) @@ -613,7 +614,7 @@ namespace MediaBrowser.Server.Startup.Common RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager)); - HttpServer = ServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamProvider, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer); + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamProvider, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); RegisterSingleInstance(HttpServer, false); progress.Report(10); @@ -736,6 +737,32 @@ namespace MediaBrowser.Server.Startup.Common await ((UserManager)UserManager).Initialize().ConfigureAwait(false); } + private ICertificate GetCertificate(string certificateLocation) + { + if (string.IsNullOrWhiteSpace(certificateLocation)) + { + return null; + } + + try + { + X509Certificate2 localCert = new X509Certificate2(certificateLocation); + //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; + if (localCert.PrivateKey == null) + { + //throw new FileNotFoundException("Secure requested, no private key included", certificateLocation); + return null; + } + + return new Certificate(localCert); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading cert from {0}", ex, certificateLocation); + return null; + } + } + private IImageProcessor GetImageProcessor() { var maxConcurrentImageProcesses = Math.Max(Environment.ProcessorCount, 4); @@ -969,6 +996,7 @@ namespace MediaBrowser.Server.Startup.Common } private string CertificatePath { get; set; } + private ICertificate Certificate { get; set; } private IEnumerable<string> GetUrlPrefixes() { @@ -998,10 +1026,11 @@ namespace MediaBrowser.Server.Startup.Common private void StartServer() { CertificatePath = GetCertificatePath(true); + Certificate = GetCertificate(CertificatePath); try { - ServerManager.Start(GetUrlPrefixes(), CertificatePath); + ServerManager.Start(GetUrlPrefixes()); return; } catch (Exception ex) @@ -1018,7 +1047,7 @@ namespace MediaBrowser.Server.Startup.Common try { - ServerManager.Start(GetUrlPrefixes(), CertificatePath); + ServerManager.Start(GetUrlPrefixes()); } catch (Exception ex) { @@ -1298,7 +1327,7 @@ namespace MediaBrowser.Server.Startup.Common public bool SupportsHttps { - get { return !string.IsNullOrWhiteSpace(HttpServer.CertificatePath); } + get { return Certificate != null; } } public async Task<string> GetLocalApiUrl() diff --git a/MediaBrowser.Server.Startup.Common/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Startup.Common/Configuration/ServerConfigurationManager.cs new file mode 100644 index 000000000..756a8d0bf --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/Configuration/ServerConfigurationManager.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Emby.Common.Implementations.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Server.Startup.Common.Configuration +{ + /// <summary> + /// Class ServerConfigurationManager + /// </summary> + public class ServerConfigurationManager : BaseConfigurationManager, IServerConfigurationManager + { + + /// <summary> + /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="logManager">The log manager.</param> + /// <param name="xmlSerializer">The XML serializer.</param> + /// <param name="fileSystem">The file system.</param> + public ServerConfigurationManager(IApplicationPaths applicationPaths, ILogManager logManager, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + : base(applicationPaths, logManager, xmlSerializer, fileSystem) + { + UpdateMetadataPath(); + } + + public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating; + + /// <summary> + /// Gets the type of the configuration. + /// </summary> + /// <value>The type of the configuration.</value> + protected override Type ConfigurationType + { + get { return typeof(ServerConfiguration); } + } + + /// <summary> + /// Gets the application paths. + /// </summary> + /// <value>The application paths.</value> + public IServerApplicationPaths ApplicationPaths + { + get { return (IServerApplicationPaths)CommonApplicationPaths; } + } + + /// <summary> + /// Gets the configuration. + /// </summary> + /// <value>The configuration.</value> + public ServerConfiguration Configuration + { + get { return (ServerConfiguration)CommonConfiguration; } + } + + /// <summary> + /// Called when [configuration updated]. + /// </summary> + protected override void OnConfigurationUpdated() + { + UpdateMetadataPath(); + + base.OnConfigurationUpdated(); + } + + public override void AddParts(IEnumerable<IConfigurationFactory> factories) + { + base.AddParts(factories); + + UpdateTranscodingTempPath(); + } + + /// <summary> + /// Updates the metadata path. + /// </summary> + private void UpdateMetadataPath() + { + string metadataPath; + + if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) + { + metadataPath = GetInternalMetadataPath(); + } + else + { + metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); + } + + ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath; + + ((ServerApplicationPaths)ApplicationPaths).ItemsByNamePath = ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath; + } + + private string GetInternalMetadataPath() + { + return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata"); + } + + /// <summary> + /// Updates the transcoding temporary path. + /// </summary> + private void UpdateTranscodingTempPath() + { + var encodingConfig = this.GetConfiguration<EncodingOptions>("encoding"); + + ((ServerApplicationPaths)ApplicationPaths).TranscodingTempPath = string.IsNullOrEmpty(encodingConfig.TranscodingTempPath) ? + null : + Path.Combine(encodingConfig.TranscodingTempPath, "transcoding-temp"); + } + + protected override void OnNamedConfigurationUpdated(string key, object configuration) + { + base.OnNamedConfigurationUpdated(key, configuration); + + if (string.Equals(key, "encoding", StringComparison.OrdinalIgnoreCase)) + { + UpdateTranscodingTempPath(); + } + } + + /// <summary> + /// Replaces the configuration. + /// </summary> + /// <param name="newConfiguration">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + public override void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration) + { + var newConfig = (ServerConfiguration)newConfiguration; + + ValidatePathSubstitutions(newConfig); + ValidateMetadataPath(newConfig); + ValidateSslCertificate(newConfig); + + EventHelper.FireEventIfNotNull(ConfigurationUpdating, this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig }, Logger); + + base.ReplaceConfiguration(newConfiguration); + } + + + /// <summary> + /// Validates the SSL certificate. + /// </summary> + /// <param name="newConfig">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + private void ValidateSslCertificate(BaseApplicationConfiguration newConfig) + { + var serverConfig = (ServerConfiguration)newConfig; + + var newPath = serverConfig.CertificatePath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(Configuration.CertificatePath ?? string.Empty, newPath)) + { + // Validate + if (!FileSystem.FileExists(newPath)) + { + throw new FileNotFoundException(string.Format("Certificate file '{0}' does not exist.", newPath)); + } + } + } + + private void ValidatePathSubstitutions(ServerConfiguration newConfig) + { + foreach (var map in newConfig.PathSubstitutions) + { + if (string.IsNullOrWhiteSpace(map.From) || string.IsNullOrWhiteSpace(map.To)) + { + throw new ArgumentException("Invalid path substitution"); + } + } + } + + /// <summary> + /// Validates the metadata path. + /// </summary> + /// <param name="newConfig">The new configuration.</param> + /// <exception cref="System.IO.DirectoryNotFoundException"></exception> + private void ValidateMetadataPath(ServerConfiguration newConfig) + { + var newPath = newConfig.MetadataPath; + + if (!string.IsNullOrWhiteSpace(newPath) + && !string.Equals(Configuration.MetadataPath ?? string.Empty, newPath)) + { + // Validate + if (!FileSystem.DirectoryExists(newPath)) + { + throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); + } + + EnsureWriteAccess(newPath); + } + } + + public void DisableMetadataService(string service) + { + DisableMetadataService(typeof(Movie), Configuration, service); + DisableMetadataService(typeof(Episode), Configuration, service); + DisableMetadataService(typeof(Series), Configuration, service); + DisableMetadataService(typeof(Season), Configuration, service); + DisableMetadataService(typeof(MusicArtist), Configuration, service); + DisableMetadataService(typeof(MusicAlbum), Configuration, service); + DisableMetadataService(typeof(MusicVideo), Configuration, service); + DisableMetadataService(typeof(Video), Configuration, service); + } + + private void DisableMetadataService(Type type, ServerConfiguration config, string service) + { + var options = GetMetadataOptions(type, config); + + if (!options.DisabledMetadataSavers.Contains(service, StringComparer.OrdinalIgnoreCase)) + { + var list = options.DisabledMetadataSavers.ToList(); + + list.Add(service); + + options.DisabledMetadataSavers = list.ToArray(); + } + } + + private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config) + { + var options = config.MetadataOptions + .FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase)); + + if (options == null) + { + var list = config.MetadataOptions.ToList(); + + options = new MetadataOptions + { + ItemType = type.Name + }; + + list.Add(options); + + config.MetadataOptions = list.ToArray(); + } + + return options; + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/HttpServerFactory.cs b/MediaBrowser.Server.Startup.Common/HttpServerFactory.cs new file mode 100644 index 000000000..c0c376996 --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/HttpServerFactory.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Emby.Common.Implementations.Net; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using ServiceStack.Text.Jsv; +using SocketHttpListener.Primitives; + +namespace MediaBrowser.Server.Startup.Common +{ + /// <summary> + /// Class ServerFactory + /// </summary> + public static class HttpServerFactory + { + /// <summary> + /// Creates the server. + /// </summary> + /// <returns>IHttpServer.</returns> + public static IHttpServer CreateServer(IServerApplicationHost applicationHost, + ILogManager logManager, + IServerConfigurationManager config, + INetworkManager networkmanager, + IMemoryStreamFactory streamProvider, + string serverName, + string defaultRedirectpath, + ITextEncoding textEncoding, + ISocketFactory socketFactory, + ICryptoProvider cryptoProvider, + IJsonSerializer json, + IXmlSerializer xml, + IEnvironmentInfo environment, + ICertificate certificate) + { + var logger = logManager.GetLogger("HttpServer"); + + return new HttpListenerHost(applicationHost, + logger, + config, + serverName, + defaultRedirectpath, + networkmanager, + streamProvider, + textEncoding, + socketFactory, + cryptoProvider, + json, + xml, + environment, + certificate, + new StreamFactory(), + GetParseFn); + } + + private static Func<string, object> GetParseFn(Type propertyType) + { + return s => JsvReader.GetParseFn(propertyType)(s); + } + } + + public class StreamFactory : IStreamFactory + { + public Stream CreateNetworkStream(ISocket socket, bool ownsSocket) + { + var netSocket = (NetSocket)socket; + + return new NetworkStream(netSocket.Socket, ownsSocket); + } + + public Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate) + { + var sslStream = (SslStream)stream; + var cert = (Certificate)certificate; + + return sslStream.AuthenticateAsServerAsync(cert.X509Certificate); + } + + public Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen) + { + return new SslStream(innerStream, leaveInnerStreamOpen); + } + } + + public class Certificate : ICertificate + { + public Certificate(X509Certificate x509Certificate) + { + X509Certificate = x509Certificate; + } + + public X509Certificate X509Certificate { get; private set; } + } +} diff --git a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj index 7e01fd36d..7ac020cc4 100644 --- a/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj +++ b/MediaBrowser.Server.Startup.Common/MediaBrowser.Server.Startup.Common.csproj @@ -49,14 +49,22 @@ <HintPath>..\packages\Patterns.Logging.1.0.0.6\lib\portable-net45+win8\Patterns.Logging.dll</HintPath> <Private>True</Private> </Reference> - <Reference Include="ServiceStack.Text, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <Reference Include="ServiceStack, Version=4.0.0.0, Culture=neutral, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath> + <HintPath>..\ThirdParty\ServiceStack\ServiceStack.dll</HintPath> + </Reference> + <Reference Include="ServiceStack.Text, Version=4.5.4.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\ServiceStack.Text.4.5.4\lib\net45\ServiceStack.Text.dll</HintPath> + <Private>True</Private> </Reference> <Reference Include="SimpleInjector, Version=3.2.4.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL"> <HintPath>..\packages\SimpleInjector.3.2.4\lib\net45\SimpleInjector.dll</HintPath> <Private>True</Private> </Reference> + <Reference Include="SocketHttpListener.Portable, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\ThirdParty\emby\SocketHttpListener.Portable.dll</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Configuration" /> <Reference Include="System.Core" /> @@ -73,10 +81,12 @@ <Compile Include="ApplicationHost.cs" /> <Compile Include="ApplicationPathHelper.cs" /> <Compile Include="Browser\BrowserLauncher.cs" /> + <Compile Include="Configuration\ServerConfigurationManager.cs" /> <Compile Include="EntryPoints\StartupWizard.cs" /> <Compile Include="FFMpeg\FFMpegLoader.cs" /> <Compile Include="FFMpeg\FFMpegInstallInfo.cs" /> <Compile Include="FFMpeg\FFMpegInfo.cs" /> + <Compile Include="HttpServerFactory.cs" /> <Compile Include="INativeApp.cs" /> <Compile Include="MbLinkShortcutHandler.cs" /> <Compile Include="Migrations\IVersionMigration.cs" /> @@ -103,6 +113,7 @@ <Compile Include="Security\X509Extension.cs" /> <Compile Include="Security\X509Extensions.cs" /> <Compile Include="Security\X520Attributes.cs" /> + <Compile Include="ServerApplicationPaths.cs" /> <Compile Include="StartupOptions.cs" /> <Compile Include="SystemEvents.cs" /> <Compile Include="TextLocalizer.cs" /> diff --git a/MediaBrowser.Server.Startup.Common/ServerApplicationPaths.cs b/MediaBrowser.Server.Startup.Common/ServerApplicationPaths.cs new file mode 100644 index 000000000..11de1c4ab --- /dev/null +++ b/MediaBrowser.Server.Startup.Common/ServerApplicationPaths.cs @@ -0,0 +1,233 @@ +using System.IO; +using Emby.Common.Implementations; +using MediaBrowser.Controller; + +namespace MediaBrowser.Server.Startup.Common +{ + /// <summary> + /// Extends BaseApplicationPaths to add paths that are only applicable on the server + /// </summary> + public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths + { + /// <summary> + /// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class. + /// </summary> + public ServerApplicationPaths(string programDataPath, string applicationPath, string applicationResourcesPath) + : base(programDataPath, applicationPath) + { + ApplicationResourcesPath = applicationResourcesPath; + } + + public string ApplicationResourcesPath { get; private set; } + + /// <summary> + /// Gets the path to the base root media directory + /// </summary> + /// <value>The root folder path.</value> + public string RootFolderPath + { + get + { + return Path.Combine(ProgramDataPath, "root"); + } + } + + /// <summary> + /// Gets the path to the default user view directory. Used if no specific user view is defined. + /// </summary> + /// <value>The default user views path.</value> + public string DefaultUserViewsPath + { + get + { + return Path.Combine(RootFolderPath, "default"); + } + } + + /// <summary> + /// Gets the path to localization data. + /// </summary> + /// <value>The localization path.</value> + public string LocalizationPath + { + get + { + return Path.Combine(ProgramDataPath, "localization"); + } + } + + /// <summary> + /// The _ibn path + /// </summary> + private string _ibnPath; + /// <summary> + /// Gets the path to the Images By Name directory + /// </summary> + /// <value>The images by name path.</value> + public string ItemsByNamePath + { + get + { + return _ibnPath ?? (_ibnPath = Path.Combine(ProgramDataPath, "ImagesByName")); + } + set + { + _ibnPath = value; + } + } + + /// <summary> + /// Gets the path to the People directory + /// </summary> + /// <value>The people path.</value> + public string PeoplePath + { + get + { + return Path.Combine(ItemsByNamePath, "People"); + } + } + + public string ArtistsPath + { + get + { + return Path.Combine(ItemsByNamePath, "artists"); + } + } + + /// <summary> + /// Gets the path to the Genre directory + /// </summary> + /// <value>The genre path.</value> + public string GenrePath + { + get + { + return Path.Combine(ItemsByNamePath, "Genre"); + } + } + + /// <summary> + /// Gets the path to the Genre directory + /// </summary> + /// <value>The genre path.</value> + public string MusicGenrePath + { + get + { + return Path.Combine(ItemsByNamePath, "MusicGenre"); + } + } + + /// <summary> + /// Gets the path to the Studio directory + /// </summary> + /// <value>The studio path.</value> + public string StudioPath + { + get + { + return Path.Combine(ItemsByNamePath, "Studio"); + } + } + + /// <summary> + /// Gets the path to the Year directory + /// </summary> + /// <value>The year path.</value> + public string YearPath + { + get + { + return Path.Combine(ItemsByNamePath, "Year"); + } + } + + /// <summary> + /// Gets the path to the General IBN directory + /// </summary> + /// <value>The general path.</value> + public string GeneralPath + { + get + { + return Path.Combine(ItemsByNamePath, "general"); + } + } + + /// <summary> + /// Gets the path to the Ratings IBN directory + /// </summary> + /// <value>The ratings path.</value> + public string RatingsPath + { + get + { + return Path.Combine(ItemsByNamePath, "ratings"); + } + } + + /// <summary> + /// Gets the media info images path. + /// </summary> + /// <value>The media info images path.</value> + public string MediaInfoImagesPath + { + get + { + return Path.Combine(ItemsByNamePath, "mediainfo"); + } + } + + /// <summary> + /// Gets the path to the user configuration directory + /// </summary> + /// <value>The user configuration directory path.</value> + public string UserConfigurationDirectoryPath + { + get + { + return Path.Combine(ConfigurationDirectoryPath, "users"); + } + } + + private string _transcodingTempPath; + public string TranscodingTempPath + { + get + { + return _transcodingTempPath ?? (_transcodingTempPath = Path.Combine(ProgramDataPath, "transcoding-temp")); + } + set + { + _transcodingTempPath = value; + } + } + + /// <summary> + /// Gets the game genre path. + /// </summary> + /// <value>The game genre path.</value> + public string GameGenrePath + { + get + { + return Path.Combine(ItemsByNamePath, "GameGenre"); + } + } + + private string _internalMetadataPath; + public string InternalMetadataPath + { + get + { + return _internalMetadataPath ?? (_internalMetadataPath = Path.Combine(DataPath, "metadata")); + } + set + { + _internalMetadataPath = value; + } + } + } +} diff --git a/MediaBrowser.Server.Startup.Common/packages.config b/MediaBrowser.Server.Startup.Common/packages.config index ea3d0e7f8..fa1cc928c 100644 --- a/MediaBrowser.Server.Startup.Common/packages.config +++ b/MediaBrowser.Server.Startup.Common/packages.config @@ -3,5 +3,6 @@ <package id="MediaBrowser.Naming" version="1.0.0.59" targetFramework="net46" /> <package id="Mono.Posix" version="4.0.0.0" targetFramework="net45" /> <package id="Patterns.Logging" version="1.0.0.6" targetFramework="net46" /> + <package id="ServiceStack.Text" version="4.5.4" targetFramework="net46" /> <package id="SimpleInjector" version="3.2.4" targetFramework="net46" /> </packages>
\ No newline at end of file |
