소스 검색

Merge branch 'master' into fields

Bond-009 6 년 전
부모
커밋
bdfd042d70
73개의 변경된 파일1184개의 추가작업 그리고 982개의 파일을 삭제
  1. 71 9
      .editorconfig
  2. 3 1
      Emby.Dlna/DlnaManager.cs
  3. 2 1
      Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
  4. 21 82
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  5. 16 3
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  6. 139 401
      Emby.Server.Implementations/ApplicationHost.cs
  7. 1 1
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  8. 3 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  9. 1 1
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  10. 1 1
      Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
  11. 39 35
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  12. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  13. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  14. 5 10
      Emby.Server.Implementations/ServerApplicationPaths.cs
  15. 1 1
      Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
  16. 1 1
      Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
  17. 1 1
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  18. 6 5
      Jellyfin.Server/CoreAppHost.cs
  19. 9 3
      Jellyfin.Server/Jellyfin.Server.csproj
  20. 132 83
      Jellyfin.Server/Program.cs
  21. 83 69
      Jellyfin.Server/SocketSharp/RequestMono.cs
  22. 20 24
      Jellyfin.Server/SocketSharp/SharpWebSocket.cs
  23. 37 24
      Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs
  24. 22 5
      Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs
  25. 8 12
      Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs
  26. 1 1
      MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs
  27. 1 1
      MediaBrowser.Api/System/ActivityLogWebSocketListener.cs
  28. 2 14
      MediaBrowser.Common/IApplicationHost.cs
  29. 4 0
      MediaBrowser.Common/MediaBrowser.Common.csproj
  30. 1 1
      MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs
  31. 3 3
      MediaBrowser.Model/System/PublicSystemInfo.cs
  32. 2 1
      MediaBrowser.Model/System/SystemInfo.cs
  33. 1 1
      MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs
  34. 1 1
      MediaBrowser.Providers/Movies/MovieDbImageProvider.cs
  35. 1 1
      MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
  36. 1 1
      MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
  37. 1 1
      MediaBrowser.Providers/Photos/PhotoMetadataService.cs
  38. 1 1
      MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
  39. 1 1
      MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs
  40. 1 1
      MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs
  41. 1 1
      MediaBrowser.WebDashboard/jellyfin-web
  42. 1 1
      MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
  43. 18 6
      build
  44. 2 0
      deployment/README.md
  45. 26 14
      deployment/centos-package-x64/Dockerfile
  46. 0 1
      deployment/centos-package-x64/clean.sh
  47. 34 0
      deployment/centos-package-x64/clean.sh
  48. 1 0
      deployment/centos-package-x64/dependencies.txt
  49. 20 0
      deployment/centos-package-x64/docker-build.sh
  50. 0 1
      deployment/centos-package-x64/package.sh
  51. 80 0
      deployment/centos-package-x64/package.sh
  52. 15 17
      deployment/debian-package-x64/Dockerfile
  53. 24 2
      deployment/debian-package-x64/clean.sh
  54. 19 0
      deployment/debian-package-x64/docker-build.sh
  55. 27 27
      deployment/debian-package-x64/package.sh
  56. 6 4
      deployment/debian-package-x64/pkg-src/bin/restart.sh
  57. 10 6
      deployment/debian-package-x64/pkg-src/conf/jellyfin
  58. 12 12
      deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers
  59. 1 1
      deployment/debian-package-x64/pkg-src/jellyfin.service
  60. 26 14
      deployment/fedora-package-x64/Dockerfile
  61. 29 13
      deployment/fedora-package-x64/clean.sh
  62. 20 0
      deployment/fedora-package-x64/docker-build.sh
  63. 35 41
      deployment/fedora-package-x64/package.sh
  64. 13 6
      deployment/fedora-package-x64/pkg-src/jellyfin.env
  65. 1 1
      deployment/fedora-package-x64/pkg-src/jellyfin.service
  66. 7 7
      deployment/fedora-package-x64/pkg-src/jellyfin.sudoers
  67. 21 0
      deployment/ubuntu-package-x64/Dockerfile
  68. 29 0
      deployment/ubuntu-package-x64/clean.sh
  69. 1 0
      deployment/ubuntu-package-x64/dependencies.txt
  70. 19 0
      deployment/ubuntu-package-x64/docker-build.sh
  71. 31 0
      deployment/ubuntu-package-x64/package.sh
  72. 1 0
      deployment/ubuntu-package-x64/pkg-src
  73. 8 0
      jellyfin.ruleset

+ 71 - 9
.editorconfig

@@ -55,15 +55,77 @@ dotnet_style_prefer_conditional_expression_over_return = true:silent
 ###############################
 # Naming Conventions          #
 ###############################
-# Style Definitions
-dotnet_naming_style.pascal_case_style.capitalization             = pascal_case
-# Use PascalCase for constant fields
-dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols  = constant_fields
-dotnet_naming_rule.constant_fields_should_be_pascal_case.style    = pascal_case_style
-dotnet_naming_symbols.constant_fields.applicable_kinds            = field
-dotnet_naming_symbols.constant_fields.applicable_accessibilities  = *
-dotnet_naming_symbols.constant_fields.required_modifiers          = const
+# Style Definitions (From Roslyn)
+
+# Non-private static fields are PascalCase
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
+dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
+
+dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
+dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
+
+dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
+
+# Constants are PascalCase
+dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
+dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
+
+dotnet_naming_symbols.constants.applicable_kinds = field, local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_style.constant_style.capitalization = pascal_case
+
+# Static fields are camelCase and start with s_
+dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
+dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
+
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+
+dotnet_naming_style.static_field_style.capitalization = camel_case
+dotnet_naming_style.static_field_style.required_prefix = _
+
+# Instance fields are camelCase and start with _
+dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
+dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+
+dotnet_naming_symbols.instance_fields.applicable_kinds = field
+
+dotnet_naming_style.instance_field_style.capitalization = camel_case
+dotnet_naming_style.instance_field_style.required_prefix = _
+
+# Locals and parameters are camelCase
+dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
+dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
+
+dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
+
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# Local functions are PascalCase
+dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+
+dotnet_naming_style.local_function_style.capitalization = pascal_case
+
+# By default, name items with PascalCase
+dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
+dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
+
+dotnet_naming_symbols.all_members.applicable_kinds = *
+
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
 ###############################
 # C# Coding Conventions       #
 ###############################

+ 3 - 1
Emby.Dlna/DlnaManager.cs

@@ -38,7 +38,9 @@ namespace Emby.Dlna
             IFileSystem fileSystem,
             IApplicationPaths appPaths,
             ILoggerFactory loggerFactory,
-            IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IAssemblyInfo assemblyInfo)
+            IJsonSerializer jsonSerializer,
+            IServerApplicationHost appHost,
+            IAssemblyInfo assemblyInfo)
         {
             _xmlSerializer = xmlSerializer;
             _fileSystem = fileSystem;

+ 2 - 1
Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs

@@ -36,7 +36,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
             };
         }
 
-        public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(config, logger, xmlReaderSettingsFactory)
+        public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory)
+            : base(config, logger, xmlReaderSettingsFactory)
         {
         }
     }

+ 21 - 82
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -1,3 +1,4 @@
+using System;
 using System.IO;
 using MediaBrowser.Common.Configuration;
 
@@ -14,50 +15,44 @@ namespace Emby.Server.Implementations.AppBase
         /// </summary>
         protected BaseApplicationPaths(
             string programDataPath,
-            string appFolderPath,
-            string logDirectoryPath = null,
-            string configurationDirectoryPath = null,
-            string cacheDirectoryPath = null)
+            string logDirectoryPath,
+            string configurationDirectoryPath,
+            string cacheDirectoryPath)
         {
             ProgramDataPath = programDataPath;
-            ProgramSystemPath = appFolderPath;
             LogDirectoryPath = logDirectoryPath;
             ConfigurationDirectoryPath = configurationDirectoryPath;
             CachePath = cacheDirectoryPath;
+
+            DataPath = Path.Combine(ProgramDataPath, "data");
         }
 
+        /// <summary>
+        /// Gets the path to the program data folder
+        /// </summary>
+        /// <value>The program data path.</value>
         public string ProgramDataPath { get; private set; }
 
         /// <summary>
         /// Gets the path to the system folder
         /// </summary>
-        public string ProgramSystemPath { get; private set; }
+        public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
 
-        /// <summary>
-        /// The _data directory
-        /// </summary>
-        private string _dataDirectory;
         /// <summary>
         /// Gets the folder path to the data directory
         /// </summary>
         /// <value>The data directory.</value>
+        private string _dataPath;
         public string DataPath
         {
-            get
-            {
-                if (_dataDirectory == null)
-                {
-                    _dataDirectory = Path.Combine(ProgramDataPath, "data");
-
-                    Directory.CreateDirectory(_dataDirectory);
-                }
-
-                return _dataDirectory;
-            }
+            get => _dataPath;
+            private set => _dataPath = Directory.CreateDirectory(value).FullName;
         }
 
-        private const string _virtualDataPath = "%AppDataPath%";
-        public string VirtualDataPath => _virtualDataPath;
+        /// <summary>
+        /// Gets the magic strings used for virtual path manipulation.
+        /// </summary>
+        public string VirtualDataPath { get; } = "%AppDataPath%";
 
         /// <summary>
         /// Gets the image cache path.
@@ -83,55 +78,17 @@ namespace Emby.Server.Implementations.AppBase
         /// <value>The plugin configurations path.</value>
         public string TempUpdatePath => Path.Combine(ProgramDataPath, "updates");
 
-        /// <summary>
-        /// The _log directory
-        /// </summary>
-        private string _logDirectoryPath;
-
         /// <summary>
         /// Gets the path to the log directory
         /// </summary>
         /// <value>The log directory path.</value>
-        public string LogDirectoryPath
-        {
-            get
-            {
-                if (string.IsNullOrEmpty(_logDirectoryPath))
-                {
-                    _logDirectoryPath = Path.Combine(ProgramDataPath, "logs");
-
-                    Directory.CreateDirectory(_logDirectoryPath);
-                }
-
-                return _logDirectoryPath;
-            }
-            set => _logDirectoryPath = value;
-        }
-
-        /// <summary>
-        /// The _config directory
-        /// </summary>
-        private string _configurationDirectoryPath;
+        public string LogDirectoryPath { get; private set; }
 
         /// <summary>
         /// Gets the path to the application configuration root directory
         /// </summary>
         /// <value>The configuration directory path.</value>
-        public string ConfigurationDirectoryPath
-        {
-            get
-            {
-                if (string.IsNullOrEmpty(_configurationDirectoryPath))
-                {
-                    _configurationDirectoryPath = Path.Combine(ProgramDataPath, "config");
-
-                    Directory.CreateDirectory(_configurationDirectoryPath);
-                }
-
-                return _configurationDirectoryPath;
-            }
-            set => _configurationDirectoryPath = value;
-        }
+        public string ConfigurationDirectoryPath { get; private set; }
 
         /// <summary>
         /// Gets the path to the system configuration file
@@ -139,29 +96,11 @@ namespace Emby.Server.Implementations.AppBase
         /// <value>The system configuration file path.</value>
         public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
 
-        /// <summary>
-        /// The _cache directory
-        /// </summary>
-        private string _cachePath;
         /// <summary>
         /// Gets the folder path to the cache directory
         /// </summary>
         /// <value>The cache directory.</value>
-        public string CachePath
-        {
-            get
-            {
-                if (string.IsNullOrEmpty(_cachePath))
-                {
-                    _cachePath = Path.Combine(ProgramDataPath, "cache");
-
-                    Directory.CreateDirectory(_cachePath);
-                }
-
-                return _cachePath;
-            }
-            set => _cachePath = value;
-        }
+        public string CachePath { get; set; }
 
         /// <summary>
         /// Gets the folder path to the temp directory within the cache folder

+ 16 - 3
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -171,16 +171,29 @@ namespace Emby.Server.Implementations.AppBase
         private void UpdateCachePath()
         {
             string cachePath;
-
+            // If the configuration file has no entry (i.e. not set in UI)
             if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath))
             {
-                cachePath = null;
+                // If the current live configuration has no entry (i.e. not set on CLI/envvars, during startup)
+                if (string.IsNullOrWhiteSpace(((BaseApplicationPaths)CommonApplicationPaths).CachePath))
+                {
+                    // Set cachePath to a default value under ProgramDataPath
+                    cachePath = Path.Combine(((BaseApplicationPaths)CommonApplicationPaths).ProgramDataPath, "cache");
+                }
+                else
+                {
+                    // Set cachePath to the existing live value; will require restart if UI value is removed (but not replaced)
+                    // TODO: Figure out how to re-grab this from the CLI/envvars while running
+                    cachePath = ((BaseApplicationPaths)CommonApplicationPaths).CachePath;
+                }
             }
             else
             {
-                cachePath = Path.Combine(CommonConfiguration.CachePath, "cache");
+                // Set cachePath to the new UI-set value
+                cachePath = CommonConfiguration.CachePath;
             }
 
+            Logger.LogInformation("Setting cache path to " + cachePath);
             ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
         }
 

+ 139 - 401
Emby.Server.Implementations/ApplicationHost.cs

@@ -105,6 +105,7 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.DependencyInjection;
 using ServiceStack;
 using ServiceStack.Text.Jsv;
 using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
@@ -202,7 +203,7 @@ namespace Emby.Server.Implementations
         /// Gets all concrete types.
         /// </summary>
         /// <value>All concrete types.</value>
-        public Tuple<Type, string>[] AllConcreteTypes { get; protected set; }
+        public Type[] AllConcreteTypes { get; protected set; }
 
         /// <summary>
         /// The disposable parts
@@ -219,8 +220,6 @@ namespace Emby.Server.Implementations
 
         protected IEnvironmentInfo EnvironmentInfo { get; set; }
 
-        private IBlurayExaminer BlurayExaminer { get; set; }
-
         public PackageVersionClass SystemUpdateLevel
         {
             get
@@ -232,12 +231,7 @@ namespace Emby.Server.Implementations
             }
         }
 
-        public virtual string OperatingSystemDisplayName => EnvironmentInfo.OperatingSystemName;
-
-        /// <summary>
-        /// The container
-        /// </summary>
-        protected readonly SimpleInjector.Container Container = new SimpleInjector.Container();
+        protected IServiceProvider _serviceProvider;
 
         /// <summary>
         /// Gets the server configuration manager.
@@ -309,7 +303,6 @@ namespace Emby.Server.Implementations
         /// <value>The user data repository.</value>
         private IUserDataManager UserDataManager { get; set; }
         private IUserRepository UserRepository { get; set; }
-        internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; }
         internal SqliteItemRepository ItemRepository { get; set; }
 
         private INotificationManager NotificationManager { get; set; }
@@ -453,138 +446,58 @@ namespace Emby.Server.Implementations
         /// <value>The name.</value>
         public string Name => ApplicationProductName;
 
-        private static Tuple<Assembly, string> GetAssembly(Type type)
-        {
-            var assembly = type.GetTypeInfo().Assembly;
-
-            return new Tuple<Assembly, string>(assembly, null);
-        }
-
-        public virtual IStreamHelper CreateStreamHelper()
-        {
-            return new StreamHelper();
-        }
-
         /// <summary>
-        /// Creates an instance of type and resolves all constructor dependancies
+        /// Creates an instance of type and resolves all constructor dependencies
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>System.Object.</returns>
         public object CreateInstance(Type type)
-        {
-            return Container.GetInstance(type);
-        }
+            => ActivatorUtilities.CreateInstance(_serviceProvider, type);
+
+        /// <summary>
+        /// Creates an instance of type and resolves all constructor dependencies
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>System.Object.</returns>
+        public T CreateInstance<T>()
+            => ActivatorUtilities.CreateInstance<T>(_serviceProvider);
 
         /// <summary>
         /// Creates the instance safe.
         /// </summary>
         /// <param name="typeInfo">The type information.</param>
         /// <returns>System.Object.</returns>
-        protected object CreateInstanceSafe(Tuple<Type, string> typeInfo)
+        protected object CreateInstanceSafe(Type type)
         {
-            var type = typeInfo.Item1;
-
             try
             {
-                return Container.GetInstance(type);
+                Logger.LogWarning("Creating instance of {Type}", type);
+                return ActivatorUtilities.CreateInstance(_serviceProvider, type);
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error creating {type}", type.FullName);
-                // Don't blow up in release mode
+                Logger.LogError(ex, "Error creating {Type}", type);
                 return null;
             }
         }
 
-        /// <summary>
-        /// Registers the specified obj.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="obj">The obj.</param>
-        /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param>
-        protected void RegisterSingleInstance<T>(T obj, bool manageLifetime = true)
-            where T : class
-        {
-            Container.RegisterInstance<T>(obj);
-
-            if (manageLifetime)
-            {
-                var disposable = obj as IDisposable;
-
-                if (disposable != null)
-                {
-                    DisposableParts.Add(disposable);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Registers the single instance.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="func">The func.</param>
-        protected void RegisterSingleInstance<T>(Func<T> func)
-            where T : class
-        {
-            Container.RegisterSingleton(func);
-        }
-
         /// <summary>
         /// Resolves this instance.
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <returns>``0.</returns>
-        public T Resolve<T>()
-        {
-            return (T)Container.GetRegistration(typeof(T), true).GetInstance();
-        }
-
-        /// <summary>
-        /// Resolves this instance.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <returns>``0.</returns>
-        public T TryResolve<T>()
-        {
-            var result = Container.GetRegistration(typeof(T), false);
-
-            if (result == null)
-            {
-                return default(T);
-            }
-            return (T)result.GetInstance();
-        }
-
-        /// <summary>
-        /// Loads the assembly.
-        /// </summary>
-        /// <param name="file">The file.</param>
-        /// <returns>Assembly.</returns>
-        protected Tuple<Assembly, string> LoadAssembly(string file)
-        {
-            try
-            {
-                var assembly = Assembly.Load(File.ReadAllBytes(file));
-
-                return new Tuple<Assembly, string>(assembly, file);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading assembly {File}", file);
-                return null;
-            }
-        }
+        public T Resolve<T>() => _serviceProvider.GetService<T>();
 
         /// <summary>
         /// Gets the export types.
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <returns>IEnumerable{Type}.</returns>
-        public IEnumerable<Tuple<Type, string>> GetExportTypes<T>()
+        public IEnumerable<Type> GetExportTypes<T>()
         {
             var currentType = typeof(T);
 
-            return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i.Item1));
+            return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
         }
 
         /// <summary>
@@ -596,9 +509,10 @@ namespace Emby.Server.Implementations
         public IEnumerable<T> GetExports<T>(bool manageLifetime = true)
         {
             var parts = GetExportTypes<T>()
-                .Select(CreateInstanceSafe)
+                .Select(x => CreateInstanceSafe(x))
                 .Where(i => i != null)
-                .Cast<T>();
+                .Cast<T>()
+                .ToList(); // Convert to list so this isn't executed for each iteration
 
             if (manageLifetime)
             {
@@ -611,33 +525,6 @@ namespace Emby.Server.Implementations
             return parts;
         }
 
-        public List<Tuple<T, string>> GetExportsWithInfo<T>(bool manageLifetime = true)
-        {
-            var parts = GetExportTypes<T>()
-                .Select(i =>
-                {
-                    var obj = CreateInstanceSafe(i);
-
-                    if (obj == null)
-                    {
-                        return null;
-                    }
-                    return new Tuple<T, string>((T)obj, i.Item2);
-                })
-                .Where(i => i != null)
-                .ToList();
-
-            if (manageLifetime)
-            {
-                lock (DisposableParts)
-                {
-                    DisposableParts.AddRange(parts.Select(i => i.Item1).OfType<IDisposable>());
-                }
-            }
-
-            return parts;
-        }
-
         /// <summary>
         /// Runs the startup tasks.
         /// </summary>
@@ -691,7 +578,7 @@ namespace Emby.Server.Implementations
             }
         }
 
-        public async Task Init()
+        public async Task Init(IServiceCollection serviceCollection)
         {
             HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
             HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -721,7 +608,7 @@ namespace Emby.Server.Implementations
 
             SetHttpLimit();
 
-            await RegisterResources();
+            await RegisterResources(serviceCollection);
 
             FindParts();
         }
@@ -736,104 +623,103 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Registers resources that classes will depend on
         /// </summary>
-        protected async Task RegisterResources()
+        protected async Task RegisterResources(IServiceCollection serviceCollection)
         {
-            RegisterSingleInstance(ConfigurationManager);
-            RegisterSingleInstance<IApplicationHost>(this);
+            serviceCollection.AddSingleton(ConfigurationManager);
+            serviceCollection.AddSingleton<IApplicationHost>(this);
+
+            serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            RegisterSingleInstance<IApplicationPaths>(ApplicationPaths);
 
-            RegisterSingleInstance(JsonSerializer);
+            serviceCollection.AddSingleton(JsonSerializer);
 
-            RegisterSingleInstance(LoggerFactory, false);
-            RegisterSingleInstance(Logger);
+            serviceCollection.AddSingleton(LoggerFactory);
+            serviceCollection.AddLogging();
+            serviceCollection.AddSingleton(Logger);
 
-            RegisterSingleInstance(EnvironmentInfo);
+            serviceCollection.AddSingleton(EnvironmentInfo);
 
-            RegisterSingleInstance(FileSystemManager);
+            serviceCollection.AddSingleton(FileSystemManager);
 
             HttpClient = CreateHttpClient();
-            RegisterSingleInstance(HttpClient);
+            serviceCollection.AddSingleton(HttpClient);
 
-            RegisterSingleInstance(NetworkManager);
+            serviceCollection.AddSingleton(NetworkManager);
 
             IsoManager = new IsoManager();
-            RegisterSingleInstance(IsoManager);
+            serviceCollection.AddSingleton(IsoManager);
 
             TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LoggerFactory, FileSystemManager);
-            RegisterSingleInstance(TaskManager);
+            serviceCollection.AddSingleton(TaskManager);
 
-            RegisterSingleInstance(XmlSerializer);
+            serviceCollection.AddSingleton(XmlSerializer);
 
             ProcessFactory = new ProcessFactory();
-            RegisterSingleInstance(ProcessFactory);
+            serviceCollection.AddSingleton(ProcessFactory);
 
-            var streamHelper = CreateStreamHelper();
-            ApplicationHost.StreamHelper = streamHelper;
-            RegisterSingleInstance(streamHelper);
+            ApplicationHost.StreamHelper = new StreamHelper();
+            serviceCollection.AddSingleton(StreamHelper);
 
-            RegisterSingleInstance(CryptographyProvider);
+            serviceCollection.AddSingleton(CryptographyProvider);
 
             SocketFactory = new SocketFactory();
-            RegisterSingleInstance(SocketFactory);
+            serviceCollection.AddSingleton(SocketFactory);
 
             InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, PackageRuntime);
-            RegisterSingleInstance(InstallationManager);
+            serviceCollection.AddSingleton(InstallationManager);
 
             ZipClient = new ZipClient();
-            RegisterSingleInstance(ZipClient);
+            serviceCollection.AddSingleton(ZipClient);
 
             HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, CreateBrotliCompressor());
-            RegisterSingleInstance(HttpResultFactory);
+            serviceCollection.AddSingleton(HttpResultFactory);
 
-            RegisterSingleInstance<IServerApplicationHost>(this);
-            RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths);
+            serviceCollection.AddSingleton<IServerApplicationHost>(this);
+            serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
 
-            RegisterSingleInstance(ServerConfigurationManager);
+            serviceCollection.AddSingleton(ServerConfigurationManager);
 
-            IAssemblyInfo assemblyInfo = new AssemblyInfo();
-            RegisterSingleInstance(assemblyInfo);
+            var assemblyInfo = new AssemblyInfo();
+            serviceCollection.AddSingleton<IAssemblyInfo>(assemblyInfo);
 
             LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LoggerFactory);
             await LocalizationManager.LoadAll();
-            RegisterSingleInstance<ILocalizationManager>(LocalizationManager);
+            serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
 
-            BlurayExaminer = new BdInfoExaminer(FileSystemManager);
-            RegisterSingleInstance(BlurayExaminer);
+            serviceCollection.AddSingleton<IBlurayExaminer>(new BdInfoExaminer(FileSystemManager));
 
-            RegisterSingleInstance<IXmlReaderSettingsFactory>(new XmlReaderSettingsFactory());
+            serviceCollection.AddSingleton<IXmlReaderSettingsFactory>(new XmlReaderSettingsFactory());
 
             UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
-            RegisterSingleInstance(UserDataManager);
+            serviceCollection.AddSingleton(UserDataManager);
 
             UserRepository = GetUserRepository();
             // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it
-            RegisterSingleInstance(UserRepository);
+            serviceCollection.AddSingleton(UserRepository);
 
             var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
-            DisplayPreferencesRepository = displayPreferencesRepo;
-            RegisterSingleInstance(DisplayPreferencesRepository);
+            serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
 
             ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, assemblyInfo);
-            RegisterSingleInstance<IItemRepository>(ItemRepository);
+            serviceCollection.AddSingleton<IItemRepository>(ItemRepository);
 
             AuthenticationRepository = GetAuthenticationRepository();
-            RegisterSingleInstance(AuthenticationRepository);
+            serviceCollection.AddSingleton(AuthenticationRepository);
 
             UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
-            RegisterSingleInstance(UserManager);
+            serviceCollection.AddSingleton(UserManager);
 
             LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager);
-            RegisterSingleInstance(LibraryManager);
+            serviceCollection.AddSingleton(LibraryManager);
 
             // TODO wtaylor: investigate use of second music manager
             var musicManager = new MusicManager(LibraryManager);
-            RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager));
+            serviceCollection.AddSingleton<IMusicManager>(new MusicManager(LibraryManager));
 
             LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager, EnvironmentInfo);
-            RegisterSingleInstance(LibraryMonitor);
+            serviceCollection.AddSingleton(LibraryMonitor);
 
-            RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LoggerFactory, LibraryManager, UserManager));
+            serviceCollection.AddSingleton<ISearchEngine>(new SearchEngine(LoggerFactory, LibraryManager, UserManager));
 
             CertificateInfo = GetCertificateInfo(true);
             Certificate = GetCertificate(CertificateInfo);
@@ -848,81 +734,82 @@ namespace Emby.Server.Implementations
                 GetParseFn);
 
             HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
-            RegisterSingleInstance(HttpServer);
+            serviceCollection.AddSingleton(HttpServer);
 
             ImageProcessor = GetImageProcessor();
-            RegisterSingleInstance(ImageProcessor);
+            serviceCollection.AddSingleton(ImageProcessor);
 
             TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
-            RegisterSingleInstance(TVSeriesManager);
+            serviceCollection.AddSingleton(TVSeriesManager);
 
             var encryptionManager = new EncryptionManager();
-            RegisterSingleInstance<IEncryptionManager>(encryptionManager);
+            serviceCollection.AddSingleton<IEncryptionManager>(encryptionManager);
 
             DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
-            RegisterSingleInstance(DeviceManager);
+            serviceCollection.AddSingleton(DeviceManager);
 
             MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
-            RegisterSingleInstance(MediaSourceManager);
+            serviceCollection.AddSingleton(MediaSourceManager);
 
             SubtitleManager = new SubtitleManager(LoggerFactory, FileSystemManager, LibraryMonitor, MediaSourceManager, LocalizationManager);
-            RegisterSingleInstance(SubtitleManager);
+            serviceCollection.AddSingleton(SubtitleManager);
 
             ProviderManager = new ProviderManager(HttpClient, SubtitleManager, ServerConfigurationManager, LibraryMonitor, LoggerFactory, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer);
-            RegisterSingleInstance(ProviderManager);
+            serviceCollection.AddSingleton(ProviderManager);
 
             DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
-            RegisterSingleInstance(DtoService);
+            serviceCollection.AddSingleton(DtoService);
 
             ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
-            RegisterSingleInstance(ChannelManager);
+            serviceCollection.AddSingleton(ChannelManager);
 
             SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager);
-            RegisterSingleInstance(SessionManager);
+            serviceCollection.AddSingleton(SessionManager);
 
-            var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo);
-            RegisterSingleInstance<IDlnaManager>(dlnaManager);
+            serviceCollection.AddSingleton<IDlnaManager>(
+                new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this, assemblyInfo));
 
             CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
-            RegisterSingleInstance(CollectionManager);
+            serviceCollection.AddSingleton(CollectionManager);
 
             PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LoggerFactory, UserManager, ProviderManager);
-            RegisterSingleInstance(PlaylistManager);
+            serviceCollection.AddSingleton(PlaylistManager);
 
             LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager);
-            RegisterSingleInstance(LiveTvManager);
+            serviceCollection.AddSingleton(LiveTvManager);
 
             UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
-            RegisterSingleInstance(UserViewManager);
+            serviceCollection.AddSingleton(UserViewManager);
 
             NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager);
-            RegisterSingleInstance(NotificationManager);
+            serviceCollection.AddSingleton(NotificationManager);
 
-            RegisterSingleInstance<IDeviceDiscovery>(new DeviceDiscovery(LoggerFactory, ServerConfigurationManager, SocketFactory));
+            serviceCollection.AddSingleton<IDeviceDiscovery>(
+                new DeviceDiscovery(LoggerFactory, ServerConfigurationManager, SocketFactory));
 
             ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository);
-            RegisterSingleInstance(ChapterManager);
+            serviceCollection.AddSingleton(ChapterManager);
 
-            RegisterMediaEncoder(assemblyInfo);
+            RegisterMediaEncoder(serviceCollection);
 
             EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager);
-            RegisterSingleInstance(EncodingManager);
+            serviceCollection.AddSingleton(EncodingManager);
 
             var activityLogRepo = GetActivityLogRepository();
-            RegisterSingleInstance(activityLogRepo);
-            RegisterSingleInstance<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager));
+            serviceCollection.AddSingleton(activityLogRepo);
+            serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager));
 
             var authContext = new AuthorizationContext(AuthenticationRepository, UserManager);
-            RegisterSingleInstance<IAuthorizationContext>(authContext);
-            RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
+            serviceCollection.AddSingleton<IAuthorizationContext>(authContext);
+            serviceCollection.AddSingleton<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
 
             AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, SessionManager, NetworkManager);
-            RegisterSingleInstance(AuthService);
+            serviceCollection.AddSingleton(AuthService);
 
             SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory);
-            RegisterSingleInstance(SubtitleEncoder);
+            serviceCollection.AddSingleton(SubtitleEncoder);
 
-            RegisterSingleInstance(CreateResourceFileManager());
+            serviceCollection.AddSingleton(CreateResourceFileManager());
 
             displayPreferencesRepo.Initialize();
 
@@ -935,6 +822,8 @@ namespace Emby.Server.Implementations
             ((UserDataManager)UserDataManager).Repository = userDataRepo;
             ItemRepository.Initialize(userDataRepo, UserManager);
             ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
+
+            _serviceProvider = serviceCollection.BuildServiceProvider();
         }
 
         protected virtual IBrotliCompressor CreateBrotliCompressor()
@@ -1066,7 +955,7 @@ namespace Emby.Server.Implementations
         /// Registers the media encoder.
         /// </summary>
         /// <returns>Task.</returns>
-        private void RegisterMediaEncoder(IAssemblyInfo assemblyInfo)
+        private void RegisterMediaEncoder(IServiceCollection serviceCollection)
         {
             string encoderPath = null;
             string probePath = null;
@@ -1098,7 +987,7 @@ namespace Emby.Server.Implementations
                 5000);
 
             MediaEncoder = mediaEncoder;
-            RegisterSingleInstance(MediaEncoder);
+            serviceCollection.AddSingleton(MediaEncoder);
         }
 
         /// <summary>
@@ -1174,7 +1063,10 @@ namespace Emby.Server.Implementations
             }
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
-            Plugins = GetExportsWithInfo<IPlugin>().Select(LoadPlugin).Where(i => i != null).ToArray();
+            Plugins = GetExports<IPlugin>()
+                        .Select(LoadPlugin)
+                        .Where(i => i != null)
+                        .ToArray();
 
             HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>());
 
@@ -1208,19 +1100,15 @@ namespace Emby.Server.Implementations
             IsoManager.AddParts(GetExports<IIsoMounter>());
         }
 
-        private IPlugin LoadPlugin(Tuple<IPlugin, string> info)
+        private IPlugin LoadPlugin(IPlugin plugin)
         {
-            var plugin = info.Item1;
-            var assemblyFilePath = info.Item2;
-
             try
             {
-                var assemblyPlugin = plugin as IPluginAssembly;
-
-                if (assemblyPlugin != null)
+                if (plugin is IPluginAssembly assemblyPlugin)
                 {
                     var assembly = plugin.GetType().Assembly;
                     var assemblyName = assembly.GetName();
+                    var assemblyFilePath = assembly.Location;
 
                     var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
 
@@ -1264,78 +1152,15 @@ namespace Emby.Server.Implementations
         {
             Logger.LogInformation("Loading assemblies");
 
-            var assemblyInfos = GetComposablePartAssemblies();
-
-            foreach (var assemblyInfo in assemblyInfos)
-            {
-                var assembly = assemblyInfo.Item1;
-                var path = assemblyInfo.Item2;
-
-                if (path == null)
-                {
-                    Logger.LogInformation("Loading {assemblyName}", assembly.FullName);
-                }
-                else
+            AllConcreteTypes = GetComposablePartAssemblies()
+                .SelectMany(x => x.ExportedTypes)
+                .Where(type =>
                 {
-                    Logger.LogInformation("Loading {assemblyName} from {path}", assembly.FullName, path);
-                }
-            }
-
-            AllConcreteTypes = assemblyInfos
-                .SelectMany(GetTypes)
-                .Where(info =>
-                {
-                    var t = info.Item1;
-                    return t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType;
+                    return type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType;
                 })
                 .ToArray();
         }
 
-        /// <summary>
-        /// Gets a list of types within an assembly
-        /// This will handle situations that would normally throw an exception - such as a type within the assembly that depends on some other non-existant reference
-        /// </summary>
-        protected List<Tuple<Type, string>> GetTypes(Tuple<Assembly, string> assemblyInfo)
-        {
-            if (assemblyInfo == null)
-            {
-                return new List<Tuple<Type, string>>();
-            }
-
-            var assembly = assemblyInfo.Item1;
-
-            try
-            {
-                // This null checking really shouldn't be needed but adding it due to some
-                // unhandled exceptions in mono 5.0 that are a little hard to hunt down
-                var types = assembly.GetTypes() ?? new Type[] { };
-                return types.Where(t => t != null).Select(i => new Tuple<Type, string>(i, assemblyInfo.Item2)).ToList();
-            }
-            catch (ReflectionTypeLoadException ex)
-            {
-                if (ex.LoaderExceptions != null)
-                {
-                    foreach (var loaderException in ex.LoaderExceptions)
-                    {
-                        if (loaderException != null)
-                        {
-                            Logger.LogError("LoaderException: " + loaderException.Message);
-                        }
-                    }
-                }
-
-                // If it fails we can still get a list of the Types it was able to resolve
-                var types = ex.Types ?? new Type[] { };
-                return types.Where(t => t != null).Select(i => new Tuple<Type, string>(i, assemblyInfo.Item2)).ToList();
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error loading types from assembly");
-
-                return new List<Tuple<Type, string>>();
-            }
-        }
-
         private CertificateInfo CertificateInfo { get; set; }
         protected X509Certificate Certificate { get; private set; }
 
@@ -1546,150 +1371,63 @@ namespace Emby.Server.Implementations
         /// Gets the composable part assemblies.
         /// </summary>
         /// <returns>IEnumerable{Assembly}.</returns>
-        protected List<Tuple<Assembly, string>> GetComposablePartAssemblies()
+        protected IEnumerable<Assembly> GetComposablePartAssemblies()
         {
-            var list = GetPluginAssemblies(ApplicationPaths.PluginsPath);
-
-            // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that
-            // This will prevent the .dll file from getting locked, and allow us to replace it when needed
+            if (Directory.Exists(ApplicationPaths.PluginsPath))
+            {
+                foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly))
+                {
+                    Logger.LogInformation("Loading assembly {Path}", file);
+                    yield return Assembly.LoadFrom(file);
+                }
+            }
 
             // Include composable parts in the Api assembly
-            list.Add(GetAssembly(typeof(ApiEntryPoint)));
+            yield return typeof(ApiEntryPoint).Assembly;
 
             // Include composable parts in the Dashboard assembly
-            list.Add(GetAssembly(typeof(DashboardService)));
+            yield return typeof(DashboardService).Assembly;
 
             // Include composable parts in the Model assembly
-            list.Add(GetAssembly(typeof(SystemInfo)));
+            yield return typeof(SystemInfo).Assembly;
 
             // Include composable parts in the Common assembly
-            list.Add(GetAssembly(typeof(IApplicationHost)));
+            yield return typeof(IApplicationHost).Assembly;
 
             // Include composable parts in the Controller assembly
-            list.Add(GetAssembly(typeof(IServerApplicationHost)));
+            yield return typeof(IServerApplicationHost).Assembly;
 
             // Include composable parts in the Providers assembly
-            list.Add(GetAssembly(typeof(ProviderUtils)));
+            yield return typeof(ProviderUtils).Assembly;
 
             // Include composable parts in the Photos assembly
-            list.Add(GetAssembly(typeof(PhotoProvider)));
+            yield return typeof(PhotoProvider).Assembly;
 
             // Emby.Server implementations
-            list.Add(GetAssembly(typeof(InstallationManager)));
+            yield return typeof(InstallationManager).Assembly;
 
             // MediaEncoding
-            list.Add(GetAssembly(typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder)));
+            yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly;
 
             // Dlna
-            list.Add(GetAssembly(typeof(DlnaEntryPoint)));
+            yield return typeof(DlnaEntryPoint).Assembly;
 
             // Local metadata
-            list.Add(GetAssembly(typeof(BoxSetXmlSaver)));
+            yield return typeof(BoxSetXmlSaver).Assembly;
 
             // Notifications
-            list.Add(GetAssembly(typeof(NotificationManager)));
+            yield return typeof(NotificationManager).Assembly;
 
             // Xbmc
-            list.Add(GetAssembly(typeof(ArtistNfoProvider)));
-
-            list.AddRange(GetAssembliesWithPartsInternal().Select(i => new Tuple<Assembly, string>(i, null)));
+            yield return typeof(ArtistNfoProvider).Assembly;
 
-            return list.ToList();
-        }
-
-        protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
-
-        private List<Tuple<Assembly, string>> GetPluginAssemblies(string path)
-        {
-            try
+            foreach (var i in GetAssembliesWithPartsInternal())
             {
-                return FilterAssembliesToLoad(Directory.EnumerateFiles(path, "*.dll", SearchOption.TopDirectoryOnly))
-                    .Select(LoadAssembly)
-                    .Where(a => a != null)
-                    .ToList();
-            }
-            catch (DirectoryNotFoundException)
-            {
-                return new List<Tuple<Assembly, string>>();
+                yield return i;
             }
         }
 
-        private IEnumerable<string> FilterAssembliesToLoad(IEnumerable<string> paths)
-        {
-
-            var exclude = new[]
-            {
-                "mbplus.dll",
-                "mbintros.dll",
-                "embytv.dll",
-                "Messenger.dll",
-                "Messages.dll",
-                "MediaBrowser.Plugins.TvMazeProvider.dll",
-                "MBBookshelf.dll",
-                "MediaBrowser.Channels.Adult.YouJizz.dll",
-                "MediaBrowser.Channels.Vine-co.dll",
-                "MediaBrowser.Plugins.Vimeo.dll",
-                "MediaBrowser.Channels.Vevo.dll",
-                "MediaBrowser.Plugins.Twitch.dll",
-                "MediaBrowser.Channels.SvtPlay.dll",
-                "MediaBrowser.Plugins.SoundCloud.dll",
-                "MediaBrowser.Plugins.SnesBox.dll",
-                "MediaBrowser.Plugins.RottenTomatoes.dll",
-                "MediaBrowser.Plugins.Revision3.dll",
-                "MediaBrowser.Plugins.NesBox.dll",
-                "MBChapters.dll",
-                "MediaBrowser.Channels.LeagueOfLegends.dll",
-                "MediaBrowser.Plugins.ADEProvider.dll",
-                "MediaBrowser.Channels.BallStreams.dll",
-                "MediaBrowser.Channels.Adult.Beeg.dll",
-                "ChannelDownloader.dll",
-                "Hamstercat.Emby.EmbyBands.dll",
-                "EmbyTV.dll",
-                "MediaBrowser.Channels.HitboxTV.dll",
-                "MediaBrowser.Channels.HockeyStreams.dll",
-                "MediaBrowser.Plugins.ITV.dll",
-                "MediaBrowser.Plugins.Lastfm.dll",
-                "ServerRestart.dll",
-                "MediaBrowser.Plugins.NotifyMyAndroidNotifications.dll",
-                "MetadataViewer.dll"
-            };
-
-            var minRequiredVersions = new Dictionary<string, Version>(StringComparer.OrdinalIgnoreCase)
-            {
-                { "moviethemesongs.dll", new Version(1, 6) },
-                { "themesongs.dll", new Version(1, 2) }
-            };
-
-            return paths.Where(path =>
-            {
-                var filename = Path.GetFileName(path);
-                if (exclude.Contains(filename ?? string.Empty, StringComparer.OrdinalIgnoreCase))
-                {
-                    return false;
-                }
-
-                if (minRequiredVersions.TryGetValue(filename, out Version minRequiredVersion))
-                {
-                    try
-                    {
-                        var version = Version.Parse(FileVersionInfo.GetVersionInfo(path).FileVersion);
-
-                        if (version < minRequiredVersion)
-                        {
-                            Logger.LogInformation("Not loading {filename} {version} because the minimum supported version is {minRequiredVersion}. Please update to the newer version", filename, version, minRequiredVersion);
-                            return false;
-                        }
-                    }
-                    catch (Exception ex)
-                    {
-                        Logger.LogError(ex, "Error getting version number from {path}", path);
-
-                        return false;
-                    }
-                }
-                return true;
-            });
-        }
+        protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
 
         /// <summary>
         /// Gets the system status.
@@ -1718,7 +1456,7 @@ namespace Emby.Server.Implementations
                 SupportsHttps = SupportsHttps,
                 HttpsPortNumber = HttpsPort,
                 OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(),
-                OperatingSystemDisplayName = OperatingSystemDisplayName,
+                OperatingSystemDisplayName = EnvironmentInfo.OperatingSystemName,
                 CanSelfRestart = CanSelfRestart,
                 CanSelfUpdate = CanSelfUpdate,
                 CanLaunchWebBrowser = CanLaunchWebBrowser,
@@ -1788,7 +1526,7 @@ namespace Emby.Server.Implementations
 
         public async Task<string> GetWanApiUrl(CancellationToken cancellationToken)
         {
-            var url = "http://ipv4.icanhazip.com";
+            const string url = "http://ipv4.icanhazip.com";
             try
             {
                 using (var response = await HttpClient.Get(new HttpRequestOptions

+ 1 - 1
Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs

@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Channels
 {
-    class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
+    public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
     {
         private readonly IChannelManager _channelManager;
         private readonly IUserManager _userManager;

+ 3 - 3
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -22,11 +22,11 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.4.0" />
     <PackageReference Include="sharpcompress" Version="0.22.0" />
-    <PackageReference Include="SimpleInjector" Version="4.4.2" />
-    <PackageReference Include="SQLitePCL.pretty.core" Version="1.1.8" />
-    <PackageReference Include="SQLitePCLRaw.core" Version="1.1.11" />
+    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" />
     <PackageReference Include="UTF.Unknown" Version="1.0.0-beta1" />
   </ItemGroup>
 

+ 1 - 1
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
 {
-    class UserDataChangeNotifier : IServerEntryPoint
+    public class UserDataChangeNotifier : IServerEntryPoint
     {
         private readonly ISessionManager _sessionManager;
         private readonly ILogger _logger;

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs

@@ -9,7 +9,7 @@ using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
-    class SpecialFolderResolver : FolderResolver<Folder>
+    public class SpecialFolderResolver : FolderResolver<Folder>
     {
         private readonly IFileSystem _fileSystem;
         private readonly IServerApplicationPaths _appPaths;

+ 39 - 35
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -155,56 +155,56 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null;
 
             string numberString = null;
+            string attributeValue;
+            double doubleValue;
 
-            // Check for channel number with the format from SatIp
-            // #EXTINF:0,84. VOX Schweiz
-            // #EXTINF:0,84.0 - VOX Schweiz
-            if (!string.IsNullOrWhiteSpace(nameInExtInf))
+            if (attributes.TryGetValue("tvg-chno", out attributeValue))
             {
-                var numberIndex = nameInExtInf.IndexOf(' ');
-                if (numberIndex > 0)
+                if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
                 {
-                    var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
-
-                    if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
-                    {
-                        numberString = numberPart;
-                    }
+                    numberString = attributeValue;
                 }
             }
 
-            if (!string.IsNullOrWhiteSpace(numberString))
-            {
-                numberString = numberString.Trim();
-            }
-
             if (!IsValidChannelNumber(numberString))
             {
-                if (attributes.TryGetValue("tvg-id", out string value))
+                if (attributes.TryGetValue("tvg-id", out attributeValue))
                 {
-                    if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var doubleValue))
+                    if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
                     {
-                        numberString = value;
+                        numberString = attributeValue;
+                    }
+                    else if (attributes.TryGetValue("channel-id", out attributeValue))
+                    {
+                        if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue))
+                        {
+                            numberString = attributeValue;
+                        }
                     }
                 }
-            }
 
-            if (!string.IsNullOrWhiteSpace(numberString))
-            {
-                numberString = numberString.Trim();
-            }
-
-            if (!IsValidChannelNumber(numberString))
-            {
-                if (attributes.TryGetValue("channel-id", out string value))
+                if (String.IsNullOrWhiteSpace(numberString))
                 {
-                    numberString = value;
+                    // Using this as a fallback now as this leads to Problems with channels like "5 USA"
+                    // where 5 isnt ment to be the channel number
+                    // Check for channel number with the format from SatIp
+                    // #EXTINF:0,84. VOX Schweiz
+                    // #EXTINF:0,84.0 - VOX Schweiz
+                    if (!string.IsNullOrWhiteSpace(nameInExtInf))
+                    {
+                        var numberIndex = nameInExtInf.IndexOf(' ');
+                        if (numberIndex > 0)
+                        {
+                            var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
+
+                            if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
+                            {
+                                numberString = numberPart;
+                            }
+                        }
+                    }
                 }
-            }
 
-            if (!string.IsNullOrWhiteSpace(numberString))
-            {
-                numberString = numberString.Trim();
             }
 
             if (!IsValidChannelNumber(numberString))
@@ -212,7 +212,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 numberString = null;
             }
 
-            if (string.IsNullOrWhiteSpace(numberString))
+            if (!string.IsNullOrWhiteSpace(numberString))
+            {
+                numberString = numberString.Trim();
+            }
+            else
             {
                 if (string.IsNullOrWhiteSpace(mediaUrl))
                 {

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -94,7 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             var now = DateTime.UtcNow;
 
-            var _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
+            _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
 
             //OpenedMediaSource.Protocol = MediaProtocol.File;
             //OpenedMediaSource.Path = tempFile;

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
     /// <summary>
     /// Class ChapterImagesTask
     /// </summary>
-    class ChapterImagesTask : IScheduledTask
+    public class ChapterImagesTask : IScheduledTask
     {
         /// <summary>
         /// The _logger

+ 5 - 10
Emby.Server.Implementations/ServerApplicationPaths.cs

@@ -15,21 +15,17 @@ namespace Emby.Server.Implementations
         /// </summary>
         public ServerApplicationPaths(
             string programDataPath,
-            string appFolderPath,
-            string applicationResourcesPath,
-            string logDirectoryPath = null,
-            string configurationDirectoryPath = null,
-            string cacheDirectoryPath = null)
+            string logDirectoryPath,
+            string configurationDirectoryPath,
+            string cacheDirectoryPath)
             : base(programDataPath,
-                appFolderPath,
                 logDirectoryPath,
                 configurationDirectoryPath,
                 cacheDirectoryPath)
         {
-            ApplicationResourcesPath = applicationResourcesPath;
         }
 
-        public string ApplicationResourcesPath { get; private set; }
+        public string ApplicationResourcesPath { get; } = AppContext.BaseDirectory;
 
         /// <summary>
         /// Gets the path to the base root media directory
@@ -148,7 +144,6 @@ namespace Emby.Server.Implementations
             set => _internalMetadataPath = value;
         }
 
-        private const string _virtualInternalMetadataPath = "%MetadataPath%";
-        public string VirtualInternalMetadataPath => _virtualInternalMetadataPath;
+        public string VirtualInternalMetadataPath { get; } = "%MetadataPath%";
     }
 }

+ 1 - 1
Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs

@@ -6,7 +6,7 @@ using MediaBrowser.Model.Querying;
 
 namespace Emby.Server.Implementations.Sorting
 {
-    class AiredEpisodeOrderComparer : IBaseItemComparer
+    public class AiredEpisodeOrderComparer : IBaseItemComparer
     {
         /// <summary>
         /// Compares the specified x.

+ 1 - 1
Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs

@@ -5,7 +5,7 @@ using MediaBrowser.Model.Querying;
 
 namespace Emby.Server.Implementations.Sorting
 {
-    class SeriesSortNameComparer : IBaseItemComparer
+    public class SeriesSortNameComparer : IBaseItemComparer
     {
         /// <summary>
         /// Compares the specified x.

+ 1 - 1
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -282,7 +282,7 @@ namespace Jellyfin.Drawing.Skia
                     var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack);
 
                     // decode
-                    var _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
+                    _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels());
 
                     origin = codec.EncodedOrigin;
 

+ 6 - 5
Jellyfin.Server/CoreAppHost.cs

@@ -18,15 +18,17 @@ namespace Jellyfin.Server
 
         public override bool CanSelfRestart => StartupOptions.RestartPath != null;
 
+        protected override bool SupportsDualModeSockets => true;
+
         protected override void RestartInternal() => Program.Restart();
 
         protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
-            => new[] { typeof(CoreAppHost).Assembly };
+        {
+            yield return typeof(CoreAppHost).Assembly;
+        }
 
         protected override void ShutdownInternal() => Program.Shutdown();
 
-        protected override bool SupportsDualModeSockets => true;
-
         protected override IHttpListener CreateHttpListener()
             => new WebSocketSharpListener(
                 Logger,
@@ -37,7 +39,6 @@ namespace Jellyfin.Server
                 CryptographyProvider,
                 SupportsDualModeSockets,
                 FileSystemManager,
-                EnvironmentInfo
-            );
+                EnvironmentInfo);
     }
 }

+ 9 - 3
Jellyfin.Server/Jellyfin.Server.csproj

@@ -5,11 +5,14 @@
     <OutputType>Exe</OutputType>
     <TargetFramework>netcoreapp2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
 
   <PropertyGroup>
     <!-- We need C# 7.1 for async main-->
     <LangVersion>latest</LangVersion>
+    <!-- Disable documentation warnings (for now) -->
+    <NoWarn>SA1600;CS1591</NoWarn>
   </PropertyGroup>
 
   <ItemGroup>
@@ -20,6 +23,10 @@
     <EmbeddedResource Include="Resources/Configuration/*" />
   </ItemGroup>
 
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+  </PropertyGroup>
+
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
@@ -41,9 +48,8 @@
     <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
     <PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
     <PackageReference Include="SkiaSharp" Version="1.68.0" />
-    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.12" />
-    <PackageReference Include="SQLitePCLRaw.core" Version="1.1.12" />
-    <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.12" />
+    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.13" />
+    <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.13" />
   </ItemGroup>
 
   <ItemGroup>

+ 132 - 83
Jellyfin.Server/Program.cs

@@ -21,6 +21,7 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Serilog;
 using Serilog.AspNetCore;
@@ -56,13 +57,28 @@ namespace Jellyfin.Server
                     errs => Task.FromResult(0)).ConfigureAwait(false);
         }
 
+        public static void Shutdown()
+        {
+            if (!_tokenSource.IsCancellationRequested)
+            {
+                _tokenSource.Cancel();
+            }
+        }
+
+        public static void Restart()
+        {
+            _restartOnShutdown = true;
+
+            Shutdown();
+        }
+
         private static async Task StartApp(StartupOptions options)
         {
             ServerApplicationPaths appPaths = CreateApplicationPaths(options);
 
             // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
             Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
-            await CreateLogger(appPaths);
+            await CreateLogger(appPaths).ConfigureAwait(false);
             _logger = _loggerFactory.CreateLogger("Main");
 
             AppDomain.CurrentDomain.UnhandledException += (sender, e)
@@ -75,6 +91,7 @@ namespace Jellyfin.Server
                 {
                     return; // Already shutting down
                 }
+
                 e.Cancel = true;
                 _logger.LogInformation("Ctrl+C, shutting down");
                 Environment.ExitCode = 128 + 2;
@@ -88,6 +105,7 @@ namespace Jellyfin.Server
                 {
                     return; // Already shutting down
                 }
+
                 _logger.LogInformation("Received a SIGTERM signal, shutting down");
                 Environment.ExitCode = 128 + 15;
                 Shutdown();
@@ -101,7 +119,7 @@ namespace Jellyfin.Server
             SQLitePCL.Batteries_V2.Init();
 
             // Allow all https requests
-            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
+            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; } );
 
             var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, null, appPaths.TempDirectory, true);
 
@@ -114,18 +132,18 @@ namespace Jellyfin.Server
                 new NullImageEncoder(),
                 new NetworkManager(_loggerFactory, environmentInfo)))
             {
-                await appHost.Init();
+                await appHost.Init(new ServiceCollection()).ConfigureAwait(false);
 
                 appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager);
 
-                await appHost.RunStartupTasks();
+                await appHost.RunStartupTasks().ConfigureAwait(false);
 
                 // TODO: read input for a stop command
 
                 try
                 {
                     // Block main thread until shutdown
-                    await Task.Delay(-1, _tokenSource.Token);
+                    await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
                 }
                 catch (TaskCanceledException)
                 {
@@ -139,112 +157,156 @@ namespace Jellyfin.Server
             }
         }
 
+        /// <summary>
+        /// Create the data, config and log paths from the variety of inputs(command line args,
+        /// environment variables) or decide on what default to use.  For Windows it's %AppPath%
+        /// for everything else the XDG approach is followed:
+        /// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
+        /// </summary>
+        /// <param name="options">StartupOptions</param>
+        /// <returns>ServerApplicationPaths</returns>
         private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options)
         {
-            string programDataPath = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
-            if (string.IsNullOrEmpty(programDataPath))
+            // dataDir
+            // IF      --datadir
+            // ELSE IF $JELLYFIN_DATA_PATH
+            // ELSE IF windows, use <%APPDATA%>/jellyfin
+            // ELSE IF $XDG_DATA_HOME then use $XDG_DATA_HOME/jellyfin
+            // ELSE    use $HOME/.local/share/jellyfin
+            var dataDir = options.DataDir;
+
+            if (string.IsNullOrEmpty(dataDir))
             {
-                if (options.DataDir != null)
-                {
-                    programDataPath = options.DataDir;
-                }
-                else
+                dataDir = Environment.GetEnvironmentVariable("JELLYFIN_DATA_PATH");
+
+                if (string.IsNullOrEmpty(dataDir))
                 {
                     if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                     {
-                        programDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+                        dataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
                     }
                     else
                     {
                         // $XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored.
-                        programDataPath = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
-                        // If $XDG_DATA_HOME is either not set or empty, $HOME/.local/share should be used.
-                        if (string.IsNullOrEmpty(programDataPath))
+                        dataDir = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
+
+                        // If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used.
+                        if (string.IsNullOrEmpty(dataDir))
                         {
-                            programDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
+                            dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share");
                         }
                     }
 
-                    programDataPath = Path.Combine(programDataPath, "jellyfin");
+                    dataDir = Path.Combine(dataDir, "jellyfin");
                 }
             }
 
-            if (string.IsNullOrEmpty(programDataPath))
-            {
-                Console.WriteLine("Cannot continue without path to program data folder (try -programdata)");
-                Environment.Exit(1);
-            }
-            else
-            {
-                Directory.CreateDirectory(programDataPath);
-            }
+            // configDir
+            // IF      --configdir
+            // ELSE IF $JELLYFIN_CONFIG_DIR
+            // ELSE IF --datadir, use <datadir>/config (assume portable run)
+            // ELSE IF <datadir>/config exists, use that
+            // ELSE IF windows, use <datadir>/config
+            // ELSE IF $XDG_CONFIG_HOME use $XDG_CONFIG_HOME/jellyfin
+            // ELSE    $HOME/.config/jellyfin
+            var configDir = options.ConfigDir;
 
-            string configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
             if (string.IsNullOrEmpty(configDir))
             {
-                if (options.ConfigDir != null)
-                {
-                    configDir = options.ConfigDir;
-                }
-                else
+                configDir = Environment.GetEnvironmentVariable("JELLYFIN_CONFIG_DIR");
+
+                if (string.IsNullOrEmpty(configDir))
                 {
-                    // Let BaseApplicationPaths set up the default value
-                    configDir = null;
+                    if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                    {
+                        // Hang config folder off already set dataDir
+                        configDir = Path.Combine(dataDir, "config");
+                    }
+                    else
+                    {
+                        // $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored.
+                        configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
+
+                        // If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME /.config should be used.
+                        if (string.IsNullOrEmpty(configDir))
+                        {
+                            configDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
+                        }
+
+                        configDir = Path.Combine(configDir, "jellyfin");
+                    }
                 }
             }
 
-            if (configDir != null)
-            {
-                Directory.CreateDirectory(configDir);
-            }
+            // cacheDir
+            // IF      --cachedir
+            // ELSE IF $JELLYFIN_CACHE_DIR
+            // ELSE IF windows, use <datadir>/cache
+            // ELSE IF XDG_CACHE_HOME, use $XDG_CACHE_HOME/jellyfin
+            // ELSE    HOME/.cache/jellyfin
+            var cacheDir = options.CacheDir;
 
-            string cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
             if (string.IsNullOrEmpty(cacheDir))
             {
-                if (options.CacheDir != null)
-                {
-                    cacheDir = options.CacheDir;
-                }
-                else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                cacheDir = Environment.GetEnvironmentVariable("JELLYFIN_CACHE_DIR");
+
+                if (string.IsNullOrEmpty(cacheDir))
                 {
-                    // $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored.
-                    cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
-                    // If $XDG_CACHE_HOME is either not set or empty, $HOME/.cache should be used.
-                    if (string.IsNullOrEmpty(cacheDir))
+                    if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                     {
-                        cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
+                        // Hang cache folder off already set dataDir
+                        cacheDir = Path.Combine(dataDir, "cache");
+                    }
+                    else
+                    {
+                        // $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored.
+                        cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
+
+                        // If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache should be used.
+                        if (string.IsNullOrEmpty(cacheDir))
+                        {
+                            cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
+                        }
+
+                        cacheDir = Path.Combine(cacheDir, "jellyfin");
                     }
-                    cacheDir = Path.Combine(cacheDir, "jellyfin");
                 }
             }
 
-            if (cacheDir != null)
-            {
-                Directory.CreateDirectory(cacheDir);
-            }
+            // logDir
+            // IF      --logdir
+            // ELSE IF $JELLYFIN_LOG_DIR
+            // ELSE IF --datadir, use <datadir>/log (assume portable run)
+            // ELSE    <datadir>/log
+            var logDir = options.LogDir;
 
-            string logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
             if (string.IsNullOrEmpty(logDir))
             {
-                if (options.LogDir != null)
-                {
-                    logDir = options.LogDir;
-                }
-                else
+                logDir = Environment.GetEnvironmentVariable("JELLYFIN_LOG_DIR");
+
+                if (string.IsNullOrEmpty(logDir))
                 {
-                    // Let BaseApplicationPaths set up the default value
-                    logDir = null;
+                    // Hang log folder off already set dataDir
+                    logDir = Path.Combine(dataDir, "log");
                 }
             }
 
-            if (logDir != null)
+            // Ensure the main folders exist before we continue
+            try
             {
+                Directory.CreateDirectory(dataDir);
                 Directory.CreateDirectory(logDir);
+                Directory.CreateDirectory(configDir);
+                Directory.CreateDirectory(cacheDir);
+            }
+            catch (IOException ex)
+            {
+                Console.Error.WriteLine("Error whilst attempting to create folder");
+                Console.Error.WriteLine(ex.ToString());
+                Environment.Exit(1);
             }
 
-            string appPath = AppContext.BaseDirectory;
-
-            return new ServerApplicationPaths(programDataPath, appPath, appPath, logDir, configDir, cacheDir);
+            return new ServerApplicationPaths(dataDir, logDir, configDir, cacheDir);
         }
 
         private static async Task CreateLogger(IApplicationPaths appPaths)
@@ -263,6 +325,7 @@ namespace Jellyfin.Server
                         await rscstr.CopyToAsync(fstr).ConfigureAwait(false);
                     }
                 }
+
                 var configuration = new ConfigurationBuilder()
                     .SetBasePath(appPaths.ConfigurationDirectoryPath)
                     .AddJsonFile("logging.json")
@@ -290,7 +353,7 @@ namespace Jellyfin.Server
             }
         }
 
-        public static IImageEncoder GetImageEncoder(
+        private static IImageEncoder GetImageEncoder(
             IFileSystem fileSystem,
             IApplicationPaths appPaths,
             ILocalizationManager localizationManager)
@@ -331,26 +394,12 @@ namespace Jellyfin.Server
                         {
                             return MediaBrowser.Model.System.OperatingSystem.BSD;
                         }
+
                         throw new Exception($"Can't resolve OS with description: '{osDescription}'");
                     }
             }
         }
 
-        public static void Shutdown()
-        {
-            if (!_tokenSource.IsCancellationRequested)
-            {
-                _tokenSource.Cancel();
-            }
-        }
-
-        public static void Restart()
-        {
-            _restartOnShutdown = true;
-
-            Shutdown();
-        }
-
         private static void StartNewInstance(StartupOptions options)
         {
             _logger.LogInformation("Starting new instance");

+ 83 - 69
Jellyfin.Server/SocketSharp/RequestMono.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Server.SocketSharp
     {
         internal static string GetParameter(string header, string attr)
         {
-            int ap = header.IndexOf(attr);
+            int ap = header.IndexOf(attr, StringComparison.Ordinal);
             if (ap == -1)
             {
                 return null;
@@ -82,9 +82,7 @@ namespace Jellyfin.Server.SocketSharp
                     }
                     else
                     {
-                        //
                         // We use a substream, as in 2.x we will support large uploads streamed to disk,
-                        //
                         var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
                         files[e.Name] = sub;
                     }
@@ -127,8 +125,12 @@ namespace Jellyfin.Server.SocketSharp
 
         public string Authorization => string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"];
 
-        protected bool validate_cookies, validate_query_string, validate_form;
-        protected bool checked_cookies, checked_query_string, checked_form;
+        protected bool validate_cookies { get; set; }
+        protected bool validate_query_string { get; set; }
+        protected bool validate_form { get; set; }
+        protected bool checked_cookies { get; set; }
+        protected bool checked_query_string { get; set; }
+        protected bool checked_form { get; set; }
 
         private static void ThrowValidationException(string name, string key, string value)
         {
@@ -138,8 +140,12 @@ namespace Jellyfin.Server.SocketSharp
                 v = v.Substring(0, 16) + "...\"";
             }
 
-            string msg = string.Format("A potentially dangerous Request.{0} value was " +
-                            "detected from the client ({1}={2}).", name, key, v);
+            string msg = string.Format(
+                CultureInfo.InvariantCulture,
+                "A potentially dangerous Request.{0} value was detected from the client ({1}={2}).",
+                name,
+                key,
+                v);
 
             throw new Exception(msg);
         }
@@ -179,6 +185,7 @@ namespace Jellyfin.Server.SocketSharp
             for (int idx = 1; idx < len; idx++)
             {
                 char next = val[idx];
+
                 // See http://secunia.com/advisories/14325
                 if (current == '<' || current == '\xff1c')
                 {
@@ -256,6 +263,7 @@ namespace Jellyfin.Server.SocketSharp
                                         value.Append((char)c);
                                     }
                                 }
+
                                 if (c == -1)
                                 {
                                     AddRawKeyValue(form, key, value);
@@ -271,6 +279,7 @@ namespace Jellyfin.Server.SocketSharp
                                 key.Append((char)c);
                             }
                         }
+
                         if (c == -1)
                         {
                             AddRawKeyValue(form, key, value);
@@ -308,6 +317,7 @@ namespace Jellyfin.Server.SocketSharp
                         result.Append(key);
                         result.Append('=');
                     }
+
                     result.Append(pair.Value);
                 }
 
@@ -429,13 +439,13 @@ namespace Jellyfin.Server.SocketSharp
                             real = position + d;
                             break;
                         default:
-                            throw new ArgumentException(nameof(origin));
+                            throw new ArgumentException("Unknown SeekOrigin value", nameof(origin));
                     }
 
                     long virt = real - offset;
                     if (virt < 0 || virt > Length)
                     {
-                        throw new ArgumentException();
+                        throw new ArgumentException("Invalid position", nameof(d));
                     }
 
                     position = s.Seek(real, SeekOrigin.Begin);
@@ -491,11 +501,6 @@ namespace Jellyfin.Server.SocketSharp
             public Stream InputStream => stream;
         }
 
-        private class Helpers
-        {
-            public static readonly CultureInfo InvariantCulture = CultureInfo.InvariantCulture;
-        }
-
         internal static class StrUtils
         {
             public static bool StartsWith(string str1, string str2, bool ignore_case)
@@ -533,12 +538,17 @@ namespace Jellyfin.Server.SocketSharp
 
             public class Element
             {
-                public string ContentType;
-                public string Name;
-                public string Filename;
-                public Encoding Encoding;
-                public long Start;
-                public long Length;
+                public string ContentType { get; set; }
+
+                public string Name { get; set; }
+
+                public string Filename { get; set; }
+
+                public Encoding Encoding { get; set; }
+
+                public long Start { get; set; }
+
+                public long Length { get; set; }
 
                 public override string ToString()
                 {
@@ -547,15 +557,23 @@ namespace Jellyfin.Server.SocketSharp
                 }
             }
 
+            private const byte LF = (byte)'\n';
+
+            private const byte CR = (byte)'\r';
+
             private Stream data;
+
             private string boundary;
-            private byte[] boundary_bytes;
+
+            private byte[] boundaryBytes;
+
             private byte[] buffer;
-            private bool at_eof;
+
+            private bool atEof;
+
             private Encoding encoding;
-            private StringBuilder sb;
 
-            private const byte LF = (byte)'\n', CR = (byte)'\r';
+            private StringBuilder sb;
 
             // See RFC 2046
             // In the case of multipart entities, in which one or more different
@@ -570,18 +588,48 @@ namespace Jellyfin.Server.SocketSharp
             public HttpMultipart(Stream data, string b, Encoding encoding)
             {
                 this.data = data;
-                //DB: 30/01/11: cannot set or read the Position in HttpListener in Win.NET
-                //var ms = new MemoryStream(32 * 1024);
-                //data.CopyTo(ms);
-                //this.data = ms;
-
                 boundary = b;
-                boundary_bytes = encoding.GetBytes(b);
-                buffer = new byte[boundary_bytes.Length + 2]; // CRLF or '--'
+                boundaryBytes = encoding.GetBytes(b);
+                buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--'
                 this.encoding = encoding;
                 sb = new StringBuilder();
             }
 
+            public Element ReadNextElement()
+            {
+                if (atEof || ReadBoundary())
+                {
+                    return null;
+                }
+
+                var elem = new Element();
+                string header;
+                while ((header = ReadHeaders()) != null)
+                {
+                    if (StrUtils.StartsWith(header, "Content-Disposition:", true))
+                    {
+                        elem.Name = GetContentDispositionAttribute(header, "name");
+                        elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
+                    }
+                    else if (StrUtils.StartsWith(header, "Content-Type:", true))
+                    {
+                        elem.ContentType = header.Substring("Content-Type:".Length).Trim();
+                        elem.Encoding = GetEncoding(elem.ContentType);
+                    }
+                }
+
+                long start = data.Position;
+                elem.Start = start;
+                long pos = MoveToNextBoundary();
+                if (pos == -1)
+                {
+                    return null;
+                }
+
+                elem.Length = pos - start;
+                return elem;
+            }
+
             private string ReadLine()
             {
                 // CRLF or LF are ok as line endings.
@@ -600,6 +648,7 @@ namespace Jellyfin.Server.SocketSharp
                     {
                         break;
                     }
+
                     got_cr = b == CR;
                     sb.Append((char)b);
                 }
@@ -769,7 +818,7 @@ namespace Jellyfin.Server.SocketSharp
                             return -1;
                         }
 
-                        if (!CompareBytes(boundary_bytes, buffer))
+                        if (!CompareBytes(boundaryBytes, buffer))
                         {
                             state = 0;
                             data.Position = retval + 2;
@@ -785,7 +834,7 @@ namespace Jellyfin.Server.SocketSharp
 
                         if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-')
                         {
-                            at_eof = true;
+                            atEof = true;
                         }
                         else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF)
                         {
@@ -800,6 +849,7 @@ namespace Jellyfin.Server.SocketSharp
                             c = data.ReadByte();
                             continue;
                         }
+
                         data.Position = retval + 2;
                         if (got_cr)
                         {
@@ -818,42 +868,6 @@ namespace Jellyfin.Server.SocketSharp
                 return retval;
             }
 
-            public Element ReadNextElement()
-            {
-                if (at_eof || ReadBoundary())
-                {
-                    return null;
-                }
-
-                var elem = new Element();
-                string header;
-                while ((header = ReadHeaders()) != null)
-                {
-                    if (StrUtils.StartsWith(header, "Content-Disposition:", true))
-                    {
-                        elem.Name = GetContentDispositionAttribute(header, "name");
-                        elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename"));
-                    }
-                    else if (StrUtils.StartsWith(header, "Content-Type:", true))
-                    {
-                        elem.ContentType = header.Substring("Content-Type:".Length).Trim();
-                        elem.Encoding = GetEncoding(elem.ContentType);
-                    }
-                }
-
-                long start = 0;
-                start = data.Position;
-                elem.Start = start;
-                long pos = MoveToNextBoundary();
-                if (pos == -1)
-                {
-                    return null;
-                }
-
-                elem.Length = pos - start;
-                return elem;
-            }
-
             private static string StripPath(string path)
             {
                 if (path == null || path.Length == 0)

+ 20 - 24
Jellyfin.Server/SocketSharp/SharpWebSocket.cs

@@ -24,6 +24,7 @@ namespace Jellyfin.Server.SocketSharp
 
         private TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+        private bool _disposed = false;
 
         public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger)
         {
@@ -40,9 +41,9 @@ namespace Jellyfin.Server.SocketSharp
             _logger = logger;
             WebSocket = socket;
 
-            socket.OnMessage += socket_OnMessage;
-            socket.OnClose += socket_OnClose;
-            socket.OnError += socket_OnError;
+            socket.OnMessage += OnSocketMessage;
+            socket.OnClose += OnSocketClose;
+            socket.OnError += OnSocketError;
 
             WebSocket.ConnectAsServer();
         }
@@ -52,29 +53,22 @@ namespace Jellyfin.Server.SocketSharp
             return _taskCompletionSource.Task;
         }
 
-        void socket_OnError(object sender, SocketHttpListener.ErrorEventArgs e)
+        private void OnSocketError(object sender, SocketHttpListener.ErrorEventArgs e)
         {
             _logger.LogError("Error in SharpWebSocket: {Message}", e.Message ?? string.Empty);
-            //Closed?.Invoke(this, EventArgs.Empty);
+
+            // Closed?.Invoke(this, EventArgs.Empty);
         }
 
-        void socket_OnClose(object sender, SocketHttpListener.CloseEventArgs e)
+        private void OnSocketClose(object sender, SocketHttpListener.CloseEventArgs e)
         {
             _taskCompletionSource.TrySetResult(true);
 
             Closed?.Invoke(this, EventArgs.Empty);
         }
 
-        void socket_OnMessage(object sender, SocketHttpListener.MessageEventArgs e)
+        private void OnSocketMessage(object sender, SocketHttpListener.MessageEventArgs e)
         {
-            //if (!string.IsNullOrEmpty(e.Data))
-            //{
-            //    if (OnReceive != null)
-            //    {
-            //        OnReceive(e.Data);
-            //    }
-            //    return;
-            //}
             if (OnReceiveBytes != null)
             {
                 OnReceiveBytes(e.RawData);
@@ -117,6 +111,7 @@ namespace Jellyfin.Server.SocketSharp
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
         /// <summary>
@@ -125,16 +120,23 @@ namespace Jellyfin.Server.SocketSharp
         /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool dispose)
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             if (dispose)
             {
-                WebSocket.OnMessage -= socket_OnMessage;
-                WebSocket.OnClose -= socket_OnClose;
-                WebSocket.OnError -= socket_OnError;
+                WebSocket.OnMessage -= OnSocketMessage;
+                WebSocket.OnClose -= OnSocketClose;
+                WebSocket.OnError -= OnSocketError;
 
                 _cancellationTokenSource.Cancel();
 
                 WebSocket.Close();
             }
+
+            _disposed = true;
         }
 
         /// <summary>
@@ -142,11 +144,5 @@ namespace Jellyfin.Server.SocketSharp
         /// </summary>
         /// <value>The receive action.</value>
         public Action<byte[]> OnReceiveBytes { get; set; }
-
-        /// <summary>
-        /// Gets or sets the on receive.
-        /// </summary>
-        /// <value>The on receive.</value>
-        public Action<string> OnReceive { get; set; }
     }
 }

+ 37 - 24
Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs

@@ -34,9 +34,16 @@ namespace Jellyfin.Server.SocketSharp
         private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         private CancellationToken _disposeCancellationToken;
 
-        public WebSocketSharpListener(ILogger logger, X509Certificate certificate, IStreamHelper streamHelper,
-            INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider,
-            bool enableDualMode, IFileSystem fileSystem, IEnvironmentInfo environment)
+        public WebSocketSharpListener(
+            ILogger logger,
+            X509Certificate certificate,
+            IStreamHelper streamHelper,
+            INetworkManager networkManager,
+            ISocketFactory socketFactory,
+            ICryptoProvider cryptoProvider,
+            bool enableDualMode,
+            IFileSystem fileSystem,
+            IEnvironmentInfo environment)
         {
             _logger = logger;
             _certificate = certificate;
@@ -61,7 +68,9 @@ namespace Jellyfin.Server.SocketSharp
         public void Start(IEnumerable<string> urlPrefixes)
         {
             if (_listener == null)
+            {
                 _listener = new HttpListener(_logger, _cryptoProvider, _socketFactory, _networkManager, _streamHelper, _fileSystem, _environment);
+            }
 
             _listener.EnableDualMode = _enableDualMode;
 
@@ -83,15 +92,18 @@ namespace Jellyfin.Server.SocketSharp
 
         private void ProcessContext(HttpListenerContext context)
         {
-            var _ = Task.Run(async () => await InitTask(context, _disposeCancellationToken));
+            _ = Task.Run(async () => await InitTask(context, _disposeCancellationToken).ConfigureAwait(false));
         }
 
         private static void LogRequest(ILogger logger, HttpListenerRequest request)
         {
             var url = request.Url.ToString();
 
-            logger.LogInformation("{0} {1}. UserAgent: {2}",
-                request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod, url, request.UserAgent ?? string.Empty);
+            logger.LogInformation(
+                "{0} {1}. UserAgent: {2}",
+                request.IsWebSocketRequest ? "WS" : "HTTP " + request.HttpMethod,
+                url,
+                request.UserAgent ?? string.Empty);
         }
 
         private Task InitTask(HttpListenerContext context, CancellationToken cancellationToken)
@@ -201,7 +213,7 @@ namespace Jellyfin.Server.SocketSharp
             }
             catch (ObjectDisposedException)
             {
-                //TODO Investigate and properly fix.
+                // TODO: Investigate and properly fix.
             }
             catch (Exception ex)
             {
@@ -223,38 +235,39 @@ namespace Jellyfin.Server.SocketSharp
         public Task Stop()
         {
             _disposeCancellationTokenSource.Cancel();
-
-            if (_listener != null)
-            {
-                _listener.Close();
-            }
+            _listener?.Close();
 
             return Task.CompletedTask;
         }
 
+        /// <summary>
+        /// Releases the unmanaged resources and disposes of the managed resources used.
+        /// </summary>
         public void Dispose()
         {
             Dispose(true);
+            GC.SuppressFinalize(this);
         }
 
         private bool _disposed;
-        private readonly object _disposeLock = new object();
+
+        /// <summary>
+        /// Releases the unmanaged resources and disposes of the managed resources used.
+        /// </summary>
+        /// <param name="disposing">Whether or not the managed resources should be disposed</param>
         protected virtual void Dispose(bool disposing)
         {
-            if (_disposed) return;
-
-            lock (_disposeLock)
+            if (_disposed)
             {
-                if (_disposed) return;
-
-                if (disposing)
-                {
-                    Stop();
-                }
+                return;
+            }
 
-                //release unmanaged resources here...
-                _disposed = true;
+            if (disposing)
+            {
+                Stop().GetAwaiter().GetResult();
             }
+
+            _disposed = true;
         }
     }
 }

+ 22 - 5
Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.IO;
 using System.Text;
 using Emby.Server.Implementations.HttpServer;
@@ -24,7 +25,7 @@ namespace Jellyfin.Server.SocketSharp
             this.request = httpContext.Request;
             this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
 
-            //HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
+            // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
         }
 
         private static string GetHandlerPathIfAny(string listenerUrl)
@@ -41,7 +42,7 @@ namespace Jellyfin.Server.SocketSharp
             }
 
             var startHostUrl = listenerUrl.Substring(pos + "://".Length);
-            var endPos = startHostUrl.IndexOf('/');
+            var endPos = startHostUrl.IndexOf('/', StringComparison.Ordinal);
             if (endPos == -1)
             {
                 return null;
@@ -69,9 +70,11 @@ namespace Jellyfin.Server.SocketSharp
 
         public string UserHostAddress => request.UserHostAddress;
 
-        public string XForwardedFor => string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
+        public string XForwardedFor
+            => string.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"];
 
-        public int? XForwardedPort => string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"]);
+        public int? XForwardedPort
+            => string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
 
         public string XForwardedProtocol => string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"];
 
@@ -107,6 +110,7 @@ namespace Jellyfin.Server.SocketSharp
                 switch (crlf)
                 {
                     case 0:
+                    {
                         if (c == '\r')
                         {
                             crlf = 1;
@@ -121,29 +125,39 @@ namespace Jellyfin.Server.SocketSharp
                         {
                             throw new ArgumentException("net_WebHeaderInvalidControlChars");
                         }
+
                         break;
+                    }
 
                     case 1:
+                    {
                         if (c == '\n')
                         {
                             crlf = 2;
                             break;
                         }
+
                         throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                    }
 
                     case 2:
+                    {
                         if (c == ' ' || c == '\t')
                         {
                             crlf = 0;
                             break;
                         }
+
                         throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
+                    }
                 }
             }
+
             if (crlf != 0)
             {
                 throw new ArgumentException("net_WebHeaderInvalidCRLFChars");
             }
+
             return name;
         }
 
@@ -156,6 +170,7 @@ namespace Jellyfin.Server.SocketSharp
                     return true;
                 }
             }
+
             return false;
         }
 
@@ -343,6 +358,7 @@ namespace Jellyfin.Server.SocketSharp
                     this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
                     this.pathInfo = NormalizePathInfo(pathInfo, mode);
                 }
+
                 return this.pathInfo;
             }
         }
@@ -444,7 +460,7 @@ namespace Jellyfin.Server.SocketSharp
 
         public string ContentType => request.ContentType;
 
-        public Encoding contentEncoding;
+        private Encoding contentEncoding;
         public Encoding ContentEncoding
         {
             get => contentEncoding ?? request.ContentEncoding;
@@ -502,6 +518,7 @@ namespace Jellyfin.Server.SocketSharp
                         i++;
                     }
                 }
+
                 return httpFiles;
             }
         }

+ 8 - 12
Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs

@@ -13,12 +13,12 @@ using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse;
 using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse;
 using IRequest = MediaBrowser.Model.Services.IRequest;
 
-
 namespace Jellyfin.Server.SocketSharp
 {
     public class WebSocketSharpResponse : IHttpResponse
     {
         private readonly ILogger _logger;
+
         private readonly HttpListenerResponse _response;
 
         public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request)
@@ -30,7 +30,9 @@ namespace Jellyfin.Server.SocketSharp
         }
 
         public IRequest Request { get; private set; }
+
         public Dictionary<string, object> Items { get; private set; }
+
         public object OriginalResponse => _response;
 
         public int StatusCode
@@ -51,7 +53,7 @@ namespace Jellyfin.Server.SocketSharp
             set => _response.ContentType = value;
         }
 
-        //public ICookies Cookies { get; set; }
+        public QueryParamCollection Headers => _response.Headers;
 
         public void AddHeader(string name, string value)
         {
@@ -64,8 +66,6 @@ namespace Jellyfin.Server.SocketSharp
             _response.AddHeader(name, value);
         }
 
-        public QueryParamCollection Headers => _response.Headers;
-
         public string GetHeader(string name)
         {
             return _response.Headers[name];
@@ -114,9 +114,9 @@ namespace Jellyfin.Server.SocketSharp
 
         public void SetContentLength(long contentLength)
         {
-            //you can happily set the Content-Length header in Asp.Net
-            //but HttpListener will complain if you do - you have to set ContentLength64 on the response.
-            //workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
+            // you can happily set the Content-Length header in Asp.Net
+            // but HttpListener will complain if you do - you have to set ContentLength64 on the response.
+            // workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header
             _response.ContentLength64 = contentLength;
         }
 
@@ -147,15 +147,12 @@ namespace Jellyfin.Server.SocketSharp
             {
                 sb.Append($";domain={cookie.Domain}");
             }
-            //else if (restrictAllCookiesToDomain != null)
-            //{
-            //    sb.Append($";domain={restrictAllCookiesToDomain}");
-            //}
 
             if (cookie.Secure)
             {
                 sb.Append(";Secure");
             }
+
             if (cookie.HttpOnly)
             {
                 sb.Append(";HttpOnly");
@@ -164,7 +161,6 @@ namespace Jellyfin.Server.SocketSharp
             return sb.ToString();
         }
 
-
         public bool SendChunked
         {
             get => _response.SendChunked;

+ 1 - 1
MediaBrowser.Api/Session/SessionInfoWebSocketListener.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Api.Session
     /// <summary>
     /// Class SessionInfoWebSocketListener
     /// </summary>
-    class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
+    public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnumerable<SessionInfo>, WebSocketListenerState>
     {
         /// <summary>
         /// Gets the name.

+ 1 - 1
MediaBrowser.Api/System/ActivityLogWebSocketListener.cs

@@ -11,7 +11,7 @@ namespace MediaBrowser.Api.System
     /// <summary>
     /// Class SessionInfoWebSocketListener
     /// </summary>
-    class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
+    public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<List<ActivityLogEntry>, WebSocketListenerState>
     {
         /// <summary>
         /// Gets the name.

+ 2 - 14
MediaBrowser.Common/IApplicationHost.cs

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Updates;
+using Microsoft.Extensions.DependencyInjection;
 
 namespace MediaBrowser.Common
 {
@@ -13,12 +14,6 @@ namespace MediaBrowser.Common
     /// </summary>
     public interface IApplicationHost
     {
-        /// <summary>
-        /// Gets the display name of the operating system.
-        /// </summary>
-        /// <value>The display name of the operating system.</value>
-        string OperatingSystemDisplayName { get; }
-
         /// <summary>
         /// Gets the name.
         /// </summary>
@@ -104,13 +99,6 @@ namespace MediaBrowser.Common
         /// <returns>``0.</returns>
         T Resolve<T>();
 
-        /// <summary>
-        /// Resolves this instance.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <returns>``0.</returns>
-        T TryResolve<T>();
-
         /// <summary>
         /// Shuts down.
         /// </summary>
@@ -131,7 +119,7 @@ namespace MediaBrowser.Common
         /// <summary>
         /// Inits this instance.
         /// </summary>
-        Task Init();
+        Task Init(IServiceCollection serviceCollection);
 
         /// <summary>
         /// Creates the instance.

+ 4 - 0
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -11,6 +11,10 @@
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
+  </ItemGroup>
+
   <ItemGroup>
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>

+ 1 - 1
MediaBrowser.LocalMetadata/Providers/PlaylistXmlProvider.cs

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.LocalMetadata.Providers
 {
-    class PlaylistXmlProvider : BaseXmlProvider<Playlist>
+    public class PlaylistXmlProvider : BaseXmlProvider<Playlist>
     {
         private readonly ILogger _logger;
         private readonly IProviderManager _providerManager;

+ 3 - 3
MediaBrowser.Model/System/PublicSystemInfo.cs

@@ -24,12 +24,12 @@ namespace MediaBrowser.Model.System
         /// Gets or sets the server version.
         /// </summary>
         /// <value>The version.</value>
-        public string Version { get; set; }        
+        public string Version { get; set; }
 
         /// <summary>
-        /// Gets or sets the operating sytem.
+        /// Gets or sets the operating system.
         /// </summary>
-        /// <value>The operating sytem.</value>
+        /// <value>The operating system.</value>
         public string OperatingSystem { get; set; }
 
         /// <summary>

+ 2 - 1
MediaBrowser.Model/System/SystemInfo.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Runtime.InteropServices;
 using MediaBrowser.Model.Updates;
 
@@ -136,7 +137,7 @@ namespace MediaBrowser.Model.System
         /// </summary>
         public SystemInfo()
         {
-            CompletedInstallations = new InstallationInfo[] { };
+            CompletedInstallations = Array.Empty<InstallationInfo>();
         }
     }
 }

+ 1 - 1
MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs

@@ -14,7 +14,7 @@ using MediaBrowser.Providers.Movies;
 
 namespace MediaBrowser.Providers.BoxSets
 {
-    class MovieDbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
+    public class MovieDbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IHttpClient _httpClient;
 

+ 1 - 1
MediaBrowser.Providers/Movies/MovieDbImageProvider.cs

@@ -16,7 +16,7 @@ using MediaBrowser.Model.Serialization;
 
 namespace MediaBrowser.Providers.Movies
 {
-    class MovieDbImageProvider : IRemoteImageProvider, IHasOrder
+    public class MovieDbImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IHttpClient _httpClient;

+ 1 - 1
MediaBrowser.Providers/Music/MusicVideoMetadataService.cs

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Music
 {
-    class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
+    public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo>
     {
         protected override void MergeData(MetadataResult<MusicVideo> source, MetadataResult<MusicVideo> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {

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

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Photos
 {
-    class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
+    public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo>
     {
         protected override void MergeData(MetadataResult<PhotoAlbum> source, MetadataResult<PhotoAlbum> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {

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

@@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Photos
 {
-    class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
+    public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo>
     {
         protected override void MergeData(MetadataResult<Photo> source, MetadataResult<Photo> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {

+ 1 - 1
MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs

@@ -11,7 +11,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Playlists
 {
-    class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
+    public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo>
     {
         protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item)
         {

+ 1 - 1
MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs

@@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.TV.Omdb
 {
-    class OmdbEpisodeProvider :
+    public class OmdbEpisodeProvider :
             IRemoteMetadataProvider<Episode, EpisodeInfo>,
             IHasOrder
     {

+ 1 - 1
MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs

@@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.TV.TheMovieDb
 {
-    class MovieDbEpisodeProvider :
+    public class MovieDbEpisodeProvider :
             MovieDbProviderBase,
             IRemoteMetadataProvider<Episode, EpisodeInfo>,
             IHasOrder

+ 1 - 1
MediaBrowser.WebDashboard/jellyfin-web

@@ -1 +1 @@
-Subproject commit 094c1deae91c51b8bbf8ebb16a55758af110f04d
+Subproject commit c7ce1ac8eccd50f1bd759b30fbe60ea797ffe86e

+ 1 - 1
MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs

@@ -13,7 +13,7 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.XbmcMetadata.Parsers
 {
-    class MovieNfoParser : BaseNfoParser<Video>
+    public class MovieNfoParser : BaseNfoParser<Video>
     {
         protected override bool SupportsUrlAfterClosingXmlTag => true;
 

+ 18 - 6
build

@@ -23,8 +23,9 @@ usage() {
     echo -e "Usage:"
     echo -e " $ build --list-platforms"
     echo -e " $ build --list-actions <platform>"
-    echo -e " $ build [-b/--web-branch <web_branch>] <platform> <action>"
+    echo -e " $ build [-k/--keep-artifacts] [-b/--web-branch <web_branch>] <platform> <action>"
     echo -e ""
+    echo -e "The 'keep-artifacts' option preserves build artifacts, e.g. Docker images for system package builds."
     echo -e "The web_branch defaults to the same branch name as the current main branch."
     echo -e "To build all platforms, use 'all'."
     echo -e "To perform all build actions, use 'all'."
@@ -67,6 +68,14 @@ if [[ $1 == '--list-actions' ]]; then
     exit 0
 fi
 
+# Parse keep-artifacts option
+if [[ $1 == '-k' || $1 == '--keep-artifacts' ]]; then
+    keep_artifacts="y"
+    shift 1
+else
+    keep_artifacts="n"
+fi
+
 # Parse branch option
 if [[ $1 == '-b' || $1 == '--web-branch' ]]; then
 	web_branch="$2"
@@ -193,6 +202,13 @@ for target_platform in ${platform[@]}; do
     echo -e "> Processing platform ${target_platform}"
     date_start=$( date +%s )
     pushd ${target_platform}
+    cleanup() {
+        echo -e ">> Processing action clean"
+        if [[ -f clean.sh && -x clean.sh ]]; then
+            ./clean.sh ${keep_artifacts}
+        fi
+    }
+    trap cleanup EXIT INT
     for target_action in ${action[@]}; do
         echo -e ">> Processing action ${target_action}"
         if [[ -f ${target_action}.sh && -x ${target_action}.sh ]]; then
@@ -204,12 +220,8 @@ for target_platform in ${platform[@]}; do
         target_dir="../../../jellyfin-build/${target_platform}"
         mkdir -p ${target_dir}
         mv pkg-dist/* ${target_dir}/
-
-        echo -e ">> Processing action clean"
-        if [[ -f clean.sh && -x clean.sh ]]; then
-            ./clean.sh 
-        fi
     fi
+    cleanup
     date_end=$( date +%s )
     echo -e "> Completed platform ${target_platform} in $( expr ${date_end} - ${date_start} ) seconds."
     popd

+ 2 - 0
deployment/README.md

@@ -55,6 +55,8 @@ These builds are not necessarily run from the `build` script, but are present fo
 
 * The `clean` action should always `exit 0` even if no work is done or it fails.
 
+* The `clean` action can be passed a variable as argument 1, named `keep_artifacts`, containing either the value `y` or `n`. It is indended to handle situations when the user runs `build --keep-artifacts` and should be handled intelligently. Usually, this is used to preserve Docker images while still removing temporary directories.
+
 ### Output Files
 
 * Upon completion of the defined actions, at least one output file must be created in the `<platform>/pkg-dist` directory.

+ 26 - 14
deployment/centos-package-x64/Dockerfile

@@ -1,15 +1,27 @@
 FROM centos:7
-ARG HOME=/build
-RUN mkdir /build && \
-    yum install -y @buildsys-build rpmdevtools yum-plugins-core && \
-    rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm && \
-    rpmdev-setuptree
-
-WORKDIR /build/rpmbuild
-COPY ./deployment/centos-package-x64/pkg-src/jellyfin.spec SPECS
-COPY ./deployment/centos-package-x64/pkg-src/ SOURCES
-
-RUN spectool -g -R SPECS/jellyfin.spec && \
-    rpmbuild -bs SPECS/jellyfin.spec && \
-    yum-builddep  -y SRPMS/jellyfin-*.src.rpm && \
-    rpmbuild -bb SPECS/jellyfin.spec;
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/centos-package-x64
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+
+# Prepare CentOS build environment
+RUN yum update -y \
+ && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
+ && rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \
+ && rpmdev-setuptree \
+ && yum install -y dotnet-sdk-${SDK_VERSION} \
+ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && mkdir -p ${SOURCE_DIR}/SPECS \
+ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
+ && mkdir -p ${SOURCE_DIR}/SOURCES \
+ && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]

+ 0 - 1
deployment/centos-package-x64/clean.sh

@@ -1 +0,0 @@
-../fedora-package-x64/clean.sh

+ 34 - 0
deployment/centos-package-x64/clean.sh

@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+source ../common.build.sh
+
+keep_artifacts="${1}"
+
+WORKDIR="$( pwd )"
+VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+package_source_dir="${WORKDIR}/pkg-src"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-centos-build"
+
+rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \
+  || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+  || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+  || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+    docker_sudo=""
+    if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+      && [[ ! ${EUID:-1000} -eq 0 ]] \
+      && [[ ! ${USER} == "root" ]] \
+      && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+        docker_sudo=sudo
+    fi
+    ${docker_sudo} docker image rm ${image_name} --force
+fi

+ 1 - 0
deployment/centos-package-x64/dependencies.txt

@@ -0,0 +1 @@
+docker

+ 20 - 0
deployment/centos-package-x64/docker-build.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Builds the RPM inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+ls -al SOURCES/pkg-src/
+
+# Build RPM
+spectool -g -R SPECS/jellyfin.spec
+rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/rpm
+mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/

+ 0 - 1
deployment/centos-package-x64/package.sh

@@ -1 +0,0 @@
-../fedora-package-x64/package.sh

+ 80 - 0
deployment/centos-package-x64/package.sh

@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+
+source ../common.build.sh
+
+WORKDIR="$( pwd )"
+VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+pkg_src_dir="${WORKDIR}/pkg-src"
+current_user="$( whoami )"
+image_name="jellyfin-centos-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+  && [[ ! ${EUID:-1000} -eq 0 ]] \
+  && [[ ! ${USER} == "root" ]] \
+  && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+    docker_sudo="sudo"
+else
+    docker_sudo=""
+fi
+
+# Create RPM source archive
+GNU_TAR=1
+mkdir -p "${package_temporary_dir}"
+echo "Bundling all sources for RPM build."
+tar \
+--transform "s,^\.,jellyfin-${VERSION}," \
+--exclude='.git*' \
+--exclude='**/.git' \
+--exclude='**/.hg' \
+--exclude='**/.vs' \
+--exclude='**/.vscode' \
+--exclude='deployment' \
+--exclude='**/bin' \
+--exclude='**/obj' \
+--exclude='**/.nuget' \
+--exclude='*.deb' \
+--exclude='*.rpm' \
+-czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" \
+-C "../.." ./ || GNU_TAR=0
+
+if [ $GNU_TAR -eq 0 ]; then
+    echo "The installed tar binary did not support --transform. Using workaround."
+    mkdir -p "${package_temporary_dir}/jellyfin"
+    # Not GNU tar
+    tar \
+    --exclude='.git*' \
+    --exclude='**/.git' \
+    --exclude='**/.hg' \
+    --exclude='**/.vs' \
+    --exclude='**/.vscode' \
+    --exclude='deployment' \
+    --exclude='**/bin' \
+    --exclude='**/obj' \
+    --exclude='**/.nuget' \
+    --exclude='*.deb' \
+    --exclude='*.rpm' \
+    -zcf \
+    "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
+    -C "../.." ./
+    echo "Extracting filtered package."
+    tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
+    echo "Removing filtered package."
+    rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
+    echo "Repackaging package into final tarball."
+    tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
+fi
+
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the RPMs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+# Correct ownership on the RPMs (as current user, then as root if that fails)
+chown -R "${current_user}" "${package_temporary_dir}" \
+  || sudo chown -R "${current_user}" "${package_temporary_dir}"
+# Move the RPMs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/rpm/* "${output_dir}"

+ 15 - 17
deployment/debian-package-x64/Dockerfile

@@ -1,23 +1,21 @@
-FROM debian:9
-ARG SOURCEDIR=/repo 
+FROM microsoft/dotnet:2.2-sdk-stretch
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-x64
+ARG ARTIFACT_DIR=/dist
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
 ENV DEB_BUILD_OPTIONS=noddebs
 
-# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current
+# Prepare Debian build environment
 RUN apt-get update \
- && apt-get install -y apt-transport-https debhelper gnupg wget devscripts \
- && wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg \
- && mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ \
- && wget -q https://packages.microsoft.com/config/debian/9/prod.list \
- && mv prod.list /etc/apt/sources.list.d/microsoft-prod.list \
- && chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg \
- && chown root:root /etc/apt/sources.list.d/microsoft-prod.list \
- && apt-get update
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
+ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
 
-WORKDIR ${SOURCEDIR}
-COPY . .
-COPY ./deployment/debian-package-x64/pkg-src ./debian
+VOLUME ${ARTIFACT_DIR}/
 
-RUN yes | mk-build-deps -i debian/control \
-    && dpkg-buildpackage -us -uc
+COPY . ${SOURCE_DIR}/
 
-WORKDIR /
+ENTRYPOINT ["/docker-build.sh"]

+ 24 - 2
deployment/debian-package-x64/clean.sh

@@ -2,6 +2,28 @@
 
 source ../common.build.sh
 
-VERSION=`get_version ../..`
+keep_artifacts="${1}"
 
-clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION}
+WORKDIR="$( pwd )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-debian-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+  || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+  || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+    docker_sudo=""
+    if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+      && [[ ! ${EUID:-1000} -eq 0 ]] \
+      && [[ ! ${USER} == "root" ]] \
+      && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+        docker_sudo=sudo
+    fi
+    ${docker_sudo} docker image rm ${image_name} --force
+fi

+ 19 - 0
deployment/debian-package-x64/docker-build.sh

@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Builds the DEB inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
+sed -i '/dotnet-sdk-2.2,/d' debian/control
+
+# Build DEB
+dpkg-buildpackage -us -uc
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/deb
+mv /jellyfin_* ${ARTIFACT_DIR}/deb/

+ 27 - 27
deployment/debian-package-x64/package.sh

@@ -2,30 +2,30 @@
 
 source ../common.build.sh
 
-VERSION=`get_version ../..`
-
-# TODO get the version in the package automatically. And using the changelog to decide the debian package suffix version.
-
-# Build a Jellyfin .deb file with Docker on Linux
-# Places the output .deb file in the parent directory
-
-package_temporary_dir="`pwd`/pkg-dist-tmp"
-output_dir="`pwd`/pkg-dist"
-current_user="`whoami`"
-image_name="jellyfin-debuild"
-
-cleanup() {
-    set +o errexit
-    docker image rm $image_name --force
-    rm -rf "$package_temporary_dir"
-}
-trap cleanup EXIT INT
-
-docker build ../.. -t "$image_name" -f ./Dockerfile --build-arg SOURCEDIR="/jellyfin-${VERSION}"
-mkdir -p "$package_temporary_dir"
-mkdir -p "$output_dir"
-docker run --rm -v "$package_temporary_dir:/temp" "$image_name" sh -c 'find / -maxdepth 1 -type f -name "jellyfin*" -exec mv {} /temp \;'
-chown -R "$current_user" "$package_temporary_dir" \
-|| sudo chown -R "$current_user" "$package_temporary_dir"
-
-mv "$package_temporary_dir"/* "$output_dir"
+WORKDIR="$( pwd )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-debian-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+  && [[ ! ${EUID:-1000} -eq 0 ]] \
+  && [[ ! ${USER} == "root" ]] \
+  && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+    docker_sudo="sudo"
+else
+    docker_sudo=""
+fi
+
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+# Correct ownership on the DEBs (as current user, then as root if that fails)
+chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
+  || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/deb/* "${output_dir}"

+ 6 - 4
deployment/debian-package-x64/pkg-src/bin/restart.sh

@@ -2,10 +2,12 @@
 
 NAME=jellyfin
 
-restart_cmds=("s6-svc -t /var/run/s6/services/${NAME}" \
-  "systemctl restart ${NAME}" \
-  "service ${NAME} restart" \
-  "/etc/init.d/${NAME} restart") 
+restart_cmds=(
+  "systemctl restart ${NAME}"
+  "service ${NAME} restart"
+  "/etc/init.d/${NAME} restart"
+  "s6-svc -t /var/run/s6/services/${NAME}"
+)
 
 for restart_cmd in "${restart_cmds[@]}"; do
   cmd=$(echo "$restart_cmd" | awk '{print $1}')

+ 10 - 6
deployment/debian-package-x64/pkg-src/conf/jellyfin

@@ -19,13 +19,17 @@ JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin"
 JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
 
 # Restart script for in-app server control
-JELLYFIN_RESTART_OPT="--restartpath /usr/lib/jellyfin/restart.sh"
+JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
 
-# [OPTIONAL] ffmpeg binary paths
-#JELLYFIN_FFMPEG_OPTS="--ffmpeg /usr/bin/ffmpeg --ffprobe /usr/bin/ffprobe"
+# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
+#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
+#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
 
-# [OPTIONAL] Additional user-defined options for the binary
-#JELLYFIN_ADD_OPTS=""
+# [OPTIONAL] run Jellyfin as a headless service
+#JELLYFIN_SERVICE_OPT="--service"
+
+# [OPTIONAL] run Jellyfin without the web app
+#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
 
 #
 # SysV init/Upstart options
@@ -34,4 +38,4 @@ JELLYFIN_RESTART_OPT="--restartpath /usr/lib/jellyfin/restart.sh"
 # Application username
 JELLYFIN_USER="jellyfin"
 # Full application command
-JELLYFIN_ARGS="--datadir $JELLYFIN_DATA_DIRECTORY --configdir $JELLYFIN_CONFIG_DIRECTORY --logdir $JELLYFIN_LOG_DIRECTORY --cachedir $JELLYFIN_CACHE_DIRECTORY $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPTS $JELLYFIN_ADD_OPTS"
+JELLYFIN_ARGS="--datadir=$JELLYFIN_DATA_DIRECTORY --configdir=$JELLYFIN_CONFIG_DIRECTORY --logdir=$JELLYFIN_LOG_DIRECTORY --cachedir=$JELLYFIN_CACHE_DIRECTORY $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_FFPROBE_OPT $JELLYFIN_SERVICE_OPT $JELLFIN_NOWEBAPP_OPT"

+ 12 - 12
deployment/debian-package-x64/pkg-src/conf/jellyfin-sudoers

@@ -10,15 +10,15 @@ Cmnd_Alias STARTSERVER_INITD = /etc/init.d/jellyfin start
 Cmnd_Alias STOPSERVER_INITD = /etc/init.d/jellyfin stop
 
 
-%jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSV
-%jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSV
-%jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSV
-%jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
-%jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
-%jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
-%jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_INITD
-%jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_INITD
-%jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_INITD
+jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSV
+jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSV
+jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSV
+jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_INITD
+jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_INITD
+jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_INITD
 
 Defaults!RESTARTSERVER_SYSV !requiretty
 Defaults!STARTSERVER_SYSV !requiretty
@@ -31,7 +31,7 @@ Defaults!STARTSERVER_INITD !requiretty
 Defaults!STOPSERVER_INITD !requiretty
 
 #Allow the server to mount iso images
-%jellyfin ALL=(ALL) NOPASSWD: /bin/mount
-%jellyfin ALL=(ALL) NOPASSWD: /bin/umount
+jellyfin ALL=(ALL) NOPASSWD: /bin/mount
+jellyfin ALL=(ALL) NOPASSWD: /bin/umount
 
-Defaults:%jellyfin !requiretty
+Defaults:jellyfin !requiretty

+ 1 - 1
deployment/debian-package-x64/pkg-src/jellyfin.service

@@ -6,7 +6,7 @@ After = network.target
 Type = simple
 EnvironmentFile = /etc/default/jellyfin
 User = jellyfin
-ExecStart = /usr/bin/jellyfin --datadir ${JELLYFIN_DATA_DIRECTORY} --configdir ${JELLYFIN_CONFIG_DIRECTORY} --logdir ${JELLYFIN_LOG_DIRECTORY} --cachedir ${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPTS} ${JELLYFIN_ADD_OPTS}
+ExecStart = /usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
 Restart = on-failure
 TimeoutSec = 15
 

+ 26 - 14
deployment/fedora-package-x64/Dockerfile

@@ -1,15 +1,27 @@
 FROM fedora:29
-ARG HOME=/build
-RUN mkdir /build && \
-    dnf install -y @buildsys-build rpmdevtools dnf-plugins-core && \
-    dnf copr enable -y @dotnet-sig/dotnet && \
-    rpmdev-setuptree
-
-WORKDIR /build/rpmbuild
-COPY ./deployment/fedora-package-x64/pkg-src/jellyfin.spec SPECS
-COPY ./deployment/fedora-package-x64/pkg-src/ SOURCES
-
-RUN spectool -g -R SPECS/jellyfin.spec && \
-    rpmbuild -bs SPECS/jellyfin.spec && \
-    dnf build-dep -y SRPMS/jellyfin-*.src.rpm && \
-    rpmbuild -bb SPECS/jellyfin.spec;
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64
+ARG ARTIFACT_DIR=/dist
+ARG SDK_VERSION=2.2
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+
+# Prepare Fedora build environment
+RUN dnf update -y \
+ && dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \
+ && dnf copr enable -y @dotnet-sig/dotnet \
+ && rpmdev-setuptree \
+ && dnf install -y dotnet-sdk-${SDK_VERSION} \
+ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && mkdir -p ${SOURCE_DIR}/SPECS \
+ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \
+ && mkdir -p ${SOURCE_DIR}/SOURCES \
+ && ln -s ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/SOURCES
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]

+ 29 - 13
deployment/fedora-package-x64/clean.sh

@@ -2,17 +2,33 @@
 
 source ../common.build.sh
 
-VERSION=`get_version ../..`
-
-package_temporary_dir="`pwd`/pkg-dist-tmp"
-pkg_src_dir="`pwd`/pkg-src"
-image_name="jellyfin-rpmbuild"
-docker_sudo=""
-if ! $(id -Gn | grep -q 'docker') && [ ! ${EUID:-1000} -eq 0 ] && \
- [ ! $USER == "root" ] && ! $(echo "$OSTYPE" | grep -q "darwin"); then
-    docker_sudo=sudo
-fi
+keep_artifacts="${1}"
+
+WORKDIR="$( pwd )"
+VERSION="$( grep -A1 '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+package_source_dir="${WORKDIR}/pkg-src"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-fedora-build"
+
+rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null \
+  || sudo rm -f "${package_source_dir}/jellyfin-${VERSION}.tar.gz" &>/dev/null
 
-$docker_sudo docker image rm $image_name --force
-rm -rf "$package_temporary_dir"
-rm -rf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz"
+rm -rf "${package_temporary_dir}" &>/dev/null \
+  || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+  || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+    docker_sudo=""
+    if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+      && [[ ! ${EUID:-1000} -eq 0 ]] \
+      && [[ ! ${USER} == "root" ]] \
+      && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+        docker_sudo=sudo
+    fi
+    ${docker_sudo} docker image rm ${image_name} --force
+fi

+ 20 - 0
deployment/fedora-package-x64/docker-build.sh

@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# Builds the RPM inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+ls -al SOURCES/pkg-src/
+
+# Build RPM
+spectool -g -R SPECS/jellyfin.spec
+rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/rpm
+mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/rpm/

+ 35 - 41
deployment/fedora-package-x64/package.sh

@@ -1,38 +1,29 @@
-#!/usr/bin/env sh
+#!/usr/bin/env bash
 
 source ../common.build.sh
 
-VERSION=`get_version ../..`
+WORKDIR="$( pwd )"
+VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
 
-# TODO get the version in the package automatically. And using the changelog to decide the debian package suffix version.
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+pkg_src_dir="${WORKDIR}/pkg-src"
+current_user="$( whoami )"
+image_name="jellyfin-fedora-build"
 
-# Build a Jellyfin .rpm file with Docker on Linux
-# Places the output .rpm file in the parent directory
-
-set -o errexit
-set -o xtrace
-set -o nounset
-
-package_temporary_dir="`pwd`/pkg-dist-tmp"
-output_dir="`pwd`/pkg-dist"
-pkg_src_dir="`pwd`/pkg-src"
-current_user="`whoami`"
-image_name="jellyfin-rpmbuild"
-docker_sudo=""
-if ! $(id -Gn | grep -q 'docker') && [ ! ${EUID:-1000} -eq 0 ] && \
- [ ! $USER == "root" ] && ! $(echo "$OSTYPE" | grep -q "darwin"); then
-    docker_sudo=sudo
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+  && [[ ! ${EUID:-1000} -eq 0 ]] \
+  && [[ ! ${USER} == "root" ]] \
+  && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+    docker_sudo="sudo"
+else
+    docker_sudo=""
 fi
 
-cleanup() {
-    set +o errexit
-    $docker_sudo docker image rm $image_name --force
-    rm -rf "$package_temporary_dir"
-    rm -rf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz"
-}
-trap cleanup EXIT INT
+# Create RPM source archive
 GNU_TAR=1
-mkdir -p "$package_temporary_dir"
+mkdir -p "${package_temporary_dir}"
 echo "Bundling all sources for RPM build."
 tar \
 --transform "s,^\.,jellyfin-${VERSION}," \
@@ -47,12 +38,12 @@ tar \
 --exclude='**/.nuget' \
 --exclude='*.deb' \
 --exclude='*.rpm' \
--zcf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz" \
+-czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" \
 -C "../.." ./ || GNU_TAR=0
 
 if [ $GNU_TAR -eq 0 ]; then
     echo "The installed tar binary did not support --transform. Using workaround."
-    mkdir -p "$package_temporary_dir/jellyfin-${VERSION}"
+    mkdir -p "${package_temporary_dir}/jellyfin"
     # Not GNU tar
     tar \
     --exclude='.git*' \
@@ -67,20 +58,23 @@ if [ $GNU_TAR -eq 0 ]; then
     --exclude='*.deb' \
     --exclude='*.rpm' \
     -zcf \
-    "$package_temporary_dir/jellyfin-${VERSION}/jellyfin.tar.gz" \
-    -C "../.." \
-    ./
+    "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
+    -C "../.." ./
     echo "Extracting filtered package."
-    tar -xzf "$package_temporary_dir/jellyfin-${VERSION}/jellyfin.tar.gz" -C "$package_temporary_dir/jellyfin-${VERSION}"
+    tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
     echo "Removing filtered package."
-    rm "$package_temporary_dir/jellyfin-${VERSION}/jellyfin.tar.gz"
+    rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
     echo "Repackaging package into final tarball."
-    tar -zcf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz" -C "$package_temporary_dir" "jellyfin-${VERSION}"
+    tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
 fi
 
-$docker_sudo docker build ../.. -t "$image_name" -f ./Dockerfile
-mkdir -p "$output_dir"
-$docker_sudo docker run --rm -v "$package_temporary_dir:/temp" "$image_name" sh -c 'find /build/rpmbuild -maxdepth 3 -type f -name "jellyfin*.rpm" -exec mv {} /temp \;'
-chown -R "$current_user" "$package_temporary_dir" \
-|| sudo chown -R "$current_user" "$package_temporary_dir"
-mv "$package_temporary_dir"/*.rpm "$output_dir"
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the RPMs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+# Correct ownership on the RPMs (as current user, then as root if that fails)
+chown -R "${current_user}" "${package_temporary_dir}" \
+  || sudo chown -R "${current_user}" "${package_temporary_dir}"
+# Move the RPMs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/rpm/* "${output_dir}"

+ 13 - 6
deployment/fedora-package-x64/pkg-src/jellyfin.env

@@ -14,15 +14,22 @@
 # General options
 #
 
-# Tell jellyfin wich ffmpeg/ffprobe to use
-# JELLYFIN_FFMPEG="--ffmpeg /usr/bin/ffmpeg --ffprobe /usr/bin/ffprobe"
-
 # Program directories
 JELLYFIN_DATA_DIRECTORY="/var/lib/jellyfin"
 JELLYFIN_CONFIG_DIRECTORY="/etc/jellyfin"
 JELLYFIN_LOG_DIRECTORY="/var/log/jellyfin"
 JELLYFIN_CACHE_DIRECTORY="/var/cache/jellyfin"
+
 # In-App service control
-JELLYFIN_RESTART_OPT="--restartpath /usr/libexec/jellyfin/restart.sh"
-# Additional options for the binary
-JELLYFIN_ADD_OPTS=""
+JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
+
+# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
+#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
+#JELLYFIN_FFPROBE_OPT="--ffprobe=/usr/bin/ffprobe"
+
+# [OPTIONAL] run Jellyfin as a headless service
+#JELLYFIN_SERVICE_OPT="--service"
+
+# [OPTIONAL] run Jellyfin without the web app
+#JELLYFIN_NOWEBAPP_OPT="--noautorunwebapp"
+

+ 1 - 1
deployment/fedora-package-x64/pkg-src/jellyfin.service

@@ -5,7 +5,7 @@ Description=Jellyfin is a free software media system that puts you in control of
 [Service]
 EnvironmentFile=/etc/sysconfig/jellyfin
 WorkingDirectory=/var/lib/jellyfin
-ExecStart=/usr/bin/jellyfin --datadir ${JELLYFIN_DATA_DIRECTORY} --configdir ${JELLYFIN_CONFIG_DIRECTORY} --logdir ${JELLYFIN_LOG_DIRECTORY} --cachedir ${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_ADD_OPTS} ${JELLYFIN_FFMPEG}
+ExecStart=/usr/bin/jellyfin --datadir=${JELLYFIN_DATA_DIRECTORY} --configdir=${JELLYFIN_CONFIG_DIRECTORY} --logdir=${JELLYFIN_LOG_DIRECTORY} --cachedir=${JELLYFIN_CACHE_DIRECTORY} ${JELLYFIN_RESTART_OPT} ${JELLYFIN_FFMPEG_OPT} ${JELLYFIN_FFPROBE_OPT} ${JELLYFIN_SERVICE_OPT} ${JELLYFIN_NOWEBAPP_OPT}
 TimeoutSec=15
 Restart=on-failure
 User=jellyfin

+ 7 - 7
deployment/fedora-package-x64/pkg-src/jellyfin.sudoers

@@ -4,16 +4,16 @@ Cmnd_Alias STARTSERVER_SYSTEMD = /usr/bin/systemctl start jellyfin, /bin/systemc
 Cmnd_Alias STOPSERVER_SYSTEMD = /usr/bin/systemctl stop jellyfin, /bin/systemctl stop jellyfin
 
 
-%jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
-%jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
-%jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: RESTARTSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: STARTSERVER_SYSTEMD
+jellyfin ALL=(ALL) NOPASSWD: STOPSERVER_SYSTEMD
 
 Defaults!RESTARTSERVER_SYSTEMD !requiretty
 Defaults!STARTSERVER_SYSTEMD !requiretty
 Defaults!STOPSERVER_SYSTEMD !requiretty
 
-# Uncomment to allow the server to mount iso images
-# %jellyfin ALL=(ALL) NOPASSWD: /bin/mount
-# %jellyfin ALL=(ALL) NOPASSWD: /bin/umount
+# Allow the server to mount iso images
+jellyfin ALL=(ALL) NOPASSWD: /bin/mount
+jellyfin ALL=(ALL) NOPASSWD: /bin/umount
 
-Defaults:%jellyfin !requiretty
+Defaults:jellyfin !requiretty

+ 21 - 0
deployment/ubuntu-package-x64/Dockerfile

@@ -0,0 +1,21 @@
+FROM microsoft/dotnet:2.2-sdk-bionic
+# Docker build arguments
+ARG SOURCE_DIR=/jellyfin
+ARG PLATFORM_DIR=/jellyfin/deployment/ubuntu-package-x64
+ARG ARTIFACT_DIR=/dist
+# Docker run environment
+ENV SOURCE_DIR=/jellyfin
+ENV ARTIFACT_DIR=/dist
+ENV DEB_BUILD_OPTIONS=noddebs
+
+# Prepare Ubuntu build environment
+RUN apt-get update \
+ && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \
+ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
+ && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian
+
+VOLUME ${ARTIFACT_DIR}/
+
+COPY . ${SOURCE_DIR}/
+
+ENTRYPOINT ["/docker-build.sh"]

+ 29 - 0
deployment/ubuntu-package-x64/clean.sh

@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+
+source ../common.build.sh
+
+keep_artifacts="${1}"
+
+WORKDIR="$( pwd )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-ubuntu-build"
+
+rm -rf "${package_temporary_dir}" &>/dev/null \
+  || sudo rm -rf "${package_temporary_dir}" &>/dev/null
+
+rm -rf "${output_dir}" &>/dev/null \
+  || sudo rm -rf "${output_dir}" &>/dev/null
+
+if [[ ${keep_artifacts} == 'n' ]]; then
+    docker_sudo=""
+    if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+      && [[ ! ${EUID:-1000} -eq 0 ]] \
+      && [[ ! ${USER} == "root" ]] \
+      && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+        docker_sudo=sudo
+    fi
+    ${docker_sudo} docker image rm ${image_name} --force
+fi

+ 1 - 0
deployment/ubuntu-package-x64/dependencies.txt

@@ -0,0 +1 @@
+docker

+ 19 - 0
deployment/ubuntu-package-x64/docker-build.sh

@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Builds the DEB inside the Docker container
+
+set -o errexit
+set -o xtrace
+
+# Move to source directory
+pushd ${SOURCE_DIR}
+
+# Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image
+sed -i '/dotnet-sdk-2.2,/d' debian/control
+
+# Build DEB
+dpkg-buildpackage -us -uc
+
+# Move the artifacts out
+mkdir -p ${ARTIFACT_DIR}/deb
+mv /jellyfin_* ${ARTIFACT_DIR}/deb/

+ 31 - 0
deployment/ubuntu-package-x64/package.sh

@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+source ../common.build.sh
+
+WORKDIR="$( pwd )"
+
+package_temporary_dir="${WORKDIR}/pkg-dist-tmp"
+output_dir="${WORKDIR}/pkg-dist"
+current_user="$( whoami )"
+image_name="jellyfin-ubuntu-build"
+
+# Determine if sudo should be used for Docker
+if [[ ! -z $(id -Gn | grep -q 'docker') ]] \
+  && [[ ! ${EUID:-1000} -eq 0 ]] \
+  && [[ ! ${USER} == "root" ]] \
+  && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then
+    docker_sudo="sudo"
+else
+    docker_sudo=""
+fi
+
+# Set up the build environment Docker image
+${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile
+# Build the DEBs and copy out to ${package_temporary_dir}
+${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}"
+# Correct ownership on the DEBs (as current user, then as root if that fails)
+chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null \
+  || sudo chown -R "${current_user}" "${package_temporary_dir}" &>/dev/null
+# Move the DEBs to the output directory
+mkdir -p "${output_dir}"
+mv "${package_temporary_dir}"/deb/* "${output_dir}"

+ 1 - 0
deployment/ubuntu-package-x64/pkg-src

@@ -0,0 +1 @@
+../debian-package-x64/pkg-src

+ 8 - 0
jellyfin.ruleset

@@ -3,11 +3,19 @@
   <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
     <!-- disable warning SA1101: Prefix local calls with 'this.' -->
     <Rule Id="SA1101" Action="None" />
+    <!-- disable warning SA1130: Use lambda syntax -->
+    <Rule Id="SA1130" Action="None" />
     <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->
     <Rule Id="SA1200" Action="None" />
     <!-- disable warning SA1309: Fields must not begin with an underscore -->
     <Rule Id="SA1309" Action="None" />
+    <!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
+    <Rule Id="SA1512" Action="None" />
     <!-- disable warning SA1633: The file header is missing or not located at the top of the file -->
     <Rule Id="SA1633" Action="None" />
   </Rules>
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.FxCopAnalyzers" RuleNamespace="Microsoft.Design">
+    <!-- disable warning CA1054: Change the type of parameter url from string to System.Uri -->
+    <Rule Id="CA1054" Action="None" />
+  </Rules>
 </RuleSet>