Przeglądaj źródła

merging with master to clear merge conflict

Phallacy 6 lat temu
rodzic
commit
a0d31a49a0
100 zmienionych plików z 2788 dodań i 3024 usunięć
  1. 8 0
      .copr/Makefile
  2. 1 0
      .dockerignore
  3. 101 2
      .drone.yml
  4. 75 9
      .editorconfig
  5. 3 1
      .gitignore
  6. 2 2
      BDInfo/BDROM.cs
  7. 2 5
      BDInfo/TSPlaylistFile.cs
  8. 1 3
      BDInfo/TSStreamClipFile.cs
  9. 1 0
      CONTRIBUTORS.md
  10. 7 6
      Dockerfile
  11. 22 10
      Dockerfile.arm
  12. 18 14
      Dockerfile.arm64
  13. 3 1
      Emby.Dlna/DlnaManager.cs
  14. 2 1
      Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
  15. 48 15
      Emby.Dlna/PlayTo/Device.cs
  16. 5 5
      Emby.Dlna/PlayTo/PlayToManager.cs
  17. 1 3
      Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs
  18. 6 26
      Emby.Notifications/CoreNotificationTypes.cs
  19. 12 20
      Emby.Notifications/Notifications.cs
  20. 1 4
      Emby.Photos/PhotoProvider.cs
  21. 21 88
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  22. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  23. 1 3
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  24. 155 441
      Emby.Server.Implementations/ApplicationHost.cs
  25. 2 4
      Emby.Server.Implementations/Archiving/ZipClient.cs
  26. 1 1
      Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
  27. 6 2
      Emby.Server.Implementations/Collections/CollectionImageProvider.cs
  28. 1 3
      Emby.Server.Implementations/Collections/CollectionManager.cs
  29. 12 0
      Emby.Server.Implementations/ConfigurationOptions.cs
  30. 2 25
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  31. 1 10
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  32. 139 146
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  33. 1 11
      Emby.Server.Implementations/Devices/DeviceId.cs
  34. 2 10
      Emby.Server.Implementations/Devices/DeviceManager.cs
  35. 0 12
      Emby.Server.Implementations/Dto/DtoService.cs
  36. 3 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  37. 1 1
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  38. 2 11
      Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs
  39. 9 29
      Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
  40. 7 5
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  41. 40 94
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  42. 2 6
      Emby.Server.Implementations/HttpServer/StreamWriter.cs
  43. 1 9
      Emby.Server.Implementations/IO/FileRefresher.cs
  44. 4 17
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  45. 11 48
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  46. 4 10
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  47. 23 25
      Emby.Server.Implementations/Library/LibraryManager.cs
  48. 1 4
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  49. 2 1
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  50. 2 6
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  51. 1 1
      Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
  52. 2 2
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  53. 2 2
      Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs
  54. 1216 1219
      Emby.Server.Implementations/Library/UserManager.cs
  55. 1 3
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  56. 3 3
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  57. 8 7
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  58. 1 3
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  59. 2 3
      Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs
  60. 2 3
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  61. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  62. 1 4
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  63. 10 3
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  64. 4 5
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  65. 40 38
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  66. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  67. 25 25
      Emby.Server.Implementations/Localization/Core/da.json
  68. 27 27
      Emby.Server.Implementations/Localization/Core/de.json
  69. 1 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  70. 1 1
      Emby.Server.Implementations/Localization/Core/en-US.json
  71. 23 23
      Emby.Server.Implementations/Localization/Core/es.json
  72. 1 1
      Emby.Server.Implementations/Localization/Core/fr.json
  73. 35 35
      Emby.Server.Implementations/Localization/Core/hu.json
  74. 4 4
      Emby.Server.Implementations/Localization/Core/it.json
  75. 94 94
      Emby.Server.Implementations/Localization/Core/kk.json
  76. 3 3
      Emby.Server.Implementations/Localization/Core/ms.json
  77. 18 18
      Emby.Server.Implementations/Localization/Core/nl.json
  78. 5 5
      Emby.Server.Implementations/Localization/Core/ru.json
  79. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  80. 21 0
      Emby.Server.Implementations/Serialization/JsonSerializer.cs
  81. 5 10
      Emby.Server.Implementations/ServerApplicationPaths.cs
  82. 1 1
      Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
  83. 1 1
      Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
  84. 15 25
      Emby.Server.Implementations/Updates/InstallationManager.cs
  85. 1 1
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  86. 25 7
      Jellyfin.Server/CoreAppHost.cs
  87. 7 0
      Jellyfin.Server/Jellyfin.Server.csproj
  88. 161 102
      Jellyfin.Server/Program.cs
  89. 83 69
      Jellyfin.Server/SocketSharp/RequestMono.cs
  90. 20 24
      Jellyfin.Server/SocketSharp/SharpWebSocket.cs
  91. 37 24
      Jellyfin.Server/SocketSharp/WebSocketSharpListener.cs
  92. 53 39
      Jellyfin.Server/SocketSharp/WebSocketSharpRequest.cs
  93. 8 12
      Jellyfin.Server/SocketSharp/WebSocketSharpResponse.cs
  94. 23 9
      MediaBrowser.Api/BaseApiService.cs
  95. 2 8
      MediaBrowser.Api/EnvironmentService.cs
  96. 0 2
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  97. 4 1
      MediaBrowser.Api/Playback/Progressive/AudioService.cs
  98. 3 0
      MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
  99. 4 1
      MediaBrowser.Api/Playback/Progressive/VideoService.cs
  100. 5 1
      MediaBrowser.Api/Playback/UniversalAudioService.cs

+ 8 - 0
.copr/Makefile

@@ -0,0 +1,8 @@
+srpm:
+	dnf -y install git
+	git submodule update --init --recursive
+	cd deployment/fedora-package-x64;                    \
+	./create_tarball.sh;                                 \
+	rpmbuild -bs pkg-src/jellyfin.spec                   \
+	         --define "_sourcedir $$PWD/pkg-src/"        \
+		 --define "_srcrpmdir $(outdir)"

+ 1 - 0
.dockerignore

@@ -8,3 +8,4 @@ README.md
 deployment/*/dist
 deployment/*/pkg-dist
 deployment/collect-dist/
+ci/

+ 101 - 2
.drone.yml

@@ -1,12 +1,111 @@
+---
 kind: pipeline
-name: build
+name: build-debug
 
 steps:
 - name: submodules
   image: docker:git
   commands:
     - git submodule update --init --recursive
+
+- name: build
+  image: microsoft/dotnet:2-sdk
+  commands:
+    - dotnet publish "Jellyfin.Server" --configuration Debug --output "../ci/ci-debug"
+
+---
+kind: pipeline
+name: build-release
+
+steps:
+- name: submodules
+  image: docker:git
+  commands:
+    - git submodule update --init --recursive
+
+- name: build
+  image: microsoft/dotnet:2-sdk
+  commands:
+    - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
+
+---
+
+kind: pipeline
+name: check-abi
+
+steps:
+- name: submodules
+  image: docker:git
+  commands:
+    - git submodule update --init --recursive
+
 - name: build
   image: microsoft/dotnet:2-sdk
   commands:
-    - dotnet publish --configuration release --output /release Jellyfin.Server
+    - dotnet publish "Jellyfin.Server" --configuration Release --output "../ci/ci-release"
+
+- name: clone-dotnet-compat
+  image: docker:git
+  commands:
+    - git clone --depth 1 https://github.com/EraYaN/dotnet-compatibility ci/dotnet-compatibility
+
+- name: build-dotnet-compat
+  image: microsoft/dotnet:2-sdk
+  commands:
+    - dotnet publish "ci/dotnet-compatibility/CompatibilityCheckerCoreCLI" --configuration Release --output "../../ci-tools"
+
+- name: download-last-nuget-release-common
+  image: plugins/download
+  settings:
+    source: https://www.nuget.org/api/v2/package/Jellyfin.Common
+    destination: ci/Jellyfin.Common.nupkg
+
+- name: download-last-nuget-release-model
+  image: plugins/download
+  settings:
+    source: https://www.nuget.org/api/v2/package/Jellyfin.Model
+    destination: ci/Jellyfin.Model.nupkg
+
+- name: download-last-nuget-release-controller
+  image: plugins/download
+  settings:
+    source: https://www.nuget.org/api/v2/package/Jellyfin.Controller
+    destination: ci/Jellyfin.Controller.nupkg
+
+- name: download-last-nuget-release-naming
+  image: plugins/download
+  settings:
+    source: https://www.nuget.org/api/v2/package/Jellyfin.Naming
+    destination: ci/Jellyfin.Naming.nupkg
+
+- name: extract-downloaded-nuget-packages
+  image: garthk/unzip
+  commands:
+  - unzip -j ci/Jellyfin.Common.nupkg  "*.dll" -d ci/nuget-packages
+  - unzip -j ci/Jellyfin.Model.nupkg  "*.dll" -d ci/nuget-packages
+  - unzip -j ci/Jellyfin.Controller.nupkg  "*.dll" -d ci/nuget-packages
+  - unzip -j ci/Jellyfin.Naming.nupkg  "*.dll" -d ci/nuget-packages
+
+- name: run-dotnet-compat-common
+  image: microsoft/dotnet:2-runtime
+  err_ignore: true
+  commands:
+  - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Common.dll ci/ci-release/MediaBrowser.Common.dll
+
+- name: run-dotnet-compat-model
+  image: microsoft/dotnet:2-runtime
+  err_ignore: true
+  commands:
+  - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Model.dll ci/ci-release/MediaBrowser.Model.dll
+
+- name: run-dotnet-compat-controller
+  image: microsoft/dotnet:2-runtime
+  err_ignore: true
+  commands:
+  - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/MediaBrowser.Controller.dll ci/ci-release/MediaBrowser.Controller.dll
+
+- name: run-dotnet-compat-naming
+  image: microsoft/dotnet:2-runtime
+  err_ignore: true
+  commands:
+  - dotnet ci/ci-tools/CompatibilityCheckerCoreCLI.dll ci/nuget-packages/Emby.Naming.dll ci/ci-release/Emby.Naming.dll

+ 75 - 9
.editorconfig

@@ -15,6 +15,10 @@ insert_final_newline = true
 end_of_line = lf
 max_line_length = null
 
+# YAML indentation
+[*.{yml,yaml}]
+indent_size = 2
+
 # XML indentation
 [*.{csproj,xml}]
 indent_size = 2
@@ -55,15 +59,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
.gitignore

@@ -263,4 +263,6 @@ deployment/**/pkg-dist/
 deployment/**/pkg-dist-tmp/
 deployment/collect-dist/
 
-jellyfin_version.ini
+jellyfin_version.ini
+
+ci/

+ 2 - 2
BDInfo/BDROM.cs

@@ -165,7 +165,7 @@ namespace BDInfo
                 foreach (var file in files)
                 {
                     PlaylistFiles.Add(
-                        file.Name.ToUpper(), new TSPlaylistFile(this, file, _fileSystem));
+                        file.Name.ToUpper(), new TSPlaylistFile(this, file));
                 }
             }
 
@@ -185,7 +185,7 @@ namespace BDInfo
                 foreach (var file in files)
                 {
                     StreamClipFiles.Add(
-                        file.Name.ToUpper(), new TSStreamClipFile(file, _fileSystem));
+                        file.Name.ToUpper(), new TSStreamClipFile(file));
                 }
             }
 

+ 2 - 5
BDInfo/TSPlaylistFile.cs

@@ -28,7 +28,6 @@ namespace BDInfo
 {
     public class TSPlaylistFile
     {
-        private readonly IFileSystem _fileSystem;
         private FileSystemMetadata FileInfo = null;
         public string FileType = null;
         public bool IsInitialized = false;
@@ -64,21 +63,19 @@ namespace BDInfo
             new List<TSGraphicsStream>();
 
         public TSPlaylistFile(BDROM bdrom,
-            FileSystemMetadata fileInfo, IFileSystem fileSystem)
+            FileSystemMetadata fileInfo)
         {
             BDROM = bdrom;
             FileInfo = fileInfo;
-            _fileSystem = fileSystem;
             Name = fileInfo.Name.ToUpper();
         }
 
         public TSPlaylistFile(BDROM bdrom,
             string name,
-            List<TSStreamClip> clips, IFileSystem fileSystem)
+            List<TSStreamClip> clips)
         {
             BDROM = bdrom;
             Name = name;
-            _fileSystem = fileSystem;
             IsCustom = true;
             foreach (var clip in clips)
             {

+ 1 - 3
BDInfo/TSStreamClipFile.cs

@@ -28,7 +28,6 @@ namespace BDInfo
 {
     public class TSStreamClipFile
     {
-        private readonly IFileSystem _fileSystem;
         public FileSystemMetadata FileInfo = null;
         public string FileType = null;
         public bool IsValid = false;
@@ -37,10 +36,9 @@ namespace BDInfo
         public Dictionary<ushort, TSStream> Streams =
             new Dictionary<ushort, TSStream>();
 
-        public TSStreamClipFile(FileSystemMetadata fileInfo, IFileSystem fileSystem)
+        public TSStreamClipFile(FileSystemMetadata fileInfo)
         {
             FileInfo = fileInfo;
-            _fileSystem = fileSystem;
             Name = fileInfo.Name.ToUpper();
         }
 

+ 1 - 0
CONTRIBUTORS.md

@@ -18,6 +18,7 @@
  - [dkanada](https://github.com/dkanada)
  - [LogicalPhallacy](https://github.com/LogicalPhallacy/)
  - [RazeLighter777](https://github.com/RazeLighter777)
+ - [WillWill56](https://github.com/WillWill56)
 
 # Emby Contributors
 

+ 7 - 6
Dockerfile

@@ -3,9 +3,8 @@ ARG DOTNET_VERSION=2
 FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
 WORKDIR /repo
 COPY . .
-RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
- && dotnet clean \
- && dotnet publish \
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+RUN dotnet publish \
     --configuration release \
     --output /jellyfin \
     Jellyfin.Server
@@ -18,9 +17,11 @@ RUN apt-get update \
    libfontconfig1 \
  && apt-get clean autoclean \
  && apt-get autoremove \
- && rm -rf /var/lib/{apt,dpkg,cache,log}
+ && rm -rf /var/lib/{apt,dpkg,cache,log} \
+ && mkdir -p /cache /config /media \
+ && chmod 777 /cache /config /media
 COPY --from=ffmpeg / /
 COPY --from=builder /jellyfin /jellyfin
 EXPOSE 8096
-VOLUME /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config
+VOLUME /cache /config /media
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache

+ 22 - 10
Dockerfile.arm

@@ -1,24 +1,36 @@
+# Requires binfm_misc registration
+# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=3.0
 
 
-FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch-arm32v7 as builder
+FROM multiarch/qemu-user-static:x86_64-arm as qemu
+FROM alpine as qemu_extract
+COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
+RUN tar -xzvf qemu-arm-static.tar.gz
+
+FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
 WORKDIR /repo
 COPY . .
-#TODO Remove or update the sed line when we update dotnet version.
-RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
- && find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; \
- && dotnet clean -maxcpucount:1 \
- && dotnet publish \
-    -maxcpucount:1 \
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# TODO Remove or update the sed line when we update dotnet version.
+RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish \
+    -r linux-arm \
     --configuration release \
     --output /jellyfin \
     Jellyfin.Server
 
 
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
+COPY --from=qemu_extract qemu-arm-static /usr/bin
 RUN apt-get update \
- && apt-get install -y ffmpeg
+ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ && mkdir -p /cache /config /media \
+ && chmod 777 /cache /config /media
 COPY --from=builder /jellyfin /jellyfin
 EXPOSE 8096
-VOLUME /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config
+VOLUME /cache /config /media
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache

+ 18 - 14
Dockerfile.arm64

@@ -1,33 +1,37 @@
-# Requires binfm_misc registration for aarch64
+# Requires binfm_misc registration
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=3.0
 
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 FROM alpine as qemu_extract
-COPY --from=qemu /usr/bin qemu_user_static.tgz
-RUN tar -xzvf qemu_user_static.tgz
+COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
+RUN tar -xzvf qemu-aarch64-static.tar.gz
 
 
-FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch-arm64v8 as builder
-COPY --from=qemu_extract qemu-* /usr/bin
+FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
 WORKDIR /repo
 COPY . .
-#TODO Remove or update the sed line when we update dotnet version.
-RUN export DOTNET_CLI_TELEMETRY_OPTOUT=1 \
- && find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; \
- && dotnet clean \
- && dotnet publish \
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+# TODO Remove or update the sed line when we update dotnet version.
+RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \;
+# Discard objs - may cause failures if exists
+RUN find . -type d -name obj | xargs -r rm -r
+# Build
+RUN dotnet publish \
+    -r linux-arm64 \
     --configuration release \
     --output /jellyfin \
     Jellyfin.Server
 
 
 FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
+COPY --from=qemu_extract qemu-aarch64-static /usr/bin
 RUN apt-get update \
- && apt-get install -y ffmpeg
-COPY --from=qemu_extract qemu-* /usr/bin
+ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
+ && mkdir -p /cache /config /media \
+ && chmod 777 /cache /config /media
 COPY --from=builder /jellyfin /jellyfin
 EXPOSE 8096
-VOLUME /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config
+VOLUME /cache /config /media
+ENTRYPOINT dotnet /jellyfin/jellyfin.dll --datadir /config --cachedir /cache

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

+ 48 - 15
Emby.Dlna/PlayTo/Device.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using System.Xml;
 using System.Xml.Linq;
 using Emby.Dlna.Common;
 using Emby.Dlna.Server;
@@ -733,26 +734,21 @@ namespace Emby.Dlna.PlayTo
                 return (true, null);
             }
 
-            XElement uPnpResponse;
+            XElement uPnpResponse = null;
 
-            // Handle different variations sent back by devices
             try
             {
-                uPnpResponse = XElement.Parse(trackString);
+                uPnpResponse = ParseResponse(trackString);
             }
-            catch (Exception)
+            catch (Exception ex)
             {
-                // first try to add a root node with a dlna namesapce
-                try
-                {
-                    uPnpResponse = XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + trackString + "</data>");
-                    uPnpResponse = uPnpResponse.Descendants().First();
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Unable to parse xml {0}", trackString);
-                    return (true, null);
-                }
+                _logger.LogError(ex, "Uncaught exception while parsing xml");
+            }
+
+            if (uPnpResponse == null)
+            {
+                _logger.LogError("Failed to parse xml: \n {Xml}", trackString);
+                return (true, null);
             }
 
             var e = uPnpResponse.Element(uPnpNamespaces.items);
@@ -762,6 +758,43 @@ namespace Emby.Dlna.PlayTo
             return (true, uTrack);
         }
 
+        private XElement ParseResponse(string xml)
+        {
+            // Handle different variations sent back by devices
+            try
+            {
+                return XElement.Parse(xml);
+            }
+            catch (XmlException)
+            {
+
+            }
+
+            // first try to add a root node with a dlna namesapce
+            try
+            {
+                return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")
+                                .Descendants()
+                                .First();
+            }
+            catch (XmlException)
+            {
+
+            }
+
+            // some devices send back invalid xml
+            try
+            {
+                return XElement.Parse(xml.Replace("&", "&amp;"));
+            }
+            catch (XmlException)
+            {
+
+            }
+
+            return null;
+        }
+
         private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
         {
             if (container == null)

+ 5 - 5
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -89,11 +89,6 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
-            {
-                return;
-            }
-
             var cancellationToken = _disposeCancellationTokenSource.Token;
 
             await _sessionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -105,6 +100,11 @@ namespace Emby.Dlna.PlayTo
                     return;
                 }
 
+                if (_sessionManager.Sessions.Any(i => usn.IndexOf(i.DeviceId, StringComparison.OrdinalIgnoreCase) != -1))
+                {
+                    return;
+                }
+
                 await AddDevice(info, location, cancellationToken).ConfigureAwait(false);
             }
             catch (OperationCanceledException)

+ 1 - 3
Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs

@@ -19,7 +19,6 @@ namespace IsoMounter
 
         private readonly IEnvironmentInfo EnvironmentInfo;
         private readonly bool ExecutablesAvailable;
-        private readonly IFileSystem FileSystem;
         private readonly ILogger _logger;
         private readonly string MountCommand;
         private readonly string MountPointRoot;
@@ -31,11 +30,10 @@ namespace IsoMounter
 
         #region Constructor(s)
 
-        public LinuxIsoManager(ILogger logger, IFileSystem fileSystem, IEnvironmentInfo environment, IProcessFactory processFactory)
+        public LinuxIsoManager(ILogger logger, IEnvironmentInfo environment, IProcessFactory processFactory)
         {
 
             EnvironmentInfo = environment;
-            FileSystem = fileSystem;
             _logger = logger;
             ProcessFactory = processFactory;
 

+ 6 - 26
Emby.Notifications/CoreNotificationTypes.cs

@@ -11,101 +11,81 @@ namespace Emby.Notifications
     public class CoreNotificationTypes : INotificationTypeFactory
     {
         private readonly ILocalizationManager _localization;
-        private readonly IServerApplicationHost _appHost;
 
-        public CoreNotificationTypes(ILocalizationManager localization, IServerApplicationHost appHost)
+        public CoreNotificationTypes(ILocalizationManager localization)
         {
             _localization = localization;
-            _appHost = appHost;
         }
 
         public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
         {
-            var knownTypes = new List<NotificationTypeInfo>
+            var knownTypes = new NotificationTypeInfo[]
             {
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.ApplicationUpdateInstalled.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.InstallationFailed.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.PluginInstalled.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.PluginError.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.PluginUninstalled.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.PluginUpdateInstalled.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.ServerRestartRequired.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.TaskFailed.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.NewLibraryContent.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.AudioPlayback.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.VideoPlayback.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.AudioPlaybackStopped.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.VideoPlaybackStopped.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.CameraImageUploaded.ToString()
                 },
-
                 new NotificationTypeInfo
                 {
                      Type = NotificationType.UserLockedOut.ToString()
-                }
-            };
-
-            if (!_appHost.CanSelfUpdate)
-            {
-                knownTypes.Add(new NotificationTypeInfo
+                },
+                new NotificationTypeInfo
                 {
                     Type = NotificationType.ApplicationUpdateAvailable.ToString()
-                });
-            }
+                }
+            };
 
             foreach (var type in knownTypes)
             {

+ 12 - 20
Emby.Notifications/Notifications.cs

@@ -5,21 +5,17 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Notifications;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Notifications;
-using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Notifications
@@ -29,43 +25,40 @@ namespace Emby.Notifications
     /// </summary>
     public class Notifications : IServerEntryPoint
     {
-        private readonly IInstallationManager _installationManager;
-        private readonly IUserManager _userManager;
         private readonly ILogger _logger;
 
-        private readonly ITaskManager _taskManager;
         private readonly INotificationManager _notificationManager;
 
         private readonly ILibraryManager _libraryManager;
-        private readonly ISessionManager _sessionManager;
         private readonly IServerApplicationHost _appHost;
 
         private Timer LibraryUpdateTimer { get; set; }
         private readonly object _libraryChangedSyncLock = new object();
 
         private readonly IConfigurationManager _config;
-        private readonly IDeviceManager _deviceManager;
         private readonly ILocalizationManager _localization;
         private readonly IActivityManager _activityManager;
 
         private string[] _coreNotificationTypes;
 
-        public Notifications(IInstallationManager installationManager, IActivityManager activityManager, ILocalizationManager localization, IUserManager userManager, ILogger logger, ITaskManager taskManager, INotificationManager notificationManager, ILibraryManager libraryManager, ISessionManager sessionManager, IServerApplicationHost appHost, IConfigurationManager config, IDeviceManager deviceManager)
+        public Notifications(
+            IActivityManager activityManager,
+            ILocalizationManager localization,
+            ILogger logger,
+            INotificationManager notificationManager,
+            ILibraryManager libraryManager,
+            IServerApplicationHost appHost,
+            IConfigurationManager config)
         {
-            _installationManager = installationManager;
-            _userManager = userManager;
             _logger = logger;
-            _taskManager = taskManager;
             _notificationManager = notificationManager;
             _libraryManager = libraryManager;
-            _sessionManager = sessionManager;
             _appHost = appHost;
             _config = config;
-            _deviceManager = deviceManager;
             _localization = localization;
             _activityManager = activityManager;
 
-            _coreNotificationTypes = new CoreNotificationTypes(localization, appHost).GetNotificationTypes().Select(i => i.Type).ToArray();
+            _coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
         }
 
         public Task RunAsync()
@@ -124,10 +117,9 @@ namespace Emby.Notifications
             return _config.GetConfiguration<NotificationOptions>("notifications");
         }
 
-        async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
+        private async void _appHost_HasUpdateAvailableChanged(object sender, EventArgs e)
         {
-            // This notification is for users who can't auto-update (aka running as service)
-            if (!_appHost.HasUpdateAvailable || _appHost.CanSelfUpdate)
+            if (!_appHost.HasUpdateAvailable)
             {
                 return;
             }
@@ -145,7 +137,7 @@ namespace Emby.Notifications
         }
 
         private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
-        void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
+        private void _libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
         {
             if (!FilterItem(e.Item))
             {

+ 1 - 4
Emby.Photos/PhotoProvider.cs

@@ -9,7 +9,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Drawing;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 using TagLib;
 using TagLib.IFD;
@@ -21,13 +20,11 @@ namespace Emby.Photos
     public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
     {
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
         private IImageProcessor _imageProcessor;
 
-        public PhotoProvider(ILogger logger, IFileSystem fileSystem, IImageProcessor imageProcessor)
+        public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
         {
             _logger = logger;
-            _fileSystem = fileSystem;
             _imageProcessor = imageProcessor;
         }
 

+ 21 - 88
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.
@@ -77,61 +72,17 @@ namespace Emby.Server.Implementations.AppBase
         /// <value>The plugin configurations path.</value>
         public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
 
-        /// <summary>
-        /// Gets the path to where temporary update files will be stored
-        /// </summary>
-        /// <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 +90,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

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -79,7 +79,7 @@ namespace Emby.Server.Implementations.AppBase
             get
             {
                 // Lazy load
-                LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer, FileSystem));
+                LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer));
                 return _configuration;
             }
             protected set

+ 1 - 3
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -1,7 +1,6 @@
 using System;
 using System.IO;
 using System.Linq;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 
 namespace Emby.Server.Implementations.AppBase
@@ -18,9 +17,8 @@ namespace Emby.Server.Implementations.AppBase
         /// <param name="type">The type.</param>
         /// <param name="path">The path.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        /// <param name="fileSystem">The file system</param>
         /// <returns>System.Object.</returns>
-        public static object GetXmlConfiguration(Type type, string path, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+        public static object GetXmlConfiguration(Type type, string path, IXmlSerializer xmlSerializer)
         {
             object configuration;
 

+ 155 - 441
Emby.Server.Implementations/ApplicationHost.cs

@@ -104,9 +104,10 @@ using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.DependencyInjection;
 using ServiceStack;
-using ServiceStack.Text.Jsv;
 using X509Certificate = System.Security.Cryptography.X509Certificates.X509Certificate;
 
 namespace Emby.Server.Implementations
@@ -122,12 +123,6 @@ namespace Emby.Server.Implementations
         /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
         public abstract bool CanSelfRestart { get; }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance can self update.
-        /// </summary>
-        /// <value><c>true</c> if this instance can self update; otherwise, <c>false</c>.</value>
-        public virtual bool CanSelfUpdate => false;
-
         public virtual bool CanLaunchWebBrowser
         {
             get
@@ -202,7 +197,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 +214,6 @@ namespace Emby.Server.Implementations
 
         protected IEnvironmentInfo EnvironmentInfo { get; set; }
 
-        private IBlurayExaminer BlurayExaminer { get; set; }
-
         public PackageVersionClass SystemUpdateLevel
         {
             get
@@ -232,12 +225,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 +297,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; }
@@ -325,6 +312,8 @@ namespace Emby.Server.Implementations
         private IMediaSourceManager MediaSourceManager { get; set; }
         private IPlaylistManager PlaylistManager { get; set; }
 
+        private readonly IConfiguration _configuration;
+
         /// <summary>
         /// Gets or sets the installation manager.
         /// </summary>
@@ -363,8 +352,10 @@ namespace Emby.Server.Implementations
             IFileSystem fileSystem,
             IEnvironmentInfo environmentInfo,
             IImageEncoder imageEncoder,
-            INetworkManager networkManager)
+            INetworkManager networkManager,
+            IConfiguration configuration)
         {
+            _configuration = configuration;
 
             // hack alert, until common can target .net core
             BaseExtensions.CryptographyProvider = CryptographyProvider;
@@ -440,7 +431,7 @@ namespace Emby.Server.Implementations
             {
                 if (_deviceId == null)
                 {
-                    _deviceId = new DeviceId(ApplicationPaths, LoggerFactory, FileSystemManager);
+                    _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
                 }
 
                 return _deviceId.Value;
@@ -453,138 +444,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.LogDebug("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 +507,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 +523,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 +576,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 +606,7 @@ namespace Emby.Server.Implementations
 
             SetHttpLimit();
 
-            await RegisterResources();
+            await RegisterResources(serviceCollection);
 
             FindParts();
         }
@@ -736,104 +621,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);
+            InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime);
+            serviceCollection.AddSingleton(InstallationManager);
 
-            ZipClient = new ZipClient(FileSystemManager);
-            RegisterSingleInstance(ZipClient);
+            ZipClient = new 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, CryptographyProvider);
-            RegisterSingleInstance(UserManager);
+            UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
+            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, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager, EnvironmentInfo);
-            RegisterSingleInstance(LibraryMonitor);
+            LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager, EnvironmentInfo);
+            serviceCollection.AddSingleton(LibraryMonitor);
 
-            RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LoggerFactory, LibraryManager, UserManager));
+            serviceCollection.AddSingleton<ISearchEngine>(new SearchEngine(LoggerFactory, LibraryManager, UserManager));
 
             CertificateInfo = GetCertificateInfo(true);
             Certificate = GetCertificate(CertificateInfo);
@@ -841,88 +725,88 @@ namespace Emby.Server.Implementations
             HttpServer = new HttpListenerHost(this,
                 LoggerFactory,
                 ServerConfigurationManager,
-                "web/index.html",
+                _configuration,
                 NetworkManager,
                 JsonSerializer,
-                XmlSerializer,
-                GetParseFn);
+                XmlSerializer);
 
             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, LoggerFactory, NetworkManager);
-            RegisterSingleInstance(DeviceManager);
+            DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
+            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, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager);
-            RegisterSingleInstance(DtoService);
+            DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
+            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 +819,8 @@ namespace Emby.Server.Implementations
             ((UserDataManager)UserDataManager).Repository = userDataRepo;
             ItemRepository.Initialize(userDataRepo, UserManager);
             ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
+
+            _serviceProvider = serviceCollection.BuildServiceProvider();
         }
 
         protected virtual IBrotliCompressor CreateBrotliCompressor()
@@ -942,11 +828,6 @@ namespace Emby.Server.Implementations
             return null;
         }
 
-        private static Func<string, object> GetParseFn(Type propertyType)
-        {
-            return s => JsvReader.GetParseFn(propertyType)(s);
-        }
-
         public virtual string PackageRuntime => "netcore";
 
         public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths, EnvironmentInfo.EnvironmentInfo environmentInfo)
@@ -1058,7 +939,7 @@ namespace Emby.Server.Implementations
 
         protected virtual FFMpegInfo GetFFMpegInfo()
         {
-            return new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, GetFfmpegInstallInfo())
+            return new FFMpegLoader(ApplicationPaths, FileSystemManager, GetFfmpegInstallInfo())
                 .GetFFMpegInfo(StartupOptions);
         }
 
@@ -1066,7 +947,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 +979,7 @@ namespace Emby.Server.Implementations
                 5000);
 
             MediaEncoder = mediaEncoder;
-            RegisterSingleInstance(MediaEncoder);
+            serviceCollection.AddSingleton(MediaEncoder);
         }
 
         /// <summary>
@@ -1174,7 +1055,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 +1092,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 +1144,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)
+            AllConcreteTypes = GetComposablePartAssemblies()
+                .SelectMany(x => x.ExportedTypes)
+                .Where(type =>
                 {
-                    Logger.LogInformation("Loading {assemblyName}", assembly.FullName);
-                }
-                else
-                {
-                    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 +1363,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)));
+            yield return typeof(ArtistNfoProvider).Assembly;
 
-            list.AddRange(GetAssembliesWithPartsInternal().Select(i => new Tuple<Assembly, string>(i, null)));
-
-            return list.ToList();
-        }
-
-        protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
-
-        private List<Tuple<Assembly, string>> GetPluginAssemblies(string path)
-        {
-            try
-            {
-                return FilterAssembliesToLoad(Directory.EnumerateFiles(path, "*.dll", SearchOption.TopDirectoryOnly))
-                    .Select(LoadAssembly)
-                    .Where(a => a != null)
-                    .ToList();
-            }
-            catch (DirectoryNotFoundException)
+            foreach (var i in GetAssembliesWithPartsInternal())
             {
-                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,9 +1448,8 @@ namespace Emby.Server.Implementations
                 SupportsHttps = SupportsHttps,
                 HttpsPortNumber = HttpsPort,
                 OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(),
-                OperatingSystemDisplayName = OperatingSystemDisplayName,
+                OperatingSystemDisplayName = EnvironmentInfo.OperatingSystemName,
                 CanSelfRestart = CanSelfRestart,
-                CanSelfUpdate = CanSelfUpdate,
                 CanLaunchWebBrowser = CanLaunchWebBrowser,
                 WanAddress = wanAddress,
                 HasUpdateAvailable = HasUpdateAvailable,
@@ -1788,7 +1517,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
@@ -2019,21 +1748,6 @@ namespace Emby.Server.Implementations
             Plugins = list.ToArray();
         }
 
-        /// <summary>
-        /// Updates the application.
-        /// </summary>
-        /// <param name="package">The package that contains the update</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="progress">The progress.</param>
-        public async Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            await InstallationManager.InstallPackage(package, false, progress, cancellationToken).ConfigureAwait(false);
-
-            HasUpdateAvailable = false;
-
-            OnApplicationUpdated(package);
-        }
-
         /// <summary>
         /// This returns localhost in the case of no external dns, and the hostname if the
         /// dns is prefixed with a valid Uri prefix.

+ 2 - 4
Emby.Server.Implementations/Archiving/ZipClient.cs

@@ -14,11 +14,9 @@ namespace Emby.Server.Implementations.Archiving
     /// </summary>
     public class ZipClient : IZipClient
     {
-        private readonly IFileSystem _fileSystem;
-
-        public ZipClient(IFileSystem fileSystem)
+        public ZipClient()
         {
-            _fileSystem = fileSystem;
+
         }
 
         /// <summary>

+ 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;

+ 6 - 2
Emby.Server.Implementations/Collections/CollectionImageProvider.cs

@@ -10,14 +10,18 @@ using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Collections
 {
     public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet>
     {
-        public CollectionImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) : base(fileSystem, providerManager, applicationPaths, imageProcessor)
+        public CollectionImageProvider(
+            IFileSystem fileSystem,
+            IProviderManager providerManager,
+            IApplicationPaths applicationPaths,
+            IImageProcessor imageProcessor)
+            : base(fileSystem, providerManager, applicationPaths, imageProcessor)
         {
         }
 

+ 1 - 3
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -342,14 +342,12 @@ namespace Emby.Server.Implementations.Collections
     {
         private readonly CollectionManager _collectionManager;
         private readonly IServerConfigurationManager _config;
-        private readonly IFileSystem _fileSystem;
         private ILogger _logger;
 
-        public CollectionManagerEntryPoint(ICollectionManager collectionManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger)
+        public CollectionManagerEntryPoint(ICollectionManager collectionManager, IServerConfigurationManager config, ILogger logger)
         {
             _collectionManager = (CollectionManager)collectionManager;
             _config = config;
-            _fileSystem = fileSystem;
             _logger = logger;
         }
 

+ 12 - 0
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace Emby.Server.Implementations
+{
+    public static class ConfigurationOptions
+    {
+        public static readonly Dictionary<string, string> Configuration = new Dictionary<string, string>
+        {
+            {"HttpListenerHost:DefaultRedirectPath", "web/index.html"}
+        };
+    }
+}

+ 2 - 25
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -224,7 +224,7 @@ namespace Emby.Server.Implementations.Data
                 });
             }
 
-            db.ExecuteAll(string.Join(";", queries.ToArray()));
+            db.ExecuteAll(string.Join(";", queries));
             Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
         }
 
@@ -232,23 +232,6 @@ namespace Emby.Server.Implementations.Data
 
         protected virtual int? CacheSize => null;
 
-        internal static void CheckOk(int rc)
-        {
-            string msg = "";
-
-            if (raw.SQLITE_OK != rc)
-            {
-                throw CreateException((ErrorCode)rc, msg);
-            }
-        }
-
-        internal static Exception CreateException(ErrorCode rc, string msg)
-        {
-            var exp = new Exception(msg);
-
-            return exp;
-        }
-
         private bool _disposed;
         protected void CheckDisposed()
         {
@@ -375,13 +358,6 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public class DummyToken : IDisposable
-        {
-            public void Dispose()
-            {
-            }
-        }
-
         public static IDisposable Read(this ReaderWriterLockSlim obj)
         {
             //if (BaseSqliteRepository.ThreadSafeMode > 0)
@@ -390,6 +366,7 @@ namespace Emby.Server.Implementations.Data
             //}
             return new WriteLockToken(obj);
         }
+
         public static IDisposable Write(this ReaderWriterLockSlim obj)
         {
             //if (BaseSqliteRepository.ThreadSafeMode > 0)

+ 1 - 10
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -1,11 +1,8 @@
 using System;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Data
@@ -13,18 +10,12 @@ namespace Emby.Server.Implementations.Data
     public class CleanDatabaseScheduledTask : ILibraryPostScanTask
     {
         private readonly ILibraryManager _libraryManager;
-        private readonly IItemRepository _itemRepo;
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
-        private readonly IApplicationPaths _appPaths;
 
-        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem, IApplicationPaths appPaths)
+        public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger logger)
         {
             _libraryManager = libraryManager;
-            _itemRepo = itemRepo;
             _logger = logger;
-            _fileSystem = fileSystem;
-            _appPaths = appPaths;
         }
 
         public Task Run(IProgress<double> progress, CancellationToken cancellationToken)

+ 139 - 146
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -536,7 +536,7 @@ namespace Emby.Server.Implementations.Data
                 throw new ArgumentNullException(nameof(item));
             }
 
-            SaveItems(new List<BaseItem> { item }, cancellationToken);
+            SaveItems(new [] { item }, cancellationToken);
         }
 
         public void SaveImages(BaseItem item)
@@ -576,7 +576,7 @@ namespace Emby.Server.Implementations.Data
         /// or
         /// cancellationToken
         /// </exception>
-        public void SaveItems(List<BaseItem> items, CancellationToken cancellationToken)
+        public void SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
         {
             if (items == null)
             {
@@ -587,7 +587,7 @@ namespace Emby.Server.Implementations.Data
 
             CheckDisposed();
 
-            var tuples = new List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>>();
+            var tuples = new List<(BaseItem, List<Guid>, BaseItem, string, List<string>)>();
             foreach (var item in items)
             {
                 var ancestorIds = item.SupportsAncestors ?
@@ -599,7 +599,7 @@ namespace Emby.Server.Implementations.Data
                 var userdataKey = item.GetUserDataKeys().FirstOrDefault();
                 var inheritedTags = item.GetInheritedTags();
 
-                tuples.Add(new Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>(item, ancestorIds, topParent, userdataKey, inheritedTags));
+                tuples.Add((item, ancestorIds, topParent, userdataKey, inheritedTags));
             }
 
             using (WriteLock.Write())
@@ -615,7 +615,7 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        private void SaveItemsInTranscation(IDatabaseConnection db, List<Tuple<BaseItem, List<Guid>, BaseItem, string, List<string>>> tuples)
+        private void SaveItemsInTranscation(IDatabaseConnection db, IEnumerable<(BaseItem, List<Guid>, BaseItem, string, List<string>)> tuples)
         {
             var statements = PrepareAllSafe(db, new string[]
             {
@@ -966,7 +966,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item.ExtraIds.Length > 0)
             {
-                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds.ToArray()));
+                saveItemStatement.TryBind("@ExtraIds", string.Join("|", item.ExtraIds));
             }
             else
             {
@@ -1183,9 +1183,9 @@ namespace Emby.Server.Implementations.Data
         /// <exception cref="ArgumentException"></exception>
         public BaseItem RetrieveItem(Guid id)
         {
-            if (id.Equals(Guid.Empty))
+            if (id == Guid.Empty)
             {
-                throw new ArgumentNullException(nameof(id));
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
             }
 
             CheckDisposed();
@@ -2079,14 +2079,14 @@ namespace Emby.Server.Implementations.Data
                 return false;
             }
 
-            var sortingFields = query.OrderBy.Select(i => i.Item1);
+            var sortingFields = new HashSet<string>(query.OrderBy.Select(i => i.Item1), StringComparer.OrdinalIgnoreCase);
 
-            return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase)
-                    || sortingFields.Contains(ItemSortBy.SeriesDatePlayed, StringComparer.OrdinalIgnoreCase)
+            return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
+                    || sortingFields.Contains(ItemSortBy.IsPlayed)
+                    || sortingFields.Contains(ItemSortBy.IsUnplayed)
+                    || sortingFields.Contains(ItemSortBy.PlayCount)
+                    || sortingFields.Contains(ItemSortBy.DatePlayed)
+                    || sortingFields.Contains(ItemSortBy.SeriesDatePlayed)
                     || query.IsFavoriteOrLiked.HasValue
                     || query.IsFavorite.HasValue
                     || query.IsResumable.HasValue
@@ -2094,9 +2094,9 @@ namespace Emby.Server.Implementations.Data
                     || query.IsLiked.HasValue;
         }
 
-        private readonly List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields))
+        private readonly ItemFields[] _allFields = Enum.GetNames(typeof(ItemFields))
             .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
-            .ToList();
+            .ToArray();
 
         private string[] GetColumnNamesFromField(ItemFields field)
         {
@@ -2151,18 +2151,26 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        private bool HasProgramAttributes(InternalItemsQuery query)
+        private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "Series",
+            "Season",
+            "MusicAlbum",
+            "MusicArtist",
+            "PhotoAlbum"
+        };
+
+        private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Program",
+            "TvChannel",
+            "LiveTvProgram",
+            "LiveTvTvChannel"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasProgramAttributes(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2172,29 +2180,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Program",
-                "TvChannel",
-                "LiveTvProgram",
-                "LiveTvTvChannel"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _programTypes.Contains(x));
         }
 
-        private bool HasServiceName(InternalItemsQuery query)
+        private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "TvChannel",
+            "LiveTvTvChannel"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasServiceName(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2204,27 +2201,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "TvChannel",
-                "LiveTvTvChannel"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _serviceTypes.Contains(x));
         }
 
-        private bool HasStartDate(InternalItemsQuery query)
+        private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "MusicAlbum",
-                "MusicArtist",
-                "PhotoAlbum"
-            };
+            "Program",
+            "LiveTvProgram"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasStartDate(InternalItemsQuery query)
+        {
+            if (_programExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2234,13 +2222,7 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Program",
-                "LiveTvProgram"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _startDateTypes.Contains(x));
         }
 
         private bool HasEpisodeAttributes(InternalItemsQuery query)
@@ -2263,16 +2245,26 @@ namespace Emby.Server.Implementations.Data
             return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
         }
 
-        private bool HasArtistFields(InternalItemsQuery query)
+
+        private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
-            var excludeParentTypes = new string[]
-            {
-                "Series",
-                "Season",
-                "PhotoAlbum"
-            };
+            "Series",
+            "Season",
+            "PhotoAlbum"
+        };
+
+        private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Audio",
+            "MusicAlbum",
+            "MusicVideo",
+            "AudioBook",
+            "AudioPodcast"
+        };
 
-            if (excludeParentTypes.Contains(query.ParentType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+        private bool HasArtistFields(InternalItemsQuery query)
+        {
+            if (_artistExcludeParentTypes.Contains(query.ParentType))
             {
                 return false;
             }
@@ -2282,18 +2274,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Audio",
-                "MusicAlbum",
-                "MusicVideo",
-                "AudioBook",
-                "AudioPodcast"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _artistsTypes.Contains(x));
         }
 
+        private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "Audio",
+            "MusicAlbum",
+            "MusicVideo",
+            "AudioBook",
+            "AudioPodcast"
+        };
+
         private bool HasSeriesFields(InternalItemsQuery query)
         {
             if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
@@ -2306,26 +2298,18 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            var types = new string[]
-            {
-                "Book",
-                "AudioBook",
-                "Episode",
-                "Season"
-            };
-
-            return types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase));
+            return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
         }
 
-        private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns)
+        private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
         {
             var list = startColumns.ToList();
 
-            foreach (var field in allFields)
+            foreach (var field in _allFields)
             {
                 if (!HasField(query, field))
                 {
-                    foreach (var fieldToRemove in GetColumnNamesFromField(field).ToList())
+                    foreach (var fieldToRemove in GetColumnNamesFromField(field))
                     {
                         list.Remove(fieldToRemove);
                     }
@@ -2419,11 +2403,14 @@ namespace Emby.Server.Implementations.Data
 
                 list.Add(builder.ToString());
 
-                var excludeIds = query.ExcludeItemIds.ToList();
-                excludeIds.Add(item.Id);
-                excludeIds.AddRange(item.ExtraIds);
+                var oldLen = query.ExcludeItemIds.Length;
+                var newLen = oldLen + item.ExtraIds.Length + 1;
+                var excludeIds = new Guid[newLen];
+                query.ExcludeItemIds.CopyTo(excludeIds, 0);
+                excludeIds[oldLen] = item.Id;
+                item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
 
-                query.ExcludeItemIds = excludeIds.ToArray();
+                query.ExcludeItemIds = excludeIds;
                 query.ExcludeProviderIds = item.ProviderIds;
             }
 
@@ -2444,7 +2431,7 @@ namespace Emby.Server.Implementations.Data
                 list.Add(builder.ToString());
             }
 
-            return list.ToArray();
+            return list;
         }
 
         private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@@ -2723,18 +2710,17 @@ namespace Emby.Server.Implementations.Data
 
         private void AddItem(List<BaseItem> items, BaseItem newItem)
         {
-            var providerIds = newItem.ProviderIds.ToList();
-
             for (var i = 0; i < items.Count; i++)
             {
                 var item = items[i];
 
-                foreach (var providerId in providerIds)
+                foreach (var providerId in newItem.ProviderIds)
                 {
                     if (providerId.Key == MetadataProviders.TmdbCollection.ToString())
                     {
                         continue;
                     }
+
                     if (item.GetProviderId(providerId.Key) == providerId.Value)
                     {
                         if (newItem.SourceType == SourceType.Library)
@@ -2753,10 +2739,10 @@ namespace Emby.Server.Implementations.Data
         {
             var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds;
 
-            int slowThreshold = 1000;
+            int slowThreshold = 100;
 
 #if DEBUG
-            slowThreshold = 250;
+            slowThreshold = 10;
 #endif
 
             if (elapsed >= slowThreshold)
@@ -2806,7 +2792,7 @@ namespace Emby.Server.Implementations.Data
 
             var whereText = whereClauses.Count == 0 ?
                 string.Empty :
-                " where " + string.Join(" AND ", whereClauses.ToArray());
+                " where " + string.Join(" AND ", whereClauses);
 
             commandText += whereText
                         + GetGroupBy(query)
@@ -2930,25 +2916,31 @@ namespace Emby.Server.Implementations.Data
 
         private string GetOrderByText(InternalItemsQuery query)
         {
-            var orderBy = query.OrderBy.ToList();
-            var enableOrderInversion = false;
-
-            if (query.SimilarTo != null && orderBy.Count == 0)
+            if (string.IsNullOrEmpty(query.SearchTerm))
             {
-                orderBy.Add(new ValueTuple<string, SortOrder>("SimilarityScore", SortOrder.Descending));
-                orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
-            }
+                int oldLen = query.OrderBy.Length;
 
-            if (!string.IsNullOrEmpty(query.SearchTerm))
+                if (query.SimilarTo != null && oldLen == 0)
+                {
+                    var arr = new (string, SortOrder)[oldLen + 2];
+                    query.OrderBy.CopyTo(arr, 0);
+                    arr[oldLen] = ("SimilarityScore", SortOrder.Descending);
+                    arr[oldLen + 1] = (ItemSortBy.Random, SortOrder.Ascending);
+                    query.OrderBy = arr;
+                }
+            }
+            else
             {
-                orderBy = new List<(string, SortOrder)>();
-                orderBy.Add(new ValueTuple<string, SortOrder>("SearchScore", SortOrder.Descending));
-                orderBy.Add(new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending));
+                query.OrderBy = new []
+                {
+                    ("SearchScore", SortOrder.Descending),
+                    (ItemSortBy.SortName, SortOrder.Ascending)
+                };
             }
 
-            query.OrderBy = orderBy.ToArray();
+            var orderBy = query.OrderBy;
 
-            if (orderBy.Count == 0)
+            if (orderBy.Length == 0)
             {
                 return string.Empty;
             }
@@ -2957,6 +2949,7 @@ namespace Emby.Server.Implementations.Data
             {
                 var columnMap = MapOrderByField(i.Item1, query);
                 var columnAscending = i.Item2 == SortOrder.Ascending;
+                const bool enableOrderInversion = false;
                 if (columnMap.Item2 && enableOrderInversion)
                 {
                     columnAscending = !columnAscending;
@@ -2968,7 +2961,7 @@ namespace Emby.Server.Implementations.Data
             }));
         }
 
-        private ValueTuple<string, bool> MapOrderByField(string name, InternalItemsQuery query)
+        private (string, bool) MapOrderByField(string name, InternalItemsQuery query)
         {
             if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
             {
@@ -3218,7 +3211,7 @@ namespace Emby.Server.Implementations.Data
 
             var whereText = whereClauses.Count == 0 ?
                 string.Empty :
-                " where " + string.Join(" AND ", whereClauses.ToArray());
+                " where " + string.Join(" AND ", whereClauses);
 
             commandText += whereText
                         + GetGroupBy(query)
@@ -4378,7 +4371,7 @@ namespace Emby.Server.Implementations.Data
             }
             else if (query.Years.Length > 1)
             {
-                var val = string.Join(",", query.Years.ToArray());
+                var val = string.Join(",", query.Years);
 
                 whereClauses.Add("ProductionYear in (" + val + ")");
             }
@@ -4952,7 +4945,12 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 return result;
             }
 
-            return new[] { value }.Where(IsValidType);
+            if (IsValidType(value))
+            {
+                return new[] { value };
+            }
+
+            return Array.Empty<string>();
         }
 
         public void DeleteItem(Guid id, CancellationToken cancellationToken)
@@ -5215,32 +5213,32 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 0, 1 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 0 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 1 }, typeof(MusicArtist).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 3 }, typeof(Studio).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 2 }, typeof(Genre).FullName);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
         {
             return GetItemValues(query, new[] { 2 }, typeof(MusicGenre).FullName);
         }
@@ -5317,7 +5315,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             }
         }
 
-        private QueryResult<Tuple<BaseItem, ItemCounts>> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
+        private QueryResult<(BaseItem, ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType)
         {
             if (query == null)
             {
@@ -5335,7 +5333,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             var typeClause = itemValueTypes.Length == 1 ?
                 ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
-                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")");
+                ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
 
             InternalItemsQuery typeSubQuery = null;
 
@@ -5363,11 +5361,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                 whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
 
-                var typeWhereText = whereClauses.Count == 0 ?
-                    string.Empty :
-                    " where " + string.Join(" AND ", whereClauses);
-
-                itemCountColumnQuery += typeWhereText;
+                itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
 
                 itemCountColumns = new Dictionary<string, string>()
                 {
@@ -5400,7 +5394,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 IsSeries = query.IsSeries
             };
 
-            columns = GetFinalColumnsToSelect(query, columns.ToArray()).ToList();
+            columns = GetFinalColumnsToSelect(query, columns);
 
             var commandText = "select "
                             + string.Join(",", columns)
@@ -5492,8 +5486,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 {
                     return connection.RunInTransaction(db =>
                     {
-                        var list = new List<Tuple<BaseItem, ItemCounts>>();
-                        var result = new QueryResult<Tuple<BaseItem, ItemCounts>>();
+                        var list = new List<(BaseItem, ItemCounts)>();
+                        var result = new QueryResult<(BaseItem, ItemCounts)>();
 
                         var statements = PrepareAllSafe(db, statementTexts);
 
@@ -5531,7 +5525,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                                     {
                                         var countStartColumn = columns.Count - 1;
 
-                                        list.Add(new Tuple<BaseItem, ItemCounts>(item, GetItemCounts(row, countStartColumn, typesToCount)));
+                                        list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
                                     }
                                 }
 
@@ -6198,6 +6192,5 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             return item;
         }
-
     }
 }

+ 1 - 11
Emby.Server.Implementations/Devices/DeviceId.cs

@@ -11,7 +11,6 @@ namespace Emby.Server.Implementations.Devices
     {
         private readonly IApplicationPaths _appPaths;
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
 
         private readonly object _syncLock = new object();
 
@@ -86,19 +85,10 @@ namespace Emby.Server.Implementations.Devices
 
         private string _id;
 
-        public DeviceId(
-            IApplicationPaths appPaths,
-            ILoggerFactory loggerFactory,
-            IFileSystem fileSystem)
+        public DeviceId(IApplicationPaths appPaths, ILoggerFactory loggerFactory)
         {
-            if (fileSystem == null)
-            {
-                throw new ArgumentNullException(nameof(fileSystem));
-            }
-
             _appPaths = appPaths;
             _logger = loggerFactory.CreateLogger("SystemId");
-            _fileSystem = fileSystem;
         }
 
         public string Value => _id ?? (_id = GetDeviceId());

+ 2 - 10
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -34,8 +34,6 @@ namespace Emby.Server.Implementations.Devices
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryMonitor _libraryMonitor;
         private readonly IServerConfigurationManager _config;
-        private readonly ILogger _logger;
-        private readonly INetworkManager _network;
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localizationManager;
 
@@ -55,17 +53,13 @@ namespace Emby.Server.Implementations.Devices
             IUserManager userManager,
             IFileSystem fileSystem,
             ILibraryMonitor libraryMonitor,
-            IServerConfigurationManager config,
-            ILoggerFactory loggerFactory,
-            INetworkManager network)
+            IServerConfigurationManager config)
         {
             _json = json;
             _userManager = userManager;
             _fileSystem = fileSystem;
             _libraryMonitor = libraryMonitor;
             _config = config;
-            _logger = loggerFactory.CreateLogger(nameof(DeviceManager));
-            _network = network;
             _libraryManager = libraryManager;
             _localizationManager = localizationManager;
             _authRepo = authRepo;
@@ -414,14 +408,12 @@ namespace Emby.Server.Implementations.Devices
     {
         private readonly DeviceManager _deviceManager;
         private readonly IServerConfigurationManager _config;
-        private readonly IFileSystem _fileSystem;
         private ILogger _logger;
 
-        public DeviceManagerEntryPoint(IDeviceManager deviceManager, IServerConfigurationManager config, IFileSystem fileSystem, ILogger logger)
+        public DeviceManagerEntryPoint(IDeviceManager deviceManager, IServerConfigurationManager config, ILogger logger)
         {
             _deviceManager = (DeviceManager)deviceManager;
             _config = config;
-            _fileSystem = fileSystem;
             _logger = logger;
         }
 

+ 0 - 12
Emby.Server.Implementations/Dto/DtoService.cs

@@ -36,13 +36,9 @@ namespace Emby.Server.Implementations.Dto
         private readonly IItemRepository _itemRepo;
 
         private readonly IImageProcessor _imageProcessor;
-        private readonly IServerConfigurationManager _config;
-        private readonly IFileSystem _fileSystem;
         private readonly IProviderManager _providerManager;
 
-        private readonly Func<IChannelManager> _channelManagerFactory;
         private readonly IApplicationHost _appHost;
-        private readonly Func<IDeviceManager> _deviceManager;
         private readonly Func<IMediaSourceManager> _mediaSourceManager;
         private readonly Func<ILiveTvManager> _livetvManager;
 
@@ -52,12 +48,8 @@ namespace Emby.Server.Implementations.Dto
             IUserDataManager userDataRepository,
             IItemRepository itemRepo,
             IImageProcessor imageProcessor,
-            IServerConfigurationManager config,
-            IFileSystem fileSystem,
             IProviderManager providerManager,
-            Func<IChannelManager> channelManagerFactory,
             IApplicationHost appHost,
-            Func<IDeviceManager> deviceManager,
             Func<IMediaSourceManager> mediaSourceManager,
             Func<ILiveTvManager> livetvManager)
         {
@@ -66,12 +58,8 @@ namespace Emby.Server.Implementations.Dto
             _userDataRepository = userDataRepository;
             _itemRepo = itemRepo;
             _imageProcessor = imageProcessor;
-            _config = config;
-            _fileSystem = fileSystem;
             _providerManager = providerManager;
-            _channelManagerFactory = channelManagerFactory;
             _appHost = appHost;
-            _deviceManager = deviceManager;
             _mediaSourceManager = mediaSourceManager;
             _livetvManager = livetvManager;
         }

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

@@ -22,9 +22,11 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" 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.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;

+ 2 - 11
Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs

@@ -3,27 +3,19 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.FFMpeg
 {
     public class FFMpegLoader
     {
-        private readonly IHttpClient _httpClient;
         private readonly IApplicationPaths _appPaths;
-        private readonly ILogger _logger;
-        private readonly IZipClient _zipClient;
         private readonly IFileSystem _fileSystem;
         private readonly FFMpegInstallInfo _ffmpegInstallInfo;
 
-        public FFMpegLoader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo)
+        public FFMpegLoader(IApplicationPaths appPaths, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo)
         {
-            _logger = logger;
             _appPaths = appPaths;
-            _httpClient = httpClient;
-            _zipClient = zipClient;
             _fileSystem = fileSystem;
             _ffmpegInstallInfo = ffmpegInstallInfo;
         }
@@ -115,8 +107,7 @@ namespace Emby.Server.Implementations.FFMpeg
             var encoderFilename = Path.GetFileName(info.EncoderPath);
             var probeFilename = Path.GetFileName(info.ProbePath);
 
-            foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath)
-                .ToList())
+            foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath))
             {
                 var allFiles = _fileSystem.GetFilePaths(directory, true).ToList();
 

+ 9 - 29
Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs

@@ -66,11 +66,6 @@ namespace Emby.Server.Implementations.HttpClientManager
 
             // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
             ServicePointManager.Expect100Continue = false;
-
-#if NET46
-// Trakt requests sometimes fail without this
-            ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls;
-#endif
         }
 
         /// <summary>
@@ -106,23 +101,6 @@ namespace Emby.Server.Implementations.HttpClientManager
             return client;
         }
 
-        private static WebRequest CreateWebRequest(string url)
-        {
-            try
-            {
-                return WebRequest.Create(url);
-            }
-            catch (NotSupportedException)
-            {
-                //Webrequest creation does fail on MONO randomly when using WebRequest.Create
-                //the issue occurs in the GetCreator method here: http://www.oschina.net/code/explore/mono-2.8.1/mcs/class/System/System.Net/WebRequest.cs
-
-                var type = Type.GetType("System.Net.HttpRequestCreator, System, Version=4.0.0.0,Culture=neutral, PublicKeyToken=b77a5c561934e089");
-                var creator = Activator.CreateInstance(type, nonPublic: true) as IWebRequestCreate;
-                return creator.Create(new Uri(url)) as HttpWebRequest;
-            }
-        }
-
         private WebRequest GetRequest(HttpRequestOptions options, string method)
         {
             string url = options.Url;
@@ -135,7 +113,7 @@ namespace Emby.Server.Implementations.HttpClientManager
                 url = url.Replace(userInfo + "@", string.Empty);
             }
 
-            var request = CreateWebRequest(url);
+            var request = WebRequest.Create(url);
 
             if (request is HttpWebRequest httpWebRequest)
             {
@@ -627,14 +605,16 @@ namespace Emby.Server.Implementations.HttpClientManager
 
                 var exception = new HttpException(webException.Message, webException);
 
-                var response = webException.Response as HttpWebResponse;
-                if (response != null)
+                using (var response = webException.Response as HttpWebResponse)
                 {
-                    exception.StatusCode = response.StatusCode;
-
-                    if ((int)response.StatusCode == 429)
+                    if (response != null)
                     {
-                        client.LastTimeout = DateTime.UtcNow;
+                        exception.StatusCode = response.StatusCode;
+
+                        if ((int)response.StatusCode == 429)
+                        {
+                            client.LastTimeout = DateTime.UtcNow;
+                        }
                     }
                 }
 

+ 7 - 5
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -19,7 +19,9 @@ using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
+using ServiceStack.Text.Jsv;
 
 namespace Emby.Server.Implementations.HttpServer
 {
@@ -53,20 +55,20 @@ namespace Emby.Server.Implementations.HttpServer
             IServerApplicationHost applicationHost,
             ILoggerFactory loggerFactory,
             IServerConfigurationManager config,
-            string defaultRedirectPath,
+            IConfiguration configuration,
             INetworkManager networkManager,
             IJsonSerializer jsonSerializer,
-            IXmlSerializer xmlSerializer,
-            Func<Type, Func<string, object>> funcParseFn)
+            IXmlSerializer xmlSerializer)
         {
             _appHost = applicationHost;
             _logger = loggerFactory.CreateLogger("HttpServer");
             _config = config;
-            DefaultRedirectPath = defaultRedirectPath;
+            DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"];
             _networkManager = networkManager;
             _jsonSerializer = jsonSerializer;
             _xmlSerializer = xmlSerializer;
-            _funcParseFn = funcParseFn;
+            
+            _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 
             Instance = this;
             ResponseFilters = Array.Empty<Action<IRequest, IResponse, object>>();

+ 40 - 94
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null)
         {
-            var result = new StreamWriter(content, contentType, _logger);
+            var result = new StreamWriter(content, contentType);
 
             if (responseHeaders == null)
             {
@@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.HttpServer
                     content = Array.Empty<byte>();
                 }
 
-                result = new StreamWriter(content, contentType, contentLength, _logger);
+                result = new StreamWriter(content, contentType, contentLength);
             }
             else
             {
@@ -143,7 +143,7 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders = new Dictionary<string, string>();
             }
 
-            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
+            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
             {
                 responseHeaders["Expires"] = "-1";
             }
@@ -175,7 +175,7 @@ namespace Emby.Server.Implementations.HttpServer
                     bytes = Array.Empty<byte>();
                 }
 
-                result = new StreamWriter(bytes, contentType, contentLength, _logger);
+                result = new StreamWriter(bytes, contentType, contentLength);
             }
             else
             {
@@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders = new Dictionary<string, string>();
             }
 
-            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
+            if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
             {
                 responseHeaders["Expires"] = "-1";
             }
@@ -277,9 +277,10 @@ namespace Emby.Server.Implementations.HttpServer
 
         private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null)
         {
-            var contentType = request.ResponseContentType;
+            // TODO: @bond use Span and .Equals
+            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
 
-            switch (GetRealContentType(contentType))
+            switch (contentType)
             {
                 case "application/xml":
                 case "text/xml":
@@ -333,13 +334,13 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (isHeadRequest)
             {
-                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength, _logger);
+                var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
                 AddResponseHeaders(result, responseHeaders);
                 return result;
             }
             else
             {
-                var result = new StreamWriter(content, contentType, contentLength, _logger);
+                var result = new StreamWriter(content, contentType, contentLength);
                 AddResponseHeaders(result, responseHeaders);
                 return result;
             }
@@ -348,13 +349,19 @@ namespace Emby.Server.Implementations.HttpServer
         private byte[] Compress(byte[] bytes, string compressionType)
         {
             if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
+            {
                 return CompressBrotli(bytes);
+            }
 
             if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
+            {
                 return Deflate(bytes);
+            }
 
             if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
+            {
                 return GZip(bytes);
+            }
 
             throw new NotSupportedException(compressionType);
         }
@@ -390,13 +397,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        public static string GetRealContentType(string contentType)
-        {
-            return contentType == null
-                       ? null
-                       : contentType.Split(';')[0].ToLowerInvariant().Trim();
-        }
-
         private static string SerializeToXmlString(object from)
         {
             using (var ms = new MemoryStream())
@@ -422,18 +422,20 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// Pres the process optimized result.
         /// </summary>
-        private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType)
+        private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options)
         {
             bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
+            AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
 
             if (!noCache)
             {
-                if (IsNotModified(requestContext, cacheKey))
+                DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader);
+
+                if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
                 {
-                    AddAgeHeader(responseHeaders, lastDateModified);
-                    AddExpiresHeader(responseHeaders, cacheKeyString, cacheDuration);
+                    AddAgeHeader(responseHeaders, options.DateLastModified);
 
-                    var result = new HttpResult(Array.Empty<byte>(), contentType ?? "text/html", HttpStatusCode.NotModified);
+                    var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
 
                     AddResponseHeaders(result, responseHeaders);
 
@@ -441,8 +443,6 @@ namespace Emby.Server.Implementations.HttpServer
                 }
             }
 
-            AddCachingHeaders(responseHeaders, cacheKeyString, cacheDuration);
-
             return null;
         }
 
@@ -487,9 +487,6 @@ namespace Emby.Server.Implementations.HttpServer
                 options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path);
             }
 
-            var cacheKey = path + options.DateLastModified.Value.Ticks;
-
-            options.CacheKey = cacheKey.GetMD5();
             options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare));
 
             options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -520,7 +517,6 @@ namespace Emby.Server.Implementations.HttpServer
             return GetStaticResult(requestContext, new StaticResultOptions
             {
                 CacheDuration = cacheDuration,
-                CacheKey = cacheKey,
                 ContentFactory = factoryFn,
                 ContentType = contentType,
                 DateLastModified = lastDateModified,
@@ -534,14 +530,10 @@ namespace Emby.Server.Implementations.HttpServer
             options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
             var contentType = options.ContentType;
-            var etag = requestContext.Headers.Get("If-None-Match");
-            var cacheKey = etag != null ? new Guid(etag.Trim('\"')) : Guid.Empty;
-            if (!cacheKey.Equals(Guid.Empty))
+            if (!string.IsNullOrEmpty(requestContext.Headers.Get("If-Modified-Since")))
             {
-                var key = cacheKey.ToString("N");
-
                 // See if the result is already cached in the browser
-                var result = GetCachedResult(requestContext, options.ResponseHeaders, cacheKey, key, options.DateLastModified, options.CacheDuration, contentType);
+                var result = GetCachedResult(requestContext, options.ResponseHeaders, options);
 
                 if (result != null)
                 {
@@ -553,6 +545,8 @@ namespace Emby.Server.Implementations.HttpServer
             var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase);
             var factoryFn = options.ContentFactory;
             var responseHeaders = options.ResponseHeaders;
+            AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified);
+            AddAgeHeader(responseHeaders, options.DateLastModified);
 
             var rangeHeader = requestContext.Headers.Get("Range");
 
@@ -566,21 +560,10 @@ namespace Emby.Server.Implementations.HttpServer
                 };
 
                 AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                // Generate an ETag based on identifying information - TODO read contents from filesystem instead?
-                var responseId = $"{hasHeaders.ContentType}{options.Path}{hasHeaders.TotalContentLength}";
-                var hashedId = MD5.Create().ComputeHash(Encoding.Default.GetBytes(responseId));
-                hasHeaders.Headers["ETag"] = new Guid(hashedId).ToString("N");
-
                 return hasHeaders;
             }
 
             var stream = await factoryFn().ConfigureAwait(false);
-            // Generate an etag based on stream content
-            var streamHash = MD5.Create().ComputeHash(stream);
-            var newEtag = new Guid(streamHash).ToString("N");
-
-            // reset position so the response can re-use it -- TODO is this ok?
-            stream.Position = 0;
 
             var totalContentLength = options.ContentLength;
             if (!totalContentLength.HasValue)
@@ -603,7 +586,6 @@ namespace Emby.Server.Implementations.HttpServer
                 };
 
                 AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                hasHeaders.Headers["ETag"] = newEtag;
                 return hasHeaders;
             }
             else
@@ -621,14 +603,13 @@ namespace Emby.Server.Implementations.HttpServer
                     }
                 }
 
-                var hasHeaders = new StreamWriter(stream, contentType, _logger)
+                var hasHeaders = new StreamWriter(stream, contentType)
                 {
                     OnComplete = options.OnComplete,
                     OnError = options.OnError
                 };
 
                 AddResponseHeaders(hasHeaders, options.ResponseHeaders);
-                hasHeaders.Headers["ETag"] = newEtag;
                 return hasHeaders;
             }
         }
@@ -641,37 +622,28 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// Adds the caching responseHeaders.
         /// </summary>
-        private void AddCachingHeaders(IDictionary<string, string> responseHeaders, string cacheKey, TimeSpan? cacheDuration)
+        private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
+            bool noCache, DateTime? lastModifiedDate)
         {
-            if (cacheDuration.HasValue)
-            {
-                responseHeaders["Cache-Control"] = "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds);
-            }
-            else if (!string.IsNullOrEmpty(cacheKey))
-            {
-                responseHeaders["Cache-Control"] = "public";
-            }
-            else
+            if (noCache)
             {
                 responseHeaders["Cache-Control"] = "no-cache, no-store, must-revalidate";
                 responseHeaders["pragma"] = "no-cache, no-store, must-revalidate";
+                return;
             }
 
-            AddExpiresHeader(responseHeaders, cacheKey, cacheDuration);
-        }
-
-        /// <summary>
-        /// Adds the expires header.
-        /// </summary>
-        private static void AddExpiresHeader(IDictionary<string, string> responseHeaders, string cacheKey, TimeSpan? cacheDuration)
-        {
             if (cacheDuration.HasValue)
             {
-                responseHeaders["Expires"] = DateTime.UtcNow.Add(cacheDuration.Value).ToString("r");
+                responseHeaders["Cache-Control"] = "public, max-age=" + cacheDuration.Value.TotalSeconds;
             }
-            else if (string.IsNullOrEmpty(cacheKey))
+            else
             {
-                responseHeaders["Expires"] = "-1";
+                responseHeaders["Cache-Control"] = "public";
+            }
+
+            if (lastModifiedDate.HasValue)
+            {
+                responseHeaders["Last-Modified"] = lastModifiedDate.ToString();
             }
         }
 
@@ -687,32 +659,6 @@ namespace Emby.Server.Implementations.HttpServer
                 responseHeaders["Age"] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
             }
         }
-        /// <summary>
-        /// Determines whether [is not modified] [the specified cache key].
-        /// </summary>
-        /// <param name="requestContext">The request context.</param>
-        /// <param name="cacheKey">The cache key.</param>
-        /// <param name="lastDateModified">The last date modified.</param>
-        /// <param name="cacheDuration">Duration of the cache.</param>
-        /// <returns><c>true</c> if [is not modified] [the specified cache key]; otherwise, <c>false</c>.</returns>
-        private bool IsNotModified(IRequest requestContext, Guid cacheKey)
-        {
-            var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match");
-
-            bool hasCacheKey = !cacheKey.Equals(Guid.Empty);
-
-            // Validate If-None-Match
-            if (hasCacheKey && !string.IsNullOrEmpty(ifNoneMatchHeader))
-            {
-                if (Guid.TryParse(ifNoneMatchHeader, out var ifNoneMatch)
-                    && cacheKey.Equals(ifNoneMatch))
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
 
         /// <summary>
         /// Determines whether [is not modified] [the specified if modified since].

+ 2 - 6
Emby.Server.Implementations/HttpServer/StreamWriter.cs

@@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
     /// </summary>
     public class StreamWriter : IAsyncStreamWriter, IHasHeaders
     {
-        private ILogger Logger { get; set; }
-
         private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
 
         /// <summary>
@@ -45,7 +43,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="logger">The logger.</param>
-        public StreamWriter(Stream source, string contentType, ILogger logger)
+        public StreamWriter(Stream source, string contentType)
         {
             if (string.IsNullOrEmpty(contentType))
             {
@@ -53,7 +51,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
 
             SourceStream = source;
-            Logger = logger;
 
             Headers["Content-Type"] = contentType;
 
@@ -69,7 +66,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="logger">The logger.</param>
-        public StreamWriter(byte[] source, string contentType, int contentLength, ILogger logger)
+        public StreamWriter(byte[] source, string contentType, int contentLength)
         {
             if (string.IsNullOrEmpty(contentType))
             {
@@ -77,7 +74,6 @@ namespace Emby.Server.Implementations.HttpServer
             }
 
             SourceBytes = source;
-            Logger = logger;
 
             Headers["Content-Type"] = contentType;
 

+ 1 - 9
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -17,31 +17,23 @@ namespace Emby.Server.Implementations.IO
     public class FileRefresher : IDisposable
     {
         private ILogger Logger { get; set; }
-        private ITaskManager TaskManager { get; set; }
         private ILibraryManager LibraryManager { get; set; }
         private IServerConfigurationManager ConfigurationManager { get; set; }
-        private readonly IFileSystem _fileSystem;
         private readonly List<string> _affectedPaths = new List<string>();
         private Timer _timer;
         private readonly object _timerLock = new object();
         public string Path { get; private set; }
 
         public event EventHandler<EventArgs> Completed;
-        private readonly IEnvironmentInfo _environmentInfo;
-        private readonly ILibraryManager _libraryManager;
 
-        public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger, IEnvironmentInfo environmentInfo, ILibraryManager libraryManager1)
+        public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
         {
             logger.LogDebug("New file refresher created for {0}", path);
             Path = path;
 
-            _fileSystem = fileSystem;
             ConfigurationManager = configurationManager;
             LibraryManager = libraryManager;
-            TaskManager = taskManager;
             Logger = logger;
-            _environmentInfo = environmentInfo;
-            _libraryManager = libraryManager1;
             AddPath(path);
         }
 

+ 4 - 17
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.IO
         /// <summary>
         /// Any file name ending in any of these will be ignored by the watchers
         /// </summary>
-        private readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
             "small.jpg",
             "albumart.jpg",
@@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.IO
             "TempSBE"
         };
 
-        private readonly string[] _alwaysIgnoreSubstrings = new string[]
+        private static readonly string[] _alwaysIgnoreSubstrings = new string[]
         {
             // Synology
             "eaDir",
@@ -53,7 +53,7 @@ namespace Emby.Server.Implementations.IO
             ".actors"
         };
 
-        private readonly HashSet<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly HashSet<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
             // thumbs.db
             ".db",
@@ -123,12 +123,6 @@ namespace Emby.Server.Implementations.IO
         /// <value>The logger.</value>
         private ILogger Logger { get; set; }
 
-        /// <summary>
-        /// Gets or sets the task manager.
-        /// </summary>
-        /// <value>The task manager.</value>
-        private ITaskManager TaskManager { get; set; }
-
         private ILibraryManager LibraryManager { get; set; }
         private IServerConfigurationManager ConfigurationManager { get; set; }
 
@@ -140,19 +134,12 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         public LibraryMonitor(
             ILoggerFactory loggerFactory,
-            ITaskManager taskManager,
             ILibraryManager libraryManager,
             IServerConfigurationManager configurationManager,
             IFileSystem fileSystem,
             IEnvironmentInfo environmentInfo)
         {
-            if (taskManager == null)
-            {
-                throw new ArgumentNullException(nameof(taskManager));
-            }
-
             LibraryManager = libraryManager;
-            TaskManager = taskManager;
             Logger = loggerFactory.CreateLogger(GetType().Name);
             ConfigurationManager = configurationManager;
             _fileSystem = fileSystem;
@@ -541,7 +528,7 @@ namespace Emby.Server.Implementations.IO
                     }
                 }
 
-                var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger, _environmentInfo, LibraryManager);
+                var newRefresher = new FileRefresher(path, ConfigurationManager, LibraryManager, Logger);
                 newRefresher.Completed += NewRefresher_Completed;
                 _activeRefreshers.Add(newRefresher);
             }

+ 11 - 48
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -4,8 +4,10 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text;
+using MediaBrowser.Common.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.System;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.IO
@@ -20,61 +22,27 @@ namespace Emby.Server.Implementations.IO
         private readonly bool _supportsAsyncFileStreams;
         private char[] _invalidFileNameChars;
         private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
-        private bool EnableSeparateFileAndDirectoryQueries;
 
-        private string _tempPath;
+        private readonly string _tempPath;
 
-        private IEnvironmentInfo _environmentInfo;
-        private bool _isEnvironmentCaseInsensitive;
-
-        private string _defaultDirectory;
+        private readonly IEnvironmentInfo _environmentInfo;
+        private readonly bool _isEnvironmentCaseInsensitive;
 
         public ManagedFileSystem(
             ILoggerFactory loggerFactory,
             IEnvironmentInfo environmentInfo,
-            string defaultDirectory,
-            string tempPath,
-            bool enableSeparateFileAndDirectoryQueries)
+            IApplicationPaths applicationPaths)
         {
             Logger = loggerFactory.CreateLogger("FileSystem");
             _supportsAsyncFileStreams = true;
-            _tempPath = tempPath;
+            _tempPath = applicationPaths.TempDirectory;
             _environmentInfo = environmentInfo;
-            _defaultDirectory = defaultDirectory;
-
-            // On Linux with mono, this needs to be true or symbolic links are ignored
-            EnableSeparateFileAndDirectoryQueries = enableSeparateFileAndDirectoryQueries;
 
             SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows);
 
             _isEnvironmentCaseInsensitive = environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows;
         }
 
-        public virtual string DefaultDirectory
-        {
-            get
-            {
-                var value = _defaultDirectory;
-
-                if (!string.IsNullOrEmpty(value))
-                {
-                    try
-                    {
-                        if (Directory.Exists(value))
-                        {
-                            return value;
-                        }
-                    }
-                    catch
-                    {
-
-                    }
-                }
-
-                return null;
-            }
-        }
-
         public virtual void AddShortcutHandler(IShortcutHandler handler)
         {
             _shortcutHandlers.Add(handler);
@@ -718,7 +686,7 @@ namespace Emby.Server.Implementations.IO
             SetAttributes(path, false, false);
             File.Delete(path);
         }
-        
+
         public virtual List<FileSystemMetadata> GetDrives()
         {
             // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout
@@ -777,20 +745,15 @@ namespace Emby.Server.Implementations.IO
             var directoryInfo = new DirectoryInfo(path);
             var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
 
-            if (EnableSeparateFileAndDirectoryQueries)
-            {
-                return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
-                                .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
-            }
-
-            return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", searchOption));
+            return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
+                .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
         }
 
         private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
         {
             return infos.Select(GetFileSystemMetadata);
         }
-        
+
         public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
         {
             var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;

+ 4 - 10
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using MediaBrowser.Controller.Entities;
@@ -7,7 +6,6 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -16,16 +14,14 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     {
-        private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
-        private readonly ILogger _logger;
 
         private bool _ignoreDotPrefix;
 
         /// <summary>
         /// Any folder named in this list will be ignored - can be added to at runtime for extensibility
         /// </summary>
-        public static readonly Dictionary<string, string> IgnoreFolders = new List<string>
+        public static readonly string[] IgnoreFolders =
         {
                 "metadata",
                 "ps3_update",
@@ -50,13 +46,11 @@ namespace Emby.Server.Implementations.Library
                 // macos
                 ".AppleDouble"
 
-        }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
+        };
 
-        public CoreResolutionIgnoreRule(IFileSystem fileSystem, ILibraryManager libraryManager, ILogger logger)
+        public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
         {
-            _fileSystem = fileSystem;
             _libraryManager = libraryManager;
-            _logger = logger;
 
             _ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT;
         }
@@ -117,7 +111,7 @@ namespace Emby.Server.Implementations.Library
             if (fileInfo.IsDirectory)
             {
                 // Ignore any folders in our list
-                if (IgnoreFolders.ContainsKey(filename))
+                if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
                 {
                     return true;
                 }

+ 23 - 25
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -986,7 +986,7 @@ namespace Emby.Server.Implementations.Library
             // Ensure the location is available.
             Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath);
 
-            return new PeopleValidator(this, _logger, ConfigurationManager, _fileSystem).ValidatePeople(cancellationToken, progress);
+            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
         }
 
         /// <summary>
@@ -1225,9 +1225,9 @@ namespace Emby.Server.Implementations.Library
         /// <exception cref="ArgumentNullException">id</exception>
         public BaseItem GetItemById(Guid id)
         {
-            if (id.Equals(Guid.Empty))
+            if (id == Guid.Empty)
             {
-                throw new ArgumentNullException(nameof(id));
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
             }
 
             if (LibraryItemsCache.TryGetValue(id, out BaseItem item))
@@ -1237,8 +1237,6 @@ namespace Emby.Server.Implementations.Library
 
             item = RetrieveItem(id);
 
-            //_logger.LogDebug("GetitemById {0}", id);
-
             if (item != null)
             {
                 RegisterItem(item);
@@ -1333,7 +1331,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetItemIdsList(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1344,7 +1342,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetStudios(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1355,7 +1353,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetGenres(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1366,7 +1364,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetMusicGenres(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAllArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1377,7 +1375,7 @@ namespace Emby.Server.Implementations.Library
             return ItemRepository.GetAllArtists(query);
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1421,7 +1419,7 @@ namespace Emby.Server.Implementations.Library
             }
         }
 
-        public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query)
+        public QueryResult<(BaseItem, ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
         {
             if (query.User != null)
             {
@@ -1808,18 +1806,16 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task.</returns>
         public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken)
         {
-            var list = items.ToList();
-
-            ItemRepository.SaveItems(list, cancellationToken);
+            ItemRepository.SaveItems(items, cancellationToken);
 
-            foreach (var item in list)
+            foreach (var item in items)
             {
                 RegisterItem(item);
             }
 
             if (ItemAdded != null)
             {
-                foreach (var item in list)
+                foreach (var item in items)
                 {
                     // With the live tv guide this just creates too much noise
                     if (item.SourceType != SourceType.Library)
@@ -1853,7 +1849,7 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Updates the item.
         /// </summary>
-        public void UpdateItems(List<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
+        public void UpdateItems(IEnumerable<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
             foreach (var item in items)
             {
@@ -1908,7 +1904,7 @@ namespace Emby.Server.Implementations.Library
         /// <returns>Task.</returns>
         public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
         {
-            UpdateItems(new List<BaseItem> { item }, parent, updateReason, cancellationToken);
+            UpdateItems(new [] { item }, parent, updateReason, cancellationToken);
         }
 
         /// <summary>
@@ -2005,9 +2001,7 @@ namespace Emby.Server.Implementations.Library
                    .FirstOrDefault();
             }
 
-            var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
-
-            return options;
+            return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
         }
 
         public string GetContentType(BaseItem item)
@@ -2017,11 +2011,13 @@ namespace Emby.Server.Implementations.Library
             {
                 return configuredContentType;
             }
+
             configuredContentType = GetConfiguredContentType(item, true);
             if (!string.IsNullOrEmpty(configuredContentType))
             {
                 return configuredContentType;
             }
+
             return GetInheritedContentType(item);
         }
 
@@ -2056,6 +2052,7 @@ namespace Emby.Server.Implementations.Library
             {
                 return collectionFolder.CollectionType;
             }
+
             return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
         }
 
@@ -2066,6 +2063,7 @@ namespace Emby.Server.Implementations.Library
             {
                 return nameValuePair.Value;
             }
+
             return null;
         }
 
@@ -2108,9 +2106,9 @@ namespace Emby.Server.Implementations.Library
             string viewType,
             string sortName)
         {
-            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views");
-
-            path = Path.Combine(path, _fileSystem.GetValidFilename(viewType));
+            var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath,
+                                    "views",
+                                    _fileSystem.GetValidFilename(viewType));
 
             var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 
@@ -2543,7 +2541,7 @@ namespace Emby.Server.Implementations.Library
 
             var resolvers = new IItemResolver[]
             {
-                new GenericVideoResolver<Trailer>(this, _fileSystem)
+                new GenericVideoResolver<Trailer>(this)
             };
 
             return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)

+ 1 - 4
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
@@ -18,11 +17,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
         where T : Video, new()
     {
         protected readonly ILibraryManager LibraryManager;
-        protected readonly IFileSystem FileSystem;
 
-        protected BaseVideoResolver(ILibraryManager libraryManager, IFileSystem fileSystem)
+        protected BaseVideoResolver(ILibraryManager libraryManager)
         {
-            FileSystem = fileSystem;
             LibraryManager = libraryManager;
         }
 

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -548,7 +548,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
         private IImageProcessor _imageProcessor;
 
-        public MovieResolver(ILibraryManager libraryManager, IFileSystem fileSystem, IImageProcessor imageProcessor) : base(libraryManager, fileSystem)
+        public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
+            : base(libraryManager)
         {
             _imageProcessor = imageProcessor;
         }

+ 2 - 6
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -7,7 +7,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
@@ -15,13 +14,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
     {
         private readonly IImageProcessor _imageProcessor;
         private readonly ILibraryManager _libraryManager;
-        private readonly IFileSystem _fileSystem;
 
-        public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager, IFileSystem fileSystem)
+        public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
         {
             _imageProcessor = imageProcessor;
             _libraryManager = libraryManager;
-            _fileSystem = fileSystem;
         }
 
         /// <summary>
@@ -113,8 +110,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 return false;
             }
 
-            return imageProcessor.SupportedInputFormats.Contains((Path.GetExtension(path) ?? string.Empty).TrimStart('.'));
+            return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal);
         }
-
     }
 }

+ 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;

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -3,7 +3,6 @@ using System.Linq;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers.TV
 {
@@ -74,7 +73,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
             return null;
         }
 
-        public EpisodeResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem)
+        public EpisodeResolver(ILibraryManager libraryManager)
+            : base(libraryManager)
         {
         }
     }

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/VideoResolver.cs

@@ -1,13 +1,13 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
     public class GenericVideoResolver<T> : BaseVideoResolver<T>
         where T : Video, new()
     {
-        public GenericVideoResolver(ILibraryManager libraryManager, IFileSystem fileSystem) : base(libraryManager, fileSystem)
+        public GenericVideoResolver(ILibraryManager libraryManager)
+            : base(libraryManager)
         {
         }
     }

+ 1216 - 1219
Emby.Server.Implementations/Library/UserManager.cs

@@ -1,1219 +1,1216 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Controller.Drawing;
-using MediaBrowser.Controller.Dto;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Controller.Security;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Library
-{
-    /// <summary>
-    /// Class UserManager
-    /// </summary>
-    public class UserManager : IUserManager
-    {
-        /// <summary>
-        /// Gets the users.
-        /// </summary>
-        /// <value>The users.</value>
-        public IEnumerable<User> Users => _users;
-
-        private User[] _users;
-
-        /// <summary>
-        /// The _logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        /// <summary>
-        /// Gets or sets the configuration manager.
-        /// </summary>
-        /// <value>The configuration manager.</value>
-        private IServerConfigurationManager ConfigurationManager { get; set; }
-
-        /// <summary>
-        /// Gets the active user repository
-        /// </summary>
-        /// <value>The user repository.</value>
-        private IUserRepository UserRepository { get; set; }
-        public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
-
-        private readonly IXmlSerializer _xmlSerializer;
-        private readonly IJsonSerializer _jsonSerializer;
-
-        private readonly INetworkManager _networkManager;
-
-        private readonly Func<IImageProcessor> _imageProcessorFactory;
-        private readonly Func<IDtoService> _dtoServiceFactory;
-        private readonly IServerApplicationHost _appHost;
-        private readonly IFileSystem _fileSystem;
-        private readonly ICryptoProvider _cryptographyProvider;
-
-        private IAuthenticationProvider[] _authenticationProviders;
-        private DefaultAuthenticationProvider _defaultAuthenticationProvider;
-
-        public UserManager(
-            ILoggerFactory loggerFactory,
-            IServerConfigurationManager configurationManager,
-            IUserRepository userRepository,
-            IXmlSerializer xmlSerializer,
-            INetworkManager networkManager,
-            Func<IImageProcessor> imageProcessorFactory,
-            Func<IDtoService> dtoServiceFactory,
-            IServerApplicationHost appHost,
-            IJsonSerializer jsonSerializer,
-            IFileSystem fileSystem,
-            ICryptoProvider cryptographyProvider)
-        {
-            _logger = loggerFactory.CreateLogger(nameof(UserManager));
-            UserRepository = userRepository;
-            _xmlSerializer = xmlSerializer;
-            _networkManager = networkManager;
-            _imageProcessorFactory = imageProcessorFactory;
-            _dtoServiceFactory = dtoServiceFactory;
-            _appHost = appHost;
-            _jsonSerializer = jsonSerializer;
-            _fileSystem = fileSystem;
-            _cryptographyProvider = cryptographyProvider;
-            ConfigurationManager = configurationManager;
-            _users = Array.Empty<User>();
-
-            DeletePinFile();
-        }
-
-        public NameIdPair[] GetAuthenticationProviders()
-        {
-            return _authenticationProviders
-                .Where(i => i.IsEnabled)
-                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
-                .ThenBy(i => i.Name)
-                .Select(i => new NameIdPair
-                {
-                    Name = i.Name,
-                    Id = GetAuthenticationProviderId(i)
-                })
-                .ToArray();
-        }
-
-        public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
-        {
-            _authenticationProviders = authenticationProviders.ToArray();
-
-            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
-        }
-
-        #region UserUpdated Event
-        /// <summary>
-        /// Occurs when [user updated].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<User>> UserUpdated;
-        public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
-        public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
-        public event EventHandler<GenericEventArgs<User>> UserLockedOut;
-
-        /// <summary>
-        /// Called when [user updated].
-        /// </summary>
-        /// <param name="user">The user.</param>
-        private void OnUserUpdated(User user)
-        {
-            UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
-        }
-        #endregion
-
-        #region UserDeleted Event
-        /// <summary>
-        /// Occurs when [user deleted].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<User>> UserDeleted;
-        /// <summary>
-        /// Called when [user deleted].
-        /// </summary>
-        /// <param name="user">The user.</param>
-        private void OnUserDeleted(User user)
-        {
-            UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
-        }
-        #endregion
-
-        /// <summary>
-        /// Gets a User by Id
-        /// </summary>
-        /// <param name="id">The id.</param>
-        /// <returns>User.</returns>
-        /// <exception cref="ArgumentNullException"></exception>
-        public User GetUserById(Guid id)
-        {
-            if (id.Equals(Guid.Empty))
-            {
-                throw new ArgumentNullException(nameof(id));
-            }
-
-            return Users.FirstOrDefault(u => u.Id == id);
-        }
-
-        /// <summary>
-        /// Gets the user by identifier.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>User.</returns>
-        public User GetUserById(string id)
-        {
-            return GetUserById(new Guid(id));
-        }
-
-        public User GetUserByName(string name)
-        {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
-        }
-
-        public void Initialize()
-        {
-            _users = LoadUsers();
-
-            var users = Users.ToList();
-
-            // If there are no local users with admin rights, make them all admins
-            if (!users.Any(i => i.Policy.IsAdministrator))
-            {
-                foreach (var user in users)
-                {
-                    user.Policy.IsAdministrator = true;
-                    UpdateUserPolicy(user, user.Policy, false);
-                }
-            }
-        }
-
-        public static bool IsValidUsername(string username)
-        {
-            //This is some regex that matches only on unicode "word" characters, as well as -, _ and @
-            //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
-            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
-            return Regex.IsMatch(username, "^[\\w-'._@]*$");
-        }
-
-        private static bool IsValidUsernameCharacter(char i)
-        {
-            return IsValidUsername(i.ToString());
-        }
-
-        public string MakeValidUsername(string username)
-        {
-            if (IsValidUsername(username))
-            {
-                return username;
-            }
-
-            // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
-            var builder = new StringBuilder();
-
-            foreach (var c in username)
-            {
-                if (IsValidUsernameCharacter(c))
-                {
-                    builder.Append(c);
-                }
-            }
-            return builder.ToString();
-        }
-
-        public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession)
-        {
-            if (string.IsNullOrWhiteSpace(username))
-            {
-                throw new ArgumentNullException(nameof(username));
-            }
-
-            var user = Users
-                .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
-
-            var success = false;
-            IAuthenticationProvider authenticationProvider = null;
-
-            if (user != null)
-            {
-                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
-                authenticationProvider = authResult.Item1;
-                success = authResult.Item2;
-            }
-            else
-            {
-                // user is null
-                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
-                authenticationProvider = authResult.Item1;
-                success = authResult.Item2;
-
-                if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
-                {
-                    user = await CreateUser(username).ConfigureAwait(false);
-
-                    var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
-                    if (hasNewUserPolicy != null)
-                    {
-                        var policy = hasNewUserPolicy.GetNewUserPolicy();
-                        UpdateUserPolicy(user, policy, true);
-                    }
-                }
-            }
-
-            if (success && user != null && authenticationProvider != null)
-            {
-                var providerId = GetAuthenticationProviderId(authenticationProvider);
-
-                if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
-                {
-                    user.Policy.AuthenticationProviderId = providerId;
-                    UpdateUserPolicy(user, user.Policy, true);
-                }
-            }
-
-            if (user == null)
-            {
-                throw new SecurityException("Invalid username or password entered.");
-            }
-
-            if (user.Policy.IsDisabled)
-            {
-                throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name));
-            }
-
-            if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
-            {
-                throw new SecurityException("Forbidden.");
-            }
-
-            if (!user.IsParentalScheduleAllowed())
-            {
-                throw new SecurityException("User is not allowed access at this time.");
-            }
-
-            // Update LastActivityDate and LastLoginDate, then save
-            if (success)
-            {
-                if (isUserSession)
-                {
-                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
-                    UpdateUser(user);
-                }
-                UpdateInvalidLoginAttemptCount(user, 0);
-            }
-            else
-            {
-                UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1);
-            }
-
-            _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
-
-            return success ? user : null;
-        }
-
-        private static string GetAuthenticationProviderId(IAuthenticationProvider provider)
-        {
-            return provider.GetType().FullName;
-        }
-
-        private IAuthenticationProvider GetAuthenticationProvider(User user)
-        {
-            return GetAuthenticationProviders(user).First();
-        }
-
-        private IAuthenticationProvider[] GetAuthenticationProviders(User user)
-        {
-            var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
-
-            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
-
-            if (!string.IsNullOrEmpty(authenticationProviderId))
-            {
-                providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
-            }
-
-            if (providers.Length == 0)
-            {
-                providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
-            }
-
-            return providers;
-        }
-
-        private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
-        {
-            try
-            {
-                var requiresResolvedUser = provider as IRequiresResolvedUser;
-                if (requiresResolvedUser != null)
-                {
-                    await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
-                }
-                else
-                {
-                    await provider.Authenticate(username, password).ConfigureAwait(false);
-                }
-
-                return true;
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
-
-                return false;
-            }
-        }
-
-        private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
-        {
-            bool success = false;
-            IAuthenticationProvider authenticationProvider = null;
-
-            if (password != null && user != null)
-            {
-                // Doesn't look like this is even possible to be used, because of password == null checks below
-                hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password);
-            }
-
-            if (password == null)
-            {
-                // legacy
-                success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
-            }
-            else
-            {
-                foreach (var provider in GetAuthenticationProviders(user))
-                {
-                    success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
-
-                    if (success)
-                    {
-                        authenticationProvider = provider;
-                        break;
-                    }
-                }
-            }
-
-            if (user != null)
-            {
-                if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
-                {
-                    if (password == null)
-                    {
-                        // legacy
-                        success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
-                    }
-                    else
-                    {
-                        success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
-                    }
-                }
-            }
-
-            return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success);
-        }
-
-        private void UpdateInvalidLoginAttemptCount(User user, int newValue)
-        {
-            if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0)
-            {
-                return;
-            }
-
-            user.Policy.InvalidLoginAttemptCount = newValue;
-
-            var maxCount = user.Policy.IsAdministrator ? 3 : 5;
-
-            var fireLockout = false;
-
-            if (newValue >= maxCount)
-            {
-                _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue);
-                user.Policy.IsDisabled = true;
-
-                fireLockout = true;
-            }
-
-            UpdateUserPolicy(user, user.Policy, false);
-
-            if (fireLockout)
-            {
-                UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
-            }
-        }
-
-        private string GetLocalPasswordHash(User user)
-        {
-            return string.IsNullOrEmpty(user.EasyPassword)
-                ? null
-                : user.EasyPassword;
-        }
-
-        private bool IsPasswordEmpty(User user, string passwordHash)
-        {
-            return string.IsNullOrEmpty(passwordHash);
-        }
-
-        /// <summary>
-        /// Loads the users from the repository
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        private User[] LoadUsers()
-        {
-            var users = UserRepository.RetrieveAllUsers();
-
-            // There always has to be at least one user.
-            if (users.Count == 0)
-            {
-                var defaultName = Environment.UserName;
-                if (string.IsNullOrWhiteSpace(defaultName))
-                {
-                    defaultName = "MyJellyfinUser";
-                }
-                var name = MakeValidUsername(defaultName);
-
-                var user = InstantiateNewUser(name);
-
-                user.DateLastSaved = DateTime.UtcNow;
-
-                UserRepository.CreateUser(user);
-
-                users.Add(user);
-
-                user.Policy.IsAdministrator = true;
-                user.Policy.EnableContentDeletion = true;
-                user.Policy.EnableRemoteControlOfOtherUsers = true;
-                UpdateUserPolicy(user, user.Policy, false);
-            }
-
-            return users.ToArray();
-        }
-
-        public UserDto GetUserDto(User user, string remoteEndPoint = null)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
-            var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
-
-            var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
-                hasConfiguredEasyPassword :
-                hasConfiguredPassword;
-
-            var dto = new UserDto
-            {
-                Id = user.Id,
-                Name = user.Name,
-                HasPassword = hasPassword,
-                HasConfiguredPassword = hasConfiguredPassword,
-                HasConfiguredEasyPassword = hasConfiguredEasyPassword,
-                LastActivityDate = user.LastActivityDate,
-                LastLoginDate = user.LastLoginDate,
-                Configuration = user.Configuration,
-                ServerId = _appHost.SystemId,
-                Policy = user.Policy
-            };
-
-            if (!hasPassword && Users.Count() == 1)
-            {
-                dto.EnableAutoLogin = true;
-            }
-
-            var image = user.GetImageInfo(ImageType.Primary, 0);
-
-            if (image != null)
-            {
-                dto.PrimaryImageTag = GetImageCacheTag(user, image);
-
-                try
-                {
-                    _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user);
-                }
-                catch (Exception ex)
-                {
-                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
-                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name);
-                }
-            }
-
-            return dto;
-        }
-
-        public UserDto GetOfflineUserDto(User user)
-        {
-            var dto = GetUserDto(user);
-
-            dto.ServerName = _appHost.FriendlyName;
-
-            return dto;
-        }
-
-        private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
-        {
-            try
-            {
-                return _imageProcessorFactory().GetImageCacheTag(item, image);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path);
-                return null;
-            }
-        }
-
-        /// <summary>
-        /// Refreshes metadata for each user
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task RefreshUsersMetadata(CancellationToken cancellationToken)
-        {
-            foreach (var user in Users)
-            {
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Renames the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="newName">The new name.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public async Task RenameUser(User user, string newName)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (string.IsNullOrEmpty(newName))
-            {
-                throw new ArgumentNullException(nameof(newName));
-            }
-
-            if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
-            {
-                throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
-            }
-
-            if (user.Name.Equals(newName, StringComparison.Ordinal))
-            {
-                throw new ArgumentException("The new and old names must be different.");
-            }
-
-            await user.Rename(newName);
-
-            OnUserUpdated(user);
-        }
-
-        /// <summary>
-        /// Updates the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <exception cref="ArgumentNullException">user</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public void UpdateUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id)))
-            {
-                throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
-            }
-
-            user.DateModified = DateTime.UtcNow;
-            user.DateLastSaved = DateTime.UtcNow;
-
-            UserRepository.UpdateUser(user);
-
-            OnUserUpdated(user);
-        }
-
-        public event EventHandler<GenericEventArgs<User>> UserCreated;
-
-        private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1);
-
-        /// <summary>
-        /// Creates the user.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>User.</returns>
-        /// <exception cref="ArgumentNullException">name</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public async Task<User> CreateUser(string name)
-        {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
-            if (!IsValidUsername(name))
-            {
-                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
-            }
-
-            if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
-            {
-                throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
-            }
-
-            await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
-
-            try
-            {
-                var user = InstantiateNewUser(name);
-
-                var list = Users.ToList();
-                list.Add(user);
-                _users = list.ToArray();
-
-                user.DateLastSaved = DateTime.UtcNow;
-
-                UserRepository.CreateUser(user);
-
-                EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
-
-                return user;
-            }
-            finally
-            {
-                _userListLock.Release();
-            }
-        }
-
-        /// <summary>
-        /// Deletes the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">user</exception>
-        /// <exception cref="ArgumentException"></exception>
-        public async Task DeleteUser(User user)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            var allUsers = Users.ToList();
-
-            if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null)
-            {
-                throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id));
-            }
-
-            if (allUsers.Count == 1)
-            {
-                throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name));
-            }
-
-            if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1)
-            {
-                throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name));
-            }
-
-            await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
-
-            try
-            {
-                var configPath = GetConfigurationFilePath(user);
-
-                UserRepository.DeleteUser(user);
-
-                try
-                {
-                    _fileSystem.DeleteFile(configPath);
-                }
-                catch (IOException ex)
-                {
-                    _logger.LogError(ex, "Error deleting file {path}", configPath);
-                }
-
-                DeleteUserPolicy(user);
-
-                _users = allUsers.Where(i => i.Id != user.Id).ToArray();
-
-                OnUserDeleted(user);
-            }
-            finally
-            {
-                _userListLock.Release();
-            }
-        }
-
-        /// <summary>
-        /// Resets the password by clearing it.
-        /// </summary>
-        /// <returns>Task.</returns>
-        public Task ResetPassword(User user)
-        {
-            return ChangePassword(user, string.Empty);
-        }
-
-        public void ResetEasyPassword(User user)
-        {
-            ChangeEasyPassword(user, string.Empty, null);
-        }
-
-        public async Task ChangePassword(User user, string newPassword)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
-
-            UpdateUser(user);
-
-            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException(nameof(user));
-            }
-
-            if (newPassword != null)
-            {
-                newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
-            }
-
-            if (string.IsNullOrWhiteSpace(newPasswordHash))
-            {
-                throw new ArgumentNullException(nameof(newPasswordHash));
-            }
-
-            user.EasyPassword = newPasswordHash;
-
-            UpdateUser(user);
-
-            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
-        }
-
-        /// <summary>
-        /// Instantiates the new user.
-        /// </summary>
-        /// <param name="name">The name.</param>
-        /// <returns>User.</returns>
-        private static User InstantiateNewUser(string name)
-        {
-            return new User
-            {
-                Name = name,
-                Id = Guid.NewGuid(),
-                DateCreated = DateTime.UtcNow,
-                DateModified = DateTime.UtcNow,
-                UsesIdForConfigurationPath = true,
-                //Salt = BCrypt.GenerateSalt()
-            };
-        }
-
-        private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
-
-        private string _lastPin;
-        private PasswordPinCreationResult _lastPasswordPinCreationResult;
-        private int _pinAttempts;
-
-        private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
-        {
-            var num = new Random().Next(1, 9999);
-
-            var path = PasswordResetFile;
-
-            var pin = num.ToString("0000", CultureInfo.InvariantCulture);
-            _lastPin = pin;
-
-            var time = TimeSpan.FromMinutes(5);
-            var expiration = DateTime.UtcNow.Add(time);
-
-            var text = new StringBuilder();
-
-            var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
-
-            text.AppendLine("Use your web browser to visit:");
-            text.AppendLine(string.Empty);
-            text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
-            text.AppendLine(string.Empty);
-            text.AppendLine("Enter the following pin code:");
-            text.AppendLine(string.Empty);
-            text.AppendLine(pin);
-            text.AppendLine(string.Empty);
-
-            var localExpirationTime = expiration.ToLocalTime();
-            // Tuesday, 22 August 2006 06:30 AM
-            text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
-
-            File.WriteAllText(path, text.ToString(), Encoding.UTF8);
-
-            var result = new PasswordPinCreationResult
-            {
-                PinFile = path,
-                ExpirationDate = expiration
-            };
-
-            _lastPasswordPinCreationResult = result;
-            _pinAttempts = 0;
-
-            return result;
-        }
-
-        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
-        {
-            DeletePinFile();
-
-            var user = string.IsNullOrWhiteSpace(enteredUsername) ?
-                null :
-                GetUserByName(enteredUsername);
-
-            var action = ForgotPasswordAction.InNetworkRequired;
-            string pinFile = null;
-            DateTime? expirationDate = null;
-
-            if (user != null && !user.Policy.IsAdministrator)
-            {
-                action = ForgotPasswordAction.ContactAdmin;
-            }
-            else
-            {
-                if (isInNetwork)
-                {
-                    action = ForgotPasswordAction.PinCode;
-                }
-
-                var result = await CreatePasswordResetPin().ConfigureAwait(false);
-                pinFile = result.PinFile;
-                expirationDate = result.ExpirationDate;
-            }
-
-            return new ForgotPasswordResult
-            {
-                Action = action,
-                PinFile = pinFile,
-                PinExpirationDate = expirationDate
-            };
-        }
-
-        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
-        {
-            DeletePinFile();
-
-            var usersReset = new List<string>();
-
-            var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
-                string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
-                _lastPasswordPinCreationResult != null &&
-                _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
-
-            if (valid)
-            {
-                _lastPin = null;
-                _lastPasswordPinCreationResult = null;
-
-                foreach (var user in Users)
-                {
-                    await ResetPassword(user).ConfigureAwait(false);
-
-                    if (user.Policy.IsDisabled)
-                    {
-                        user.Policy.IsDisabled = false;
-                        UpdateUserPolicy(user, user.Policy, true);
-                    }
-                    usersReset.Add(user.Name);
-                }
-            }
-            else
-            {
-                _pinAttempts++;
-                if (_pinAttempts >= 3)
-                {
-                    _lastPin = null;
-                    _lastPasswordPinCreationResult = null;
-                }
-            }
-
-            return new PinRedeemResult
-            {
-                Success = valid,
-                UsersReset = usersReset.ToArray()
-            };
-        }
-
-        private void DeletePinFile()
-        {
-            try
-            {
-                _fileSystem.DeleteFile(PasswordResetFile);
-            }
-            catch
-            {
-
-            }
-        }
-
-        class PasswordPinCreationResult
-        {
-            public string PinFile { get; set; }
-            public DateTime ExpirationDate { get; set; }
-        }
-
-        public UserPolicy GetUserPolicy(User user)
-        {
-            var path = GetPolicyFilePath(user);
-
-            if (!File.Exists(path))
-            {
-                return GetDefaultPolicy(user);
-            }
-
-            try
-            {
-                lock (_policySyncLock)
-                {
-                    return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path);
-                }
-            }
-            catch (IOException)
-            {
-                return GetDefaultPolicy(user);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error reading policy file: {path}", path);
-
-                return GetDefaultPolicy(user);
-            }
-        }
-
-        private static UserPolicy GetDefaultPolicy(User user)
-        {
-            return new UserPolicy
-            {
-                EnableContentDownloading = true,
-                EnableSyncTranscoding = true
-            };
-        }
-
-        private readonly object _policySyncLock = new object();
-        public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
-        {
-            var user = GetUserById(userId);
-            UpdateUserPolicy(user, userPolicy, true);
-        }
-
-        private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent)
-        {
-            // The xml serializer will output differently if the type is not exact
-            if (userPolicy.GetType() != typeof(UserPolicy))
-            {
-                var json = _jsonSerializer.SerializeToString(userPolicy);
-                userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
-            }
-
-            var path = GetPolicyFilePath(user);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            lock (_policySyncLock)
-            {
-                _xmlSerializer.SerializeToFile(userPolicy, path);
-                user.Policy = userPolicy;
-            }
-
-            if (fireEvent)
-            {
-                UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
-            }
-        }
-
-        private void DeleteUserPolicy(User user)
-        {
-            var path = GetPolicyFilePath(user);
-
-            try
-            {
-                lock (_policySyncLock)
-                {
-                    _fileSystem.DeleteFile(path);
-                }
-            }
-            catch (IOException)
-            {
-
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error deleting policy file");
-            }
-        }
-
-        private static string GetPolicyFilePath(User user)
-        {
-            return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
-        }
-
-        private static string GetConfigurationFilePath(User user)
-        {
-            return Path.Combine(user.ConfigurationDirectoryPath, "config.xml");
-        }
-
-        public UserConfiguration GetUserConfiguration(User user)
-        {
-            var path = GetConfigurationFilePath(user);
-
-            if (!File.Exists(path))
-            {
-                return new UserConfiguration();
-            }
-
-            try
-            {
-                lock (_configSyncLock)
-                {
-                    return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path);
-                }
-            }
-            catch (IOException)
-            {
-                return new UserConfiguration();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error reading policy file: {path}", path);
-
-                return new UserConfiguration();
-            }
-        }
-
-        private readonly object _configSyncLock = new object();
-        public void UpdateConfiguration(Guid userId, UserConfiguration config)
-        {
-            var user = GetUserById(userId);
-            UpdateConfiguration(user, config);
-        }
-
-        public void UpdateConfiguration(User user, UserConfiguration config)
-        {
-            UpdateConfiguration(user, config, true);
-        }
-
-        private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent)
-        {
-            var path = GetConfigurationFilePath(user);
-
-            // The xml serializer will output differently if the type is not exact
-            if (config.GetType() != typeof(UserConfiguration))
-            {
-                var json = _jsonSerializer.SerializeToString(config);
-                config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json);
-            }
-
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-            lock (_configSyncLock)
-            {
-                _xmlSerializer.SerializeToFile(config, path);
-                user.Configuration = config;
-            }
-
-            if (fireEvent)
-            {
-                UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
-            }
-        }
-    }
-
-    public class DeviceAccessEntryPoint : IServerEntryPoint
-    {
-        private IUserManager _userManager;
-        private IAuthenticationRepository _authRepo;
-        private IDeviceManager _deviceManager;
-        private ISessionManager _sessionManager;
-
-        public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
-        {
-            _userManager = userManager;
-            _authRepo = authRepo;
-            _deviceManager = deviceManager;
-            _sessionManager = sessionManager;
-        }
-
-        public Task RunAsync()
-        {
-            _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
-
-            return Task.CompletedTask;
-        }
-
-        private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
-        {
-            var user = e.Argument;
-            if (!user.Policy.EnableAllDevices)
-            {
-                UpdateDeviceAccess(user);
-            }
-        }
-
-        private void UpdateDeviceAccess(User user)
-        {
-            var existing = _authRepo.Get(new AuthenticationInfoQuery
-            {
-                UserId = user.Id
-
-            }).Items;
-
-            foreach (var authInfo in existing)
-            {
-                if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
-                {
-                    _sessionManager.Logout(authInfo);
-                }
-            }
-        }
-
-        public void Dispose()
-        {
-
-        }
-    }
-}
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Plugins;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Cryptography;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Users;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Library
+{
+    /// <summary>
+    /// Class UserManager
+    /// </summary>
+    public class UserManager : IUserManager
+    {
+        /// <summary>
+        /// Gets the users.
+        /// </summary>
+        /// <value>The users.</value>
+        public IEnumerable<User> Users => _users;
+
+        private User[] _users;
+
+        /// <summary>
+        /// The _logger
+        /// </summary>
+        private readonly ILogger _logger;
+
+        /// <summary>
+        /// Gets or sets the configuration manager.
+        /// </summary>
+        /// <value>The configuration manager.</value>
+        private IServerConfigurationManager ConfigurationManager { get; set; }
+
+        /// <summary>
+        /// Gets the active user repository
+        /// </summary>
+        /// <value>The user repository.</value>
+        private IUserRepository UserRepository { get; set; }
+        public event EventHandler<GenericEventArgs<User>> UserPasswordChanged;
+
+        private readonly IXmlSerializer _xmlSerializer;
+        private readonly IJsonSerializer _jsonSerializer;
+
+        private readonly INetworkManager _networkManager;
+
+        private readonly Func<IImageProcessor> _imageProcessorFactory;
+        private readonly Func<IDtoService> _dtoServiceFactory;
+        private readonly IServerApplicationHost _appHost;
+        private readonly IFileSystem _fileSystem;
+        
+        private IAuthenticationProvider[] _authenticationProviders;
+        private DefaultAuthenticationProvider _defaultAuthenticationProvider;
+
+        public UserManager(
+            ILoggerFactory loggerFactory,
+            IServerConfigurationManager configurationManager,
+            IUserRepository userRepository,
+            IXmlSerializer xmlSerializer,
+            INetworkManager networkManager,
+            Func<IImageProcessor> imageProcessorFactory,
+            Func<IDtoService> dtoServiceFactory,
+            IServerApplicationHost appHost,
+            IJsonSerializer jsonSerializer,
+            IFileSystem fileSystem)
+        {
+            _logger = loggerFactory.CreateLogger(nameof(UserManager));
+            UserRepository = userRepository;
+            _xmlSerializer = xmlSerializer;
+            _networkManager = networkManager;
+            _imageProcessorFactory = imageProcessorFactory;
+            _dtoServiceFactory = dtoServiceFactory;
+            _appHost = appHost;
+            _jsonSerializer = jsonSerializer;
+            _fileSystem = fileSystem;
+            ConfigurationManager = configurationManager;
+            _users = Array.Empty<User>();
+
+            DeletePinFile();
+        }
+
+        public NameIdPair[] GetAuthenticationProviders()
+        {
+            return _authenticationProviders
+                .Where(i => i.IsEnabled)
+                .OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
+                .ThenBy(i => i.Name)
+                .Select(i => new NameIdPair
+                {
+                    Name = i.Name,
+                    Id = GetAuthenticationProviderId(i)
+                })
+                .ToArray();
+        }
+
+        public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
+        {
+            _authenticationProviders = authenticationProviders.ToArray();
+
+            _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
+        }
+
+        #region UserUpdated Event
+        /// <summary>
+        /// Occurs when [user updated].
+        /// </summary>
+        public event EventHandler<GenericEventArgs<User>> UserUpdated;
+        public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated;
+        public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated;
+        public event EventHandler<GenericEventArgs<User>> UserLockedOut;
+
+        /// <summary>
+        /// Called when [user updated].
+        /// </summary>
+        /// <param name="user">The user.</param>
+        private void OnUserUpdated(User user)
+        {
+            UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+        }
+        #endregion
+
+        #region UserDeleted Event
+        /// <summary>
+        /// Occurs when [user deleted].
+        /// </summary>
+        public event EventHandler<GenericEventArgs<User>> UserDeleted;
+        /// <summary>
+        /// Called when [user deleted].
+        /// </summary>
+        /// <param name="user">The user.</param>
+        private void OnUserDeleted(User user)
+        {
+            UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+        }
+        #endregion
+
+        /// <summary>
+        /// Gets a User by Id
+        /// </summary>
+        /// <param name="id">The id.</param>
+        /// <returns>User.</returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public User GetUserById(Guid id)
+        {
+            if (id == Guid.Empty)
+            {
+                throw new ArgumentException(nameof(id), "Guid can't be empty");
+            }
+
+            return Users.FirstOrDefault(u => u.Id == id);
+        }
+
+        /// <summary>
+        /// Gets the user by identifier.
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>User.</returns>
+        public User GetUserById(string id)
+        {
+            return GetUserById(new Guid(id));
+        }
+
+        public User GetUserByName(string name)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase));
+        }
+
+        public void Initialize()
+        {
+            _users = LoadUsers();
+
+            var users = Users.ToList();
+
+            // If there are no local users with admin rights, make them all admins
+            if (!users.Any(i => i.Policy.IsAdministrator))
+            {
+                foreach (var user in users)
+                {
+                    user.Policy.IsAdministrator = true;
+                    UpdateUserPolicy(user, user.Policy, false);
+                }
+            }
+        }
+
+        public static bool IsValidUsername(string username)
+        {
+            //This is some regex that matches only on unicode "word" characters, as well as -, _ and @
+            //In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
+            // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
+            return Regex.IsMatch(username, "^[\\w-'._@]*$");
+        }
+
+        private static bool IsValidUsernameCharacter(char i)
+        {
+            return IsValidUsername(i.ToString());
+        }
+
+        public string MakeValidUsername(string username)
+        {
+            if (IsValidUsername(username))
+            {
+                return username;
+            }
+
+            // Usernames can contain letters (a-z), numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)
+            var builder = new StringBuilder();
+
+            foreach (var c in username)
+            {
+                if (IsValidUsernameCharacter(c))
+                {
+                    builder.Append(c);
+                }
+            }
+            return builder.ToString();
+        }
+
+        public async Task<User> AuthenticateUser(string username, string password, string hashedPassword, string remoteEndPoint, bool isUserSession)
+        {
+            if (string.IsNullOrWhiteSpace(username))
+            {
+                throw new ArgumentNullException(nameof(username));
+            }
+
+            var user = Users
+                .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
+
+            var success = false;
+            IAuthenticationProvider authenticationProvider = null;
+
+            if (user != null)
+            {
+                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
+                authenticationProvider = authResult.Item1;
+                success = authResult.Item2;
+            }
+            else
+            {
+                // user is null
+                var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
+                authenticationProvider = authResult.Item1;
+                success = authResult.Item2;
+
+                if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
+                {
+                    user = await CreateUser(username).ConfigureAwait(false);
+
+                    var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
+                    if (hasNewUserPolicy != null)
+                    {
+                        var policy = hasNewUserPolicy.GetNewUserPolicy();
+                        UpdateUserPolicy(user, policy, true);
+                    }
+                }
+            }
+
+            if (success && user != null && authenticationProvider != null)
+            {
+                var providerId = GetAuthenticationProviderId(authenticationProvider);
+
+                if (!string.Equals(providerId, user.Policy.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
+                {
+                    user.Policy.AuthenticationProviderId = providerId;
+                    UpdateUserPolicy(user, user.Policy, true);
+                }
+            }
+
+            if (user == null)
+            {
+                throw new SecurityException("Invalid username or password entered.");
+            }
+
+            if (user.Policy.IsDisabled)
+            {
+                throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name));
+            }
+
+            if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
+            {
+                throw new SecurityException("Forbidden.");
+            }
+
+            if (!user.IsParentalScheduleAllowed())
+            {
+                throw new SecurityException("User is not allowed access at this time.");
+            }
+
+            // Update LastActivityDate and LastLoginDate, then save
+            if (success)
+            {
+                if (isUserSession)
+                {
+                    user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
+                    UpdateUser(user);
+                }
+                UpdateInvalidLoginAttemptCount(user, 0);
+            }
+            else
+            {
+                UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1);
+            }
+
+            _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
+
+            return success ? user : null;
+        }
+
+        private static string GetAuthenticationProviderId(IAuthenticationProvider provider)
+        {
+            return provider.GetType().FullName;
+        }
+
+        private IAuthenticationProvider GetAuthenticationProvider(User user)
+        {
+            return GetAuthenticationProviders(user).First();
+        }
+
+        private IAuthenticationProvider[] GetAuthenticationProviders(User user)
+        {
+            var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
+
+            var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray();
+
+            if (!string.IsNullOrEmpty(authenticationProviderId))
+            {
+                providers = providers.Where(i => string.Equals(authenticationProviderId, GetAuthenticationProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
+            }
+
+            if (providers.Length == 0)
+            {
+                providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
+            }
+
+            return providers;
+        }
+
+        private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
+        {
+            try
+            {
+                var requiresResolvedUser = provider as IRequiresResolvedUser;
+                if (requiresResolvedUser != null)
+                {
+                    await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
+                }
+                else
+                {
+                    await provider.Authenticate(username, password).ConfigureAwait(false);
+                }
+
+                return true;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
+
+                return false;
+            }
+        }
+
+        private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
+        {
+            bool success = false;
+            IAuthenticationProvider authenticationProvider = null;
+
+            if (password != null && user != null)
+            {
+                // Doesn't look like this is even possible to be used, because of password == null checks below
+                hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password);
+            }
+
+            if (password == null)
+            {
+                // legacy
+                success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+            }
+            else
+            {
+                foreach (var provider in GetAuthenticationProviders(user))
+                {
+                    success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
+
+                    if (success)
+                    {
+                        authenticationProvider = provider;
+                        break;
+                    }
+                }
+            }
+
+            if (user != null)
+            {
+                if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword)
+                {
+                    if (password == null)
+                    {
+                        // legacy
+                        success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
+                    }
+                    else
+                    {
+                        success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
+                    }
+                }
+            }
+
+            return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success);
+        }
+
+        private void UpdateInvalidLoginAttemptCount(User user, int newValue)
+        {
+            if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0)
+            {
+                return;
+            }
+
+            user.Policy.InvalidLoginAttemptCount = newValue;
+
+            var maxCount = user.Policy.IsAdministrator ? 3 : 5;
+
+            var fireLockout = false;
+
+            if (newValue >= maxCount)
+            {
+                _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue);
+                user.Policy.IsDisabled = true;
+
+                fireLockout = true;
+            }
+
+            UpdateUserPolicy(user, user.Policy, false);
+
+            if (fireLockout)
+            {
+                UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
+            }
+        }
+
+        private string GetLocalPasswordHash(User user)
+        {
+            return string.IsNullOrEmpty(user.EasyPassword)
+                ? null
+                : user.EasyPassword;
+        }
+
+        private bool IsPasswordEmpty(User user, string passwordHash)
+        {
+            return string.IsNullOrEmpty(passwordHash);
+        }
+
+        /// <summary>
+        /// Loads the users from the repository
+        /// </summary>
+        /// <returns>IEnumerable{User}.</returns>
+        private User[] LoadUsers()
+        {
+            var users = UserRepository.RetrieveAllUsers();
+
+            // There always has to be at least one user.
+            if (users.Count == 0)
+            {
+                var defaultName = Environment.UserName;
+                if (string.IsNullOrWhiteSpace(defaultName))
+                {
+                    defaultName = "MyJellyfinUser";
+                }
+                var name = MakeValidUsername(defaultName);
+
+                var user = InstantiateNewUser(name);
+
+                user.DateLastSaved = DateTime.UtcNow;
+
+                UserRepository.CreateUser(user);
+
+                users.Add(user);
+
+                user.Policy.IsAdministrator = true;
+                user.Policy.EnableContentDeletion = true;
+                user.Policy.EnableRemoteControlOfOtherUsers = true;
+                UpdateUserPolicy(user, user.Policy, false);
+            }
+
+            return users.ToArray();
+        }
+
+        public UserDto GetUserDto(User user, string remoteEndPoint = null)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            var hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
+            var hasConfiguredEasyPassword = !IsPasswordEmpty(user, GetLocalPasswordHash(user));
+
+            var hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
+                hasConfiguredEasyPassword :
+                hasConfiguredPassword;
+
+            var dto = new UserDto
+            {
+                Id = user.Id,
+                Name = user.Name,
+                HasPassword = hasPassword,
+                HasConfiguredPassword = hasConfiguredPassword,
+                HasConfiguredEasyPassword = hasConfiguredEasyPassword,
+                LastActivityDate = user.LastActivityDate,
+                LastLoginDate = user.LastLoginDate,
+                Configuration = user.Configuration,
+                ServerId = _appHost.SystemId,
+                Policy = user.Policy
+            };
+
+            if (!hasPassword && Users.Count() == 1)
+            {
+                dto.EnableAutoLogin = true;
+            }
+
+            var image = user.GetImageInfo(ImageType.Primary, 0);
+
+            if (image != null)
+            {
+                dto.PrimaryImageTag = GetImageCacheTag(user, image);
+
+                try
+                {
+                    _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user);
+                }
+                catch (Exception ex)
+                {
+                    // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions
+                    _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {user}", user.Name);
+                }
+            }
+
+            return dto;
+        }
+
+        public UserDto GetOfflineUserDto(User user)
+        {
+            var dto = GetUserDto(user);
+
+            dto.ServerName = _appHost.FriendlyName;
+
+            return dto;
+        }
+
+        private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
+        {
+            try
+            {
+                return _imageProcessorFactory().GetImageCacheTag(item, image);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error getting {imageType} image info for {imagePath}", image.Type, image.Path);
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Refreshes metadata for each user
+        /// </summary>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public async Task RefreshUsersMetadata(CancellationToken cancellationToken)
+        {
+            foreach (var user in Users)
+            {
+                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        /// <summary>
+        /// Renames the user.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="newName">The new name.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="ArgumentNullException">user</exception>
+        /// <exception cref="ArgumentException"></exception>
+        public async Task RenameUser(User user, string newName)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            if (string.IsNullOrEmpty(newName))
+            {
+                throw new ArgumentNullException(nameof(newName));
+            }
+
+            if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)))
+            {
+                throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName));
+            }
+
+            if (user.Name.Equals(newName, StringComparison.Ordinal))
+            {
+                throw new ArgumentException("The new and old names must be different.");
+            }
+
+            await user.Rename(newName);
+
+            OnUserUpdated(user);
+        }
+
+        /// <summary>
+        /// Updates the user.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <exception cref="ArgumentNullException">user</exception>
+        /// <exception cref="ArgumentException"></exception>
+        public void UpdateUser(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id)))
+            {
+                throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id));
+            }
+
+            user.DateModified = DateTime.UtcNow;
+            user.DateLastSaved = DateTime.UtcNow;
+
+            UserRepository.UpdateUser(user);
+
+            OnUserUpdated(user);
+        }
+
+        public event EventHandler<GenericEventArgs<User>> UserCreated;
+
+        private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1);
+
+        /// <summary>
+        /// Creates the user.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>User.</returns>
+        /// <exception cref="ArgumentNullException">name</exception>
+        /// <exception cref="ArgumentException"></exception>
+        public async Task<User> CreateUser(string name)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentNullException(nameof(name));
+            }
+
+            if (!IsValidUsername(name))
+            {
+                throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
+            }
+
+            if (Users.Any(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase)))
+            {
+                throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name));
+            }
+
+            await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
+
+            try
+            {
+                var user = InstantiateNewUser(name);
+
+                var list = Users.ToList();
+                list.Add(user);
+                _users = list.ToArray();
+
+                user.DateLastSaved = DateTime.UtcNow;
+
+                UserRepository.CreateUser(user);
+
+                EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger);
+
+                return user;
+            }
+            finally
+            {
+                _userListLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Deletes the user.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="ArgumentNullException">user</exception>
+        /// <exception cref="ArgumentException"></exception>
+        public async Task DeleteUser(User user)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            var allUsers = Users.ToList();
+
+            if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null)
+            {
+                throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id));
+            }
+
+            if (allUsers.Count == 1)
+            {
+                throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name));
+            }
+
+            if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1)
+            {
+                throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name));
+            }
+
+            await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false);
+
+            try
+            {
+                var configPath = GetConfigurationFilePath(user);
+
+                UserRepository.DeleteUser(user);
+
+                try
+                {
+                    _fileSystem.DeleteFile(configPath);
+                }
+                catch (IOException ex)
+                {
+                    _logger.LogError(ex, "Error deleting file {path}", configPath);
+                }
+
+                DeleteUserPolicy(user);
+
+                _users = allUsers.Where(i => i.Id != user.Id).ToArray();
+
+                OnUserDeleted(user);
+            }
+            finally
+            {
+                _userListLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Resets the password by clearing it.
+        /// </summary>
+        /// <returns>Task.</returns>
+        public Task ResetPassword(User user)
+        {
+            return ChangePassword(user, string.Empty);
+        }
+
+        public void ResetEasyPassword(User user)
+        {
+            ChangeEasyPassword(user, string.Empty, null);
+        }
+
+        public async Task ChangePassword(User user, string newPassword)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
+
+            UpdateUser(user);
+
+            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            if (newPassword != null)
+            {
+                newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
+            }
+
+            if (string.IsNullOrWhiteSpace(newPasswordHash))
+            {
+                throw new ArgumentNullException(nameof(newPasswordHash));
+            }
+
+            user.EasyPassword = newPasswordHash;
+
+            UpdateUser(user);
+
+            UserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
+        }
+
+        /// <summary>
+        /// Instantiates the new user.
+        /// </summary>
+        /// <param name="name">The name.</param>
+        /// <returns>User.</returns>
+        private static User InstantiateNewUser(string name)
+        {
+            return new User
+            {
+                Name = name,
+                Id = Guid.NewGuid(),
+                DateCreated = DateTime.UtcNow,
+                DateModified = DateTime.UtcNow,
+                UsesIdForConfigurationPath = true,
+                //Salt = BCrypt.GenerateSalt()
+            };
+        }
+
+        private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
+
+        private string _lastPin;
+        private PasswordPinCreationResult _lastPasswordPinCreationResult;
+        private int _pinAttempts;
+
+        private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
+        {
+            var num = new Random().Next(1, 9999);
+
+            var path = PasswordResetFile;
+
+            var pin = num.ToString("0000", CultureInfo.InvariantCulture);
+            _lastPin = pin;
+
+            var time = TimeSpan.FromMinutes(5);
+            var expiration = DateTime.UtcNow.Add(time);
+
+            var text = new StringBuilder();
+
+            var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
+
+            text.AppendLine("Use your web browser to visit:");
+            text.AppendLine(string.Empty);
+            text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
+            text.AppendLine(string.Empty);
+            text.AppendLine("Enter the following pin code:");
+            text.AppendLine(string.Empty);
+            text.AppendLine(pin);
+            text.AppendLine(string.Empty);
+
+            var localExpirationTime = expiration.ToLocalTime();
+            // Tuesday, 22 August 2006 06:30 AM
+            text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
+
+            File.WriteAllText(path, text.ToString(), Encoding.UTF8);
+
+            var result = new PasswordPinCreationResult
+            {
+                PinFile = path,
+                ExpirationDate = expiration
+            };
+
+            _lastPasswordPinCreationResult = result;
+            _pinAttempts = 0;
+
+            return result;
+        }
+
+        public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
+        {
+            DeletePinFile();
+
+            var user = string.IsNullOrWhiteSpace(enteredUsername) ?
+                null :
+                GetUserByName(enteredUsername);
+
+            var action = ForgotPasswordAction.InNetworkRequired;
+            string pinFile = null;
+            DateTime? expirationDate = null;
+
+            if (user != null && !user.Policy.IsAdministrator)
+            {
+                action = ForgotPasswordAction.ContactAdmin;
+            }
+            else
+            {
+                if (isInNetwork)
+                {
+                    action = ForgotPasswordAction.PinCode;
+                }
+
+                var result = await CreatePasswordResetPin().ConfigureAwait(false);
+                pinFile = result.PinFile;
+                expirationDate = result.ExpirationDate;
+            }
+
+            return new ForgotPasswordResult
+            {
+                Action = action,
+                PinFile = pinFile,
+                PinExpirationDate = expirationDate
+            };
+        }
+
+        public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
+        {
+            DeletePinFile();
+
+            var usersReset = new List<string>();
+
+            var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
+                string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
+                _lastPasswordPinCreationResult != null &&
+                _lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
+
+            if (valid)
+            {
+                _lastPin = null;
+                _lastPasswordPinCreationResult = null;
+
+                foreach (var user in Users)
+                {
+                    await ResetPassword(user).ConfigureAwait(false);
+
+                    if (user.Policy.IsDisabled)
+                    {
+                        user.Policy.IsDisabled = false;
+                        UpdateUserPolicy(user, user.Policy, true);
+                    }
+                    usersReset.Add(user.Name);
+                }
+            }
+            else
+            {
+                _pinAttempts++;
+                if (_pinAttempts >= 3)
+                {
+                    _lastPin = null;
+                    _lastPasswordPinCreationResult = null;
+                }
+            }
+
+            return new PinRedeemResult
+            {
+                Success = valid,
+                UsersReset = usersReset.ToArray()
+            };
+        }
+
+        private void DeletePinFile()
+        {
+            try
+            {
+                _fileSystem.DeleteFile(PasswordResetFile);
+            }
+            catch
+            {
+
+            }
+        }
+
+        class PasswordPinCreationResult
+        {
+            public string PinFile { get; set; }
+            public DateTime ExpirationDate { get; set; }
+        }
+
+        public UserPolicy GetUserPolicy(User user)
+        {
+            var path = GetPolicyFilePath(user);
+
+            if (!File.Exists(path))
+            {
+                return GetDefaultPolicy(user);
+            }
+
+            try
+            {
+                lock (_policySyncLock)
+                {
+                    return (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), path);
+                }
+            }
+            catch (IOException)
+            {
+                return GetDefaultPolicy(user);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error reading policy file: {path}", path);
+
+                return GetDefaultPolicy(user);
+            }
+        }
+
+        private static UserPolicy GetDefaultPolicy(User user)
+        {
+            return new UserPolicy
+            {
+                EnableContentDownloading = true,
+                EnableSyncTranscoding = true
+            };
+        }
+
+        private readonly object _policySyncLock = new object();
+        public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy)
+        {
+            var user = GetUserById(userId);
+            UpdateUserPolicy(user, userPolicy, true);
+        }
+
+        private void UpdateUserPolicy(User user, UserPolicy userPolicy, bool fireEvent)
+        {
+            // The xml serializer will output differently if the type is not exact
+            if (userPolicy.GetType() != typeof(UserPolicy))
+            {
+                var json = _jsonSerializer.SerializeToString(userPolicy);
+                userPolicy = _jsonSerializer.DeserializeFromString<UserPolicy>(json);
+            }
+
+            var path = GetPolicyFilePath(user);
+
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+            lock (_policySyncLock)
+            {
+                _xmlSerializer.SerializeToFile(userPolicy, path);
+                user.Policy = userPolicy;
+            }
+
+            if (fireEvent)
+            {
+                UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+            }
+        }
+
+        private void DeleteUserPolicy(User user)
+        {
+            var path = GetPolicyFilePath(user);
+
+            try
+            {
+                lock (_policySyncLock)
+                {
+                    _fileSystem.DeleteFile(path);
+                }
+            }
+            catch (IOException)
+            {
+
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error deleting policy file");
+            }
+        }
+
+        private static string GetPolicyFilePath(User user)
+        {
+            return Path.Combine(user.ConfigurationDirectoryPath, "policy.xml");
+        }
+
+        private static string GetConfigurationFilePath(User user)
+        {
+            return Path.Combine(user.ConfigurationDirectoryPath, "config.xml");
+        }
+
+        public UserConfiguration GetUserConfiguration(User user)
+        {
+            var path = GetConfigurationFilePath(user);
+
+            if (!File.Exists(path))
+            {
+                return new UserConfiguration();
+            }
+
+            try
+            {
+                lock (_configSyncLock)
+                {
+                    return (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), path);
+                }
+            }
+            catch (IOException)
+            {
+                return new UserConfiguration();
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error reading policy file: {path}", path);
+
+                return new UserConfiguration();
+            }
+        }
+
+        private readonly object _configSyncLock = new object();
+        public void UpdateConfiguration(Guid userId, UserConfiguration config)
+        {
+            var user = GetUserById(userId);
+            UpdateConfiguration(user, config);
+        }
+
+        public void UpdateConfiguration(User user, UserConfiguration config)
+        {
+            UpdateConfiguration(user, config, true);
+        }
+
+        private void UpdateConfiguration(User user, UserConfiguration config, bool fireEvent)
+        {
+            var path = GetConfigurationFilePath(user);
+
+            // The xml serializer will output differently if the type is not exact
+            if (config.GetType() != typeof(UserConfiguration))
+            {
+                var json = _jsonSerializer.SerializeToString(config);
+                config = _jsonSerializer.DeserializeFromString<UserConfiguration>(json);
+            }
+
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+            lock (_configSyncLock)
+            {
+                _xmlSerializer.SerializeToFile(config, path);
+                user.Configuration = config;
+            }
+
+            if (fireEvent)
+            {
+                UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user });
+            }
+        }
+    }
+
+    public class DeviceAccessEntryPoint : IServerEntryPoint
+    {
+        private IUserManager _userManager;
+        private IAuthenticationRepository _authRepo;
+        private IDeviceManager _deviceManager;
+        private ISessionManager _sessionManager;
+
+        public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
+        {
+            _userManager = userManager;
+            _authRepo = authRepo;
+            _deviceManager = deviceManager;
+            _sessionManager = sessionManager;
+        }
+
+        public Task RunAsync()
+        {
+            _userManager.UserPolicyUpdated += _userManager_UserPolicyUpdated;
+
+            return Task.CompletedTask;
+        }
+
+        private void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e)
+        {
+            var user = e.Argument;
+            if (!user.Policy.EnableAllDevices)
+            {
+                UpdateDeviceAccess(user);
+            }
+        }
+
+        private void UpdateDeviceAccess(User user)
+        {
+            var existing = _authRepo.Get(new AuthenticationInfoQuery
+            {
+                UserId = user.Id
+
+            }).Items;
+
+            foreach (var authInfo in existing)
+            {
+                if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
+                {
+                    _sessionManager.Logout(authInfo);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+
+        }
+    }
+}

+ 1 - 3
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -24,7 +24,6 @@ namespace Emby.Server.Implementations.Library.Validators
         /// </summary>
         private readonly ILogger _logger;
 
-        private readonly IServerConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
 
         /// <summary>
@@ -32,11 +31,10 @@ namespace Emby.Server.Implementations.Library.Validators
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="logger">The logger.</param>
-        public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem)
+        public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
         {
             _libraryManager = libraryManager;
             _logger = logger;
-            _config = config;
             _fileSystem = fileSystem;
         }
 

+ 3 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -105,8 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             _mediaSourceManager = mediaSourceManager;
             _streamHelper = streamHelper;
 
-            _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
-            _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
+            _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
+            _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
             _timerProvider.TimerFired += _timerProvider_TimerFired;
 
             _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
@@ -1708,7 +1708,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
             {
-                return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _httpClient, _processFactory, _config, _assemblyInfo);
+                return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
             }
 
             return new DirectRecorder(_logger, _httpClient, _fileSystem, _streamHelper);

+ 8 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -7,7 +7,6 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
@@ -17,7 +16,6 @@ using MediaBrowser.Model.Diagnostics;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Reflection;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
@@ -27,7 +25,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
     {
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
-        private readonly IHttpClient _httpClient;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerApplicationPaths _appPaths;
         private bool _hasExited;
@@ -38,19 +35,23 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly IJsonSerializer _json;
         private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly IServerConfigurationManager _config;
-        private readonly IAssemblyInfo _assemblyInfo;
 
-        public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, IHttpClient httpClient, IProcessFactory processFactory, IServerConfigurationManager config, IAssemblyInfo assemblyInfo)
+        public EncodedRecorder(
+            ILogger logger,
+            IFileSystem fileSystem,
+            IMediaEncoder mediaEncoder,
+            IServerApplicationPaths appPaths,
+            IJsonSerializer json,
+            IProcessFactory processFactory,
+            IServerConfigurationManager config)
         {
             _logger = logger;
             _fileSystem = fileSystem;
             _mediaEncoder = mediaEncoder;
             _appPaths = appPaths;
             _json = json;
-            _httpClient = httpClient;
             _processFactory = processFactory;
             _config = config;
-            _assemblyInfo = assemblyInfo;
         }
 
         private static bool CopySubtitles => false;

+ 1 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -17,15 +17,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         protected readonly ILogger Logger;
         private readonly string _dataPath;
         protected readonly Func<T, T, bool> EqualityComparer;
-        private readonly IFileSystem _fileSystem;
 
-        public ItemDataProvider(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
+        public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer)
         {
             Logger = logger;
             _dataPath = dataPath;
             EqualityComparer = equalityComparer;
             _jsonSerializer = jsonSerializer;
-            _fileSystem = fileSystem;
         }
 
         public IReadOnlyList<T> GetAll()

+ 2 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs

@@ -1,6 +1,5 @@
 using System;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
@@ -8,8 +7,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 {
     public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
     {
-        public SeriesTimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
-            : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+        public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
+            : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
         {
         }
 

+ 2 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -5,7 +5,6 @@ using System.Linq;
 using System.Threading;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Events;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
@@ -19,8 +18,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
 
-        public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
-            : base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
+        public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
+            : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
         {
             _logger = logger1;
         }

+ 1 - 1
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.LiveTv
 
         public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken)
         {
-            var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId);
+            var user = query.UserId == Guid.Empty ? null : _userManager.GetUserById(query.UserId);
 
             var topFolder = GetInternalLiveTvFolder(cancellationToken);
 

+ 1 - 4
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -9,7 +9,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
@@ -23,18 +22,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         protected readonly IServerConfigurationManager Config;
         protected readonly ILogger Logger;
         protected IJsonSerializer JsonSerializer;
-        protected readonly IMediaEncoder MediaEncoder;
         protected readonly IFileSystem FileSystem;
 
         private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
             new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
 
-        protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem)
+        protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
         {
             Config = config;
             Logger = logger;
             JsonSerializer = jsonSerializer;
-            MediaEncoder = mediaEncoder;
             FileSystem = fileSystem;
         }
 

+ 10 - 3
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -31,15 +31,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
-        private readonly IEnvironmentInfo _environment;
 
-        public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, IEnvironmentInfo environment) : base(config, logger, jsonSerializer, mediaEncoder, fileSystem)
+        public HdHomerunHost(
+            IServerConfigurationManager config,
+            ILogger logger,
+            IJsonSerializer jsonSerializer,
+            IFileSystem fileSystem,
+            IHttpClient httpClient,
+            IServerApplicationHost appHost,
+            ISocketFactory socketFactory,
+            INetworkManager networkManager)
+            : base(config, logger, jsonSerializer, fileSystem)
         {
             _httpClient = httpClient;
             _appHost = appHost;
             _socketFactory = socketFactory;
             _networkManager = networkManager;
-            _environment = environment;
         }
 
         public string Name => "HD Homerun";

+ 4 - 5
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -26,15 +26,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     {
         private readonly IHttpClient _httpClient;
         private readonly IServerApplicationHost _appHost;
-        private readonly IEnvironmentInfo _environment;
         private readonly INetworkManager _networkManager;
         private readonly IMediaSourceManager _mediaSourceManager;
 
-        public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, IEnvironmentInfo environment, INetworkManager networkManager) : base(config, logger, jsonSerializer, mediaEncoder, fileSystem)
+        public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager)
+            : base(config, logger, jsonSerializer, fileSystem)
         {
             _httpClient = httpClient;
             _appHost = appHost;
-            _environment = environment;
             _networkManager = networkManager;
             _mediaSourceManager = mediaSourceManager;
         }
@@ -52,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var channelIdPrefix = GetFullChannelIdPrefix(info);
 
-            var result = await new M3uParser(Logger, FileSystem, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
+            var result = await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
 
             return result.Cast<ChannelInfo>().ToList();
         }
@@ -115,7 +114,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task Validate(TunerHostInfo info)
         {
-            using (var stream = await new M3uParser(Logger, FileSystem, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
+            using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
             {
 
             }

+ 40 - 38
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -19,14 +19,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
     public class M3uParser
     {
         private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
         private readonly IHttpClient _httpClient;
         private readonly IServerApplicationHost _appHost;
 
-        public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost)
+        public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost)
         {
             _logger = logger;
-            _fileSystem = fileSystem;
             _httpClient = httpClient;
             _appHost = appHost;
         }
@@ -157,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))
@@ -214,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;

+ 25 - 25
Emby.Server.Implementations/Localization/Core/da.json

@@ -2,10 +2,10 @@
     "Albums": "Album",
     "AppDeviceValues": "App: {0}, Enhed: {1}",
     "Application": "Applikation",
-    "Artists": "Kunstner",
+    "Artists": "Kunstnere",
     "AuthenticationSucceededWithUserName": "{0} bekræftet med succes",
     "Books": "Bøger",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
     "Channels": "Kanaler",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Samlinger",
@@ -14,41 +14,41 @@
     "FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}",
     "Favorites": "Favoritter",
     "Folders": "Mapper",
-    "Genres": "Genre",
+    "Genres": "Genrer",
     "HeaderAlbumArtists": "Albumkunstnere",
-    "HeaderCameraUploads": "Camera Uploads",
+    "HeaderCameraUploads": "Kamera Uploads",
     "HeaderContinueWatching": "Fortsæt Afspilning",
     "HeaderFavoriteAlbums": "Favoritalbum",
     "HeaderFavoriteArtists": "Favoritkunstnere",
-    "HeaderFavoriteEpisodes": "Favoritepisoder",
-    "HeaderFavoriteShows": "Favorit serier",
-    "HeaderFavoriteSongs": "Favoritsange",
+    "HeaderFavoriteEpisodes": "Favorit-afsnit",
+    "HeaderFavoriteShows": "Favorit-serier",
+    "HeaderFavoriteSongs": "Favorit-sange",
     "HeaderLiveTV": "Live TV",
     "HeaderNextUp": "Næste",
-    "HeaderRecordingGroups": "Optagegrupper",
+    "HeaderRecordingGroups": "Optagelsesgrupper",
     "HomeVideos": "Hjemmevideoer",
-    "Inherit": "Arv",
+    "Inherit": "Nedarv",
     "ItemAddedWithName": "{0} blev tilføjet til biblioteket",
     "ItemRemovedWithName": "{0} blev fjernet fra biblioteket",
     "LabelIpAddressValue": "IP-adresse: {0}",
     "LabelRunningTimeValue": "Spilletid: {0}",
     "Latest": "Seneste",
     "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server konfigurationssektion {0} er blevet opdateret",
-    "MessageServerConfigurationUpdated": "Serverkonfiguration er blevet opdateret",
+    "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurationsafsnit {0} er blevet opdateret",
+    "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Music": "Musik",
     "MusicVideos": "Musikvideoer",
-    "NameInstallFailed": "{0} installation failed",
+    "NameInstallFailed": "{0} installationen mislykkedes",
     "NameSeasonNumber": "Sæson {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+    "NameSeasonUnknown": "Ukendt Sæson",
+    "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.",
     "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig",
     "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret",
-    "NotificationOptionAudioPlayback": "Audioafspilning påbegyndt",
-    "NotificationOptionAudioPlaybackStopped": "Audioafspilning stoppet",
+    "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
+    "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
     "NotificationOptionInstallationFailed": "Installationsfejl",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
@@ -70,16 +70,16 @@
     "ProviderValue": "Udbyder: {0}",
     "ScheduledTaskFailedWithName": "{0} fejlet",
     "ScheduledTaskStartedWithName": "{0} påbegyndt",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
-    "Shows": "Shows",
+    "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
+    "Shows": "Serier",
     "Songs": "Sange",
-    "StartupEmbyServerIsLoading": "Jellyfin Server indlæser. Prøv venligst igen om kort tid.",
+    "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.",
     "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}",
     "SubtitlesDownloadedForItem": "Undertekster downloadet for {0}",
     "Sync": "Synk",
     "System": "System",
-    "TvShows": "TV Shows",
+    "TvShows": "TV serier",
     "User": "Bruger",
     "UserCreatedWithName": "Bruger {0} er blevet oprettet",
     "UserDeletedWithName": "Brugeren {0} er blevet slettet",
@@ -88,10 +88,10 @@
     "UserOfflineFromDevice": "{0} har afbrudt fra {1}",
     "UserOnlineFromDevice": "{0} er online fra {1}",
     "UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+    "UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}",
     "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
-    "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
+    "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {0}"
 }

+ 27 - 27
Emby.Server.Implementations/Localization/Core/de.json

@@ -3,61 +3,61 @@
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "Application": "Anwendung",
     "Artists": "Interpreten",
-    "AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
+    "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
     "Books": "Bücher",
-    "CameraImageUploadedFrom": "Ein neues Bild wurde hochgeladen von {0}",
+    "CameraImageUploadedFrom": "Ein neues Foto wurde hochgeladen von {0}",
     "Channels": "Kanäle",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Sammlungen",
     "DeviceOfflineWithName": "{0} wurde getrennt",
-    "DeviceOnlineWithName": "{0} ist verbunden",
+    "DeviceOnlineWithName": "{0} hat sich verbunden",
     "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
     "Favorites": "Favoriten",
     "Folders": "Verzeichnisse",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Album-Künstler",
-    "HeaderCameraUploads": "Kamera Uploads",
+    "HeaderAlbumArtists": "Album-Interpreten",
+    "HeaderCameraUploads": "Kamera-Uploads",
     "HeaderContinueWatching": "Weiterschauen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
-    "HeaderFavoriteArtists": "Interpreten Favoriten",
+    "HeaderFavoriteArtists": "Lieblings-Interpreten",
     "HeaderFavoriteEpisodes": "Lieblingsepisoden",
     "HeaderFavoriteShows": "Lieblingsserien",
-    "HeaderFavoriteSongs": "Lieder Favoriten",
-    "HeaderLiveTV": "Live TV",
+    "HeaderFavoriteSongs": "Lieblingslieder",
+    "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Als Nächstes",
     "HeaderRecordingGroups": "Aufnahme-Gruppen",
     "HomeVideos": "Heimvideos",
     "Inherit": "Übernehmen",
     "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
     "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
-    "LabelIpAddressValue": "IP Adresse: {0}",
+    "LabelIpAddressValue": "IP-Adresse: {0}",
     "LabelRunningTimeValue": "Laufzeit: {0}",
     "Latest": "Neueste",
-    "MessageApplicationUpdated": "Jellyfin Server wurde auf den neusten Stand gebracht.",
-    "MessageApplicationUpdatedTo": "Jellyfin Server wurde auf Version {0} aktualisiert",
+    "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
+    "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
     "MessageNamedServerConfigurationUpdatedWithValue": "Der Server Einstellungsbereich {0} wurde aktualisiert",
-    "MessageServerConfigurationUpdated": "Server Einstellungen wurden aktualisiert",
+    "MessageServerConfigurationUpdated": "Servereinstellungen wurden aktualisiert",
     "MixedContent": "Gemischte Inhalte",
     "Movies": "Filme",
     "Music": "Musik",
     "MusicVideos": "Musikvideos",
-    "NameInstallFailed": "{0} Installation fehlgeschlagen",
+    "NameInstallFailed": "Installation von {0} fehlgeschlagen",
     "NameSeasonNumber": "Staffel {0}",
     "NameSeasonUnknown": "Staffel unbekannt",
-    "NewVersionIsAvailable": "Eine neue Version von Jellyfin Server steht zum Download bereit.",
+    "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.",
     "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
     "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
     "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
     "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt",
-    "NotificationOptionCameraImageUploaded": "Kamera Bild hochgeladen",
+    "NotificationOptionCameraImageUploaded": "Foto hochgeladen",
     "NotificationOptionInstallationFailed": "Installationsfehler",
     "NotificationOptionNewLibraryContent": "Neuer Inhalt hinzugefügt",
-    "NotificationOptionPluginError": "Plugin Fehler",
+    "NotificationOptionPluginError": "Plugin-Fehler",
     "NotificationOptionPluginInstalled": "Plugin installiert",
     "NotificationOptionPluginUninstalled": "Plugin deinstalliert",
     "NotificationOptionPluginUpdateInstalled": "Pluginaktualisierung installiert",
     "NotificationOptionServerRestartRequired": "Serverneustart notwendig",
-    "NotificationOptionTaskFailed": "Geplante Aufgaben fehlgeschlagen",
+    "NotificationOptionTaskFailed": "Geplante Aufgabe fehlgeschlagen",
     "NotificationOptionUserLockedOut": "Benutzer ausgeschlossen",
     "NotificationOptionVideoPlayback": "Videowiedergabe gestartet",
     "NotificationOptionVideoPlaybackStopped": "Videowiedergabe gestoppt",
@@ -68,18 +68,18 @@
     "PluginUninstalledWithName": "{0} wurde deinstalliert",
     "PluginUpdatedWithName": "{0} wurde aktualisiert",
     "ProviderValue": "Anbieter: {0}",
-    "ScheduledTaskFailedWithName": "{0} fehlgeschlagen",
-    "ScheduledTaskStartedWithName": "{0} gestartet",
+    "ScheduledTaskFailedWithName": "{0} ist fehlgeschlagen",
+    "ScheduledTaskStartedWithName": "{0} wurde gestartet",
     "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
     "Shows": "Serien",
     "Songs": "Songs",
-    "StartupEmbyServerIsLoading": "Jellyfin Server startet, bitte versuche es gleich noch einmal.",
+    "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.",
     "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
     "SubtitlesDownloadedForItem": "Untertitel heruntergeladen für {0}",
     "Sync": "Synchronisation",
     "System": "System",
-    "TvShows": "TV Sendungen",
+    "TvShows": "TV-Sendungen",
     "User": "Benutzer",
     "UserCreatedWithName": "Benutzer {0} wurde erstellt",
     "UserDeletedWithName": "Benutzer {0} wurde gelöscht",
@@ -88,10 +88,10 @@
     "UserOfflineFromDevice": "{0} wurde getrennt von {1}",
     "UserOnlineFromDevice": "{0} ist online von {1}",
     "UserPasswordChangedWithName": "Das Passwort für Benutzer {0} wurde geändert",
-    "UserPolicyUpdatedWithName": "Benutzerrichtlinie wurde für {0} aktualisiert",
-    "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} gestartet",
-    "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} beendet",
-    "ValueHasBeenAddedToLibrary": "{0} wurde ihrer Bibliothek hinzugefügt",
-    "ValueSpecialEpisodeName": "Special - {0}",
+    "UserPolicyUpdatedWithName": "Benutzerrichtlinie von {0} wurde aktualisiert",
+    "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
+    "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
+    "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
+    "ValueSpecialEpisodeName": "Extra - {0}",
     "VersionNumber": "Version {0}"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -90,7 +90,7 @@
     "UserPasswordChangedWithName": "Password has been changed for user {0}",
     "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
     "UserStartedPlayingItemWithValues": "{0} has started playing {1}",
-    "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}",
+    "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
     "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {0}"

+ 1 - 1
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -30,7 +30,7 @@
     "Inherit": "Inherit",
     "ItemAddedWithName": "{0} was added to the library",
     "ItemRemovedWithName": "{0} was removed from the library",
-    "LabelIpAddressValue": "Ip address: {0}",
+    "LabelIpAddressValue": "IP address: {0}",
     "LabelRunningTimeValue": "Running time: {0}",
     "Latest": "Latest",
     "MessageApplicationUpdated": "Jellyfin Server has been updated",

+ 23 - 23
Emby.Server.Implementations/Localization/Core/es.json

@@ -5,46 +5,46 @@
     "Artists": "Artistas",
     "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
     "Books": "Libros",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
     "Channels": "Canales",
     "ChapterNameValue": "Capítulo {0}",
     "Collections": "Colecciones",
     "DeviceOfflineWithName": "{0} se ha desconectado",
     "DeviceOnlineWithName": "{0} está conectado",
-    "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión a partir de {0}",
+    "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}",
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas del Álbum",
-    "HeaderCameraUploads": "Camera Uploads",
+    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderCameraUploads": "Subidas desde cámara",
     "HeaderContinueWatching": "Continuar viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
     "HeaderFavoriteEpisodes": "Episodios favoritos",
     "HeaderFavoriteShows": "Programas favoritos",
     "HeaderFavoriteSongs": "Canciones favoritas",
-    "HeaderLiveTV": "TV en vivo",
+    "HeaderLiveTV": "TV en directo",
     "HeaderNextUp": "Siguiendo",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "HomeVideos": "Vídeos de inicio",
+    "HomeVideos": "Vídeos caseros",
     "Inherit": "Heredar",
     "ItemAddedWithName": "{0} se ha añadido a la biblioteca",
-    "ItemRemovedWithName": "{0} se elimina de la biblioteca",
+    "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
     "LabelIpAddressValue": "Dirección IP: {0}",
     "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
     "Latest": "Últimos",
     "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "La sección de configuración del servidor {0} ha sido actualizado",
+    "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",
     "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor",
     "MixedContent": "Contenido mixto",
-    "Movies": "Peliculas",
+    "Movies": "Películas",
     "Music": "Música",
-    "MusicVideos": "Videos musicales",
-    "NameInstallFailed": "{0} installation failed",
+    "MusicVideos": "Vídeos musicales",
+    "NameInstallFailed": "{0} error de instalación",
     "NameSeasonNumber": "Temporada {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+    "NameSeasonUnknown": "Temporada desconocida",
+    "NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.",
     "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
     "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
     "NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
@@ -56,13 +56,13 @@
     "NotificationOptionPluginInstalled": "Plugin instalado",
     "NotificationOptionPluginUninstalled": "Plugin desinstalado",
     "NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada",
-    "NotificationOptionServerRestartRequired": "Requiere reinicio del servidor",
+    "NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor",
     "NotificationOptionTaskFailed": "Error de tarea programada",
     "NotificationOptionUserLockedOut": "Usuario bloqueado",
     "NotificationOptionVideoPlayback": "Se inició la reproducción de vídeo",
     "NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo detenida",
     "Photos": "Fotos",
-    "Playlists": "Listas reproducción",
+    "Playlists": "Listas de reproducción",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} se ha instalado",
     "PluginUninstalledWithName": "{0} se ha desinstalado",
@@ -70,16 +70,16 @@
     "ProviderValue": "Proveedor: {0}",
     "ScheduledTaskFailedWithName": "{0} falló",
     "ScheduledTaskStartedWithName": "{0} iniciada",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
     "Shows": "Series",
     "Songs": "Canciones",
     "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
     "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
     "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}",
     "Sync": "Sincronizar",
     "System": "Sistema",
-    "TvShows": "Series TV",
+    "TvShows": "Series de TV",
     "User": "Usuario",
     "UserCreatedWithName": "El usuario {0} ha sido creado",
     "UserDeletedWithName": "El usuario {0} ha sido borrado",
@@ -88,10 +88,10 @@
     "UserOfflineFromDevice": "{0} se ha desconectado de {1}",
     "UserOnlineFromDevice": "{0} está en línea desde {1}",
     "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
-    "UserStartedPlayingItemWithValues": "{0} ha comenzado reproducir {1}",
-    "UserStoppedPlayingItemWithValues": "{0} ha parado de reproducir {1}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}",
+    "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
+    "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
+    "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia",
     "ValueSpecialEpisodeName": "Especial - {0}",
     "VersionNumber": "Versión {0}"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/fr.json

@@ -36,7 +36,7 @@
     "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
     "MessageApplicationUpdatedTo": "Jellyfin Serveur a été mis à jour en version {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
-    "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour.",
+    "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
     "MixedContent": "Contenu mixte",
     "Movies": "Films",
     "Music": "Musique",

+ 35 - 35
Emby.Server.Implementations/Localization/Core/hu.json

@@ -5,48 +5,48 @@
     "Artists": "Előadók",
     "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva",
     "Books": "Könyvek",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Új kamerakép került feltöltésre {0}",
     "Channels": "Csatornák",
     "ChapterNameValue": "Jelenet {0}",
     "Collections": "Gyűjtemények",
     "DeviceOfflineWithName": "{0} kijelentkezett",
     "DeviceOnlineWithName": "{0} belépett",
-    "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
+    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet {0}",
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
     "HeaderAlbumArtists": "Album Előadók",
-    "HeaderCameraUploads": "Camera Uploads",
-    "HeaderContinueWatching": "Vetítés(ek) folytatása",
+    "HeaderCameraUploads": "Kamera feltöltések",
+    "HeaderContinueWatching": "Folyamatban lévő filmek",
     "HeaderFavoriteAlbums": "Kedvenc Albumok",
     "HeaderFavoriteArtists": "Kedvenc Művészek",
     "HeaderFavoriteEpisodes": "Kedvenc Epizódok",
     "HeaderFavoriteShows": "Kedvenc Műsorok",
     "HeaderFavoriteSongs": "Kedvenc Dalok",
-    "HeaderLiveTV": "Live TV",
+    "HeaderLiveTV": "Élő TV",
     "HeaderNextUp": "Következik",
-    "HeaderRecordingGroups": "Recording Groups",
+    "HeaderRecordingGroups": "Felvételi csoportok",
     "HomeVideos": "Házi videók",
     "Inherit": "Inherit",
-    "ItemAddedWithName": "{0} was added to the library",
-    "ItemRemovedWithName": "{0} was removed from the library",
-    "LabelIpAddressValue": "Ip cím: {0}",
-    "LabelRunningTimeValue": "Running time: {0}",
+    "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
+    "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
+    "LabelIpAddressValue": "IP cím: {0}",
+    "LabelRunningTimeValue": "Futási idő: {0}",
     "Latest": "Legújabb",
     "MessageApplicationUpdated": "Jellyfin Szerver frissítve",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
+    "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész {0} frissítve",
     "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve",
     "MixedContent": "Vegyes tartalom",
     "Movies": "Filmek",
     "Music": "Zene",
     "MusicVideos": "Zenei Videók",
-    "NameInstallFailed": "{0} installation failed",
-    "NameSeasonNumber": "Season {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
-    "NotificationOptionApplicationUpdateAvailable": "Program frissítés elérhető",
-    "NotificationOptionApplicationUpdateInstalled": "Program frissítés telepítve",
+    "NameInstallFailed": "{0} sikertelen telepítés",
+    "NameSeasonNumber": "Évad {0}",
+    "NameSeasonUnknown": "Ismeretlen évad",
+    "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.",
+    "NotificationOptionApplicationUpdateAvailable": "Új programfrissítés érhető el",
+    "NotificationOptionApplicationUpdateInstalled": "Programfrissítés telepítve",
     "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
     "NotificationOptionAudioPlaybackStopped": "Audió lejátszás befejezve",
     "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
@@ -57,7 +57,7 @@
     "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
     "NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve",
     "NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges",
-    "NotificationOptionTaskFailed": "Scheduled task failure",
+    "NotificationOptionTaskFailed": "Ütemezett feladat hiba",
     "NotificationOptionUserLockedOut": "Felhasználó tiltva",
     "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve",
     "NotificationOptionVideoPlaybackStopped": "Videó lejátszás befejezve",
@@ -68,30 +68,30 @@
     "PluginUninstalledWithName": "{0} eltávolítva",
     "PluginUpdatedWithName": "{0} frissítve",
     "ProviderValue": "Provider: {0}",
-    "ScheduledTaskFailedWithName": "{0} failed",
-    "ScheduledTaskStartedWithName": "{0} started",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ScheduledTaskFailedWithName": "{0} hiba",
+    "ScheduledTaskStartedWithName": "{0} elkezdve",
+    "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
     "Shows": "Műsorok",
     "Songs": "Dalok",
-    "StartupEmbyServerIsLoading": "Jellyfin Szerver betöltődik. Kérjük, próbáld meg újra később.",
+    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
-    "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
+    "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen:  {0} ehhez: {1}",
+    "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz {0}",
     "Sync": "Szinkronizál",
     "System": "Rendszer",
     "TvShows": "TV Műsorok",
     "User": "Felhasználó",
-    "UserCreatedWithName": "User {0} has been created",
-    "UserDeletedWithName": "User {0} has been deleted",
+    "UserCreatedWithName": "{0} felhasználó létrehozva",
+    "UserDeletedWithName": "{0} felhasználó törölve",
     "UserDownloadingItemWithValues": "{0} letölti {1}",
-    "UserLockedOutWithName": "User {0} has been locked out",
-    "UserOfflineFromDevice": "{0} kijelentkezett innen  {1}",
-    "UserOnlineFromDevice": "{0} is online from {1}",
-    "UserPasswordChangedWithName": "Password has been changed for user {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
-    "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt {1}",
-    "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt {1}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "UserLockedOutWithName": "{0}  felhasználó zárolva van",
+    "UserOfflineFromDevice": "{0} kijelentkezett innen:  {1}",
+    "UserOnlineFromDevice": "{0} online itt:  {1}",
+    "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
+    "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett {0}",
+    "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt:  {2}",
+    "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt: {1} itt:  {2}",
+    "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
     "ValueSpecialEpisodeName": "Special - {0}",
-    "VersionNumber": "Verzió {0}"
+    "VersionNumber": "Verzió: {0}"
 }

+ 4 - 4
Emby.Server.Implementations/Localization/Core/it.json

@@ -5,13 +5,13 @@
     "Artists": "Artisti",
     "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
     "Books": "Libri",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera {0}",
     "Channels": "Canali",
     "ChapterNameValue": "Capitolo {0}",
     "Collections": "Collezioni",
     "DeviceOfflineWithName": "{0} è stato disconnesso",
     "DeviceOnlineWithName": "{0} è connesso",
-    "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da  {0}",
+    "FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
     "Favorites": "Preferiti",
     "Folders": "Cartelle",
     "Genres": "Generi",
@@ -19,9 +19,9 @@
     "HeaderCameraUploads": "Caricamenti Fotocamera",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album preferiti",
-    "HeaderFavoriteArtists": "Artisti preferiti",
+    "HeaderFavoriteArtists": "Artisti Preferiti",
     "HeaderFavoriteEpisodes": "Episodi Preferiti",
-    "HeaderFavoriteShows": "Show preferiti",
+    "HeaderFavoriteShows": "Serie TV Preferite",
     "HeaderFavoriteSongs": "Brani Preferiti",
     "HeaderLiveTV": "Diretta TV",
     "HeaderNextUp": "Prossimo",

+ 94 - 94
Emby.Server.Implementations/Localization/Core/kk.json

@@ -1,97 +1,97 @@
 {
-    "Albums": "Альбомдар",
-    "AppDeviceValues": "Қолданба: {0}, Құрылғы: {1}",
-    "Application": "Қолданба",
-    "Artists": "Орындаушылар",
-    "AuthenticationSucceededWithUserName": "{0} түпнұсқалығын расталуы сәтті",
-    "Books": "Кітаптар",
-    "CameraImageUploadedFrom": "Жаңа сурет {0} камерасынан жүктеп алынды",
-    "Channels": "Арналар",
-    "ChapterNameValue": "{0}-сахна",
-    "Collections": "Жиынтықтар",
-    "DeviceOfflineWithName": "{0} ажыратылған",
-    "DeviceOnlineWithName": "{0} қосылған",
-    "FailedLoginAttemptWithUserName": "{0} тарапынан кіру әрекеті сәтсіз",
-    "Favorites": "Таңдаулылар",
-    "Folders": "Қалталар",
-    "Genres": "Жанрлар",
-    "HeaderAlbumArtists": "Альбом орындаушылары",
-    "HeaderCameraUploads": "Камерадан жүктелгендер",
-    "HeaderContinueWatching": "Қарауды жалғастыру",
-    "HeaderFavoriteAlbums": "Таңдаулы альбомдар",
-    "HeaderFavoriteArtists": "Таңдаулы орындаушылар",
-    "HeaderFavoriteEpisodes": "Таңдаулы бөлімдер",
-    "HeaderFavoriteShows": "Таңдаулы көрсетімдер",
-    "HeaderFavoriteSongs": "Таңдаулы әуендер",
-    "HeaderLiveTV": "Эфир",
-    "HeaderNextUp": "Кезекті",
-    "HeaderRecordingGroups": "Жазба топтары",
-    "HomeVideos": "Үйлік бейнелер",
-    "Inherit": "Мұраға иелену",
-    "ItemAddedWithName": "{0} тасығышханаға үстелінді",
-    "ItemRemovedWithName": "{0} тасығышханадан аласталды",
-    "LabelIpAddressValue": "IP-мекенжайы: {0}",
-    "LabelRunningTimeValue": "Іске қосылу уақыты: {0}",
-    "Latest": "Ең кейінгі",
-    "MessageApplicationUpdated": "Jellyfin Server жаңартылды.",
-    "MessageApplicationUpdatedTo": "Jellyfin Server {0} үшін жаңартылды",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Сервер теңшелімі ({0} бөлімі) жаңартылды",
-    "MessageServerConfigurationUpdated": "Сервер теңшелімі жаңартылды",
-    "MixedContent": "Аралас мазмұн",
-    "Movies": "Фильмдер",
-    "Music": "Музыка",
-    "MusicVideos": "Музыкалық бейнелер",
-    "NameInstallFailed": "{0} орнатылуы сәтсіз",
-    "NameSeasonNumber": "{0}-маусым",
-    "NameSeasonUnknown": "Белгісіз маусым",
-    "NewVersionIsAvailable": "Жаңа Jellyfin Server нұсқасы жүктеп алуға қолжетімді.",
-    "NotificationOptionApplicationUpdateAvailable": "Қолданба жаңартуы қолжетімді",
-    "NotificationOptionApplicationUpdateInstalled": "Қолданба жаңартуы орнатылды",
-    "NotificationOptionAudioPlayback": "Дыбыс ойнатуы басталды",
-    "NotificationOptionAudioPlaybackStopped": "Дыбыс ойнатуы тоқтатылды",
-    "NotificationOptionCameraImageUploaded": "Камерадан фотосурет кері қотарылған",
-    "NotificationOptionInstallationFailed": "Орнату сәтсіздігі",
-    "NotificationOptionNewLibraryContent": "Жаңа мазмұн үстелген",
-    "NotificationOptionPluginError": "Плагин сәтсіздігі",
-    "NotificationOptionPluginInstalled": "Плагин орнатылды",
-    "NotificationOptionPluginUninstalled": "Плагин орнатуы болдырылмады",
-    "NotificationOptionPluginUpdateInstalled": "Плагин жаңартуы орнатылды",
-    "NotificationOptionServerRestartRequired": "Серверді қайта іске қосу қажет",
-    "NotificationOptionTaskFailed": "Жоспарлаған тапсырма сәтсіздігі",
-    "NotificationOptionUserLockedOut": "Пайдаланушы құрсаулы",
-    "NotificationOptionVideoPlayback": "Бейне ойнатуы басталды",
-    "NotificationOptionVideoPlaybackStopped": "Бейне ойнатуы тоқтатылды",
-    "Photos": "Фотосуреттер",
-    "Playlists": "Ойнату тізімдері",
-    "Plugin": "Плагин",
-    "PluginInstalledWithName": "{0} орнатылды",
-    "PluginUninstalledWithName": "{0} жойылды",
-    "PluginUpdatedWithName": "{0} жаңартылды",
-    "ProviderValue": "Жеткізуші: {0}",
-    "ScheduledTaskFailedWithName": "{0} сәтсіз",
-    "ScheduledTaskStartedWithName": "{0} іске қосылды",
-    "ServerNameNeedsToBeRestarted": "{0} қайта іске қосу қажет",
-    "Shows": "Көрсетімдер",
-    "Songs": "Әуендер",
-    "StartupEmbyServerIsLoading": "Jellyfin Server жүктелуде. Әрекетті көп ұзамай қайталаңыз.",
+    "Albums": "Álbomdar",
+    "AppDeviceValues": "Qoldanba: {0}, Qurylǵy: {1}",
+    "Application": "Qoldanba",
+    "Artists": "Oryndaýshylar",
+    "AuthenticationSucceededWithUserName": "{0} túpnusqalyǵyn rastalýy sátti",
+    "Books": "Kitaptar",
+    "CameraImageUploadedFrom": "Jańa sýret {0} kamerasynan júktep alyndy",
+    "Channels": "Arnalar",
+    "ChapterNameValue": "{0}-sahna",
+    "Collections": "Jıyntyqtar",
+    "DeviceOfflineWithName": "{0} ajyratylǵan",
+    "DeviceOnlineWithName": "{0} qosylǵan",
+    "FailedLoginAttemptWithUserName": "{0} tarapynan kirý áreketi sátsiz",
+    "Favorites": "Tańdaýlylar",
+    "Folders": "Qaltalar",
+    "Genres": "Janrlar",
+    "HeaderAlbumArtists": "Álbom oryndaýshylary",
+    "HeaderCameraUploads": "Kameradan júktelgender",
+    "HeaderContinueWatching": "Qaraýdy jalǵastyrý",
+    "HeaderFavoriteAlbums": "Tańdaýly álbomdar",
+    "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
+    "HeaderFavoriteEpisodes": "Tańdaýly bólimder",
+    "HeaderFavoriteShows": "Tańdaýly kórsetimder",
+    "HeaderFavoriteSongs": "Tańdaýly áýender",
+    "HeaderLiveTV": "Efır",
+    "HeaderNextUp": "Kezekti",
+    "HeaderRecordingGroups": "Jazba toptary",
+    "HomeVideos": "Úılik beıneler",
+    "Inherit": "Muraǵa ıelený",
+    "ItemAddedWithName": "{0} tasyǵyshhanaǵa ústelindi",
+    "ItemRemovedWithName": "{0} tasyǵyshhanadan alastaldy",
+    "LabelIpAddressValue": "IP-mekenjaıy: {0}",
+    "LabelRunningTimeValue": "Oınatý ýaqyty: {0}",
+    "Latest": "Eń keıingi",
+    "MessageApplicationUpdated": "Jellyfin Serveri jańartyldy",
+    "MessageApplicationUpdatedTo": "Jellyfin Serveri {0} deńgeıge jańartyldy",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Server teńsheliminiń {0} bólimi jańartyldy",
+    "MessageServerConfigurationUpdated": "Server teńshelimi jańartyldy",
+    "MixedContent": "Aralas mazmun",
+    "Movies": "Fılmder",
+    "Music": "Mýzyka",
+    "MusicVideos": "Mýzykalyq beıneler",
+    "NameInstallFailed": "{0} ornatylýy sátsiz",
+    "NameSeasonNumber": "{0}-maýsym",
+    "NameSeasonUnknown": "Belgisiz maýsym",
+    "NewVersionIsAvailable": "Jańa Jellyfin Server nusqasy júktep alýǵa qoljetimdi.",
+    "NotificationOptionApplicationUpdateAvailable": "Qoldanba jańartýy qoljetimdi",
+    "NotificationOptionApplicationUpdateInstalled": "Qoldanba jańartýy ornatyldy",
+    "NotificationOptionAudioPlayback": "Dybys oınatýy bastaldy",
+    "NotificationOptionAudioPlaybackStopped": "Dybys oınatýy toqtatyldy",
+    "NotificationOptionCameraImageUploaded": "Kameradan fotosýret keri qotarylǵan",
+    "NotificationOptionInstallationFailed": "Ornatý sátsizdigi",
+    "NotificationOptionNewLibraryContent": "Jańa mazmun ústelgen",
+    "NotificationOptionPluginError": "Plagın sátsizdigi",
+    "NotificationOptionPluginInstalled": "Plagın ornatyldy",
+    "NotificationOptionPluginUninstalled": "Plagın ornatýy boldyrylmady",
+    "NotificationOptionPluginUpdateInstalled": "Plagın jańartýy ornatyldy",
+    "NotificationOptionServerRestartRequired": "Serverdi qaıta iske qosý qajet",
+    "NotificationOptionTaskFailed": "Josparlaǵan tapsyrma sátsizdigi",
+    "NotificationOptionUserLockedOut": "Paıdalanýshy qursaýly",
+    "NotificationOptionVideoPlayback": "Beıne oınatýy bastaldy",
+    "NotificationOptionVideoPlaybackStopped": "Beıne oınatýy toqtatyldy",
+    "Photos": "Fotosýretter",
+    "Playlists": "Oınatý tizimderi",
+    "Plugin": "Plagın",
+    "PluginInstalledWithName": "{0} ornatyldy",
+    "PluginUninstalledWithName": "{0} joıyldy",
+    "PluginUpdatedWithName": "{0} jańartyldy",
+    "ProviderValue": "Jetkizýshi: {0}",
+    "ScheduledTaskFailedWithName": "{0} sátsiz",
+    "ScheduledTaskStartedWithName": "{0} iske qosyldy",
+    "ServerNameNeedsToBeRestarted": "{0} qaıta iske qosý qajet",
+    "Shows": "Kórsetimder",
+    "Songs": "Áýender",
+    "StartupEmbyServerIsLoading": "Jellyfin Server júktelýde. Áreketti kóp uzamaı qaıtalańyz.",
     "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
-    "SubtitlesDownloadedForItem": "{0} үшін субтитрлер жүктеліп алынды",
-    "Sync": "Үндестіру",
-    "System": "Жүйе",
-    "TvShows": "ТД-көрсетімдер",
-    "User": "Пайдаланушы",
-    "UserCreatedWithName": "Пайдаланушы {0} жасалған",
-    "UserDeletedWithName": "Пайдаланушы {0} жойылған",
-    "UserDownloadingItemWithValues": "{0} мынаны жүктеп алуда: {1}",
-    "UserLockedOutWithName": "Пайдаланушы {0} құрсаулы",
-    "UserOfflineFromDevice": "{0} - {1} тарапынан ажыратылған",
-    "UserOnlineFromDevice": "{0} - {1} арқылы қосылған",
-    "UserPasswordChangedWithName": "Пайдаланушы {0} үшін құпия сөз өзгертілді",
-    "UserPolicyUpdatedWithName": "Пайдаланушы {0} үшін саясаттары жаңартылды",
-    "UserStartedPlayingItemWithValues": "{0} - {1} ойнатуын  {2} бастады",
-    "UserStoppedPlayingItemWithValues": "{0} - {1} ойнатуын  {2} тоқтатты",
-    "ValueHasBeenAddedToLibrary": "{0} (тасығышханаға үстелінді)",
-    "ValueSpecialEpisodeName": "Арнайы - {0}",
-    "VersionNumber": "Нұсқасы: {0}"
+    "SubtitleDownloadFailureFromForItem": "{1} úshin sýbtıtrlerdi {0} kózinen júktep alý sátsiz",
+    "SubtitlesDownloadedForItem": "{0} úshin sýbtıtrler júktelip alyndy",
+    "Sync": "Úndestirý",
+    "System": "Júıe",
+    "TvShows": "TD-kórsetimder",
+    "User": "Paıdalanýshy",
+    "UserCreatedWithName": "Paıdalanýshy {0} jasalǵan",
+    "UserDeletedWithName": "Paıdalanýshy {0} joıylǵan",
+    "UserDownloadingItemWithValues": "{0} mynany júktep alýda: {1}",
+    "UserLockedOutWithName": "Paıdalanýshy {0} qursaýly",
+    "UserOfflineFromDevice": "{0} - {1} tarapynan ajyratylǵan",
+    "UserOnlineFromDevice": "{0} - {1} arqyly qosylǵan",
+    "UserPasswordChangedWithName": "Paıdalanýshy {0} úshin paról ózgertildi",
+    "UserPolicyUpdatedWithName": "Paıdalanýshy {0} úshin saıasattary jańartyldy",
+    "UserStartedPlayingItemWithValues": "{0} - {1} oınatýyn {2} bastady",
+    "UserStoppedPlayingItemWithValues": "{0} - {1} oınatýyn {2} toqtatty",
+    "ValueHasBeenAddedToLibrary": "{0} (tasyǵyshhanaǵa ústelindi)",
+    "ValueSpecialEpisodeName": "Arnaıy - {0}",
+    "VersionNumber": "Nusqasy {0}"
 }

+ 3 - 3
Emby.Server.Implementations/Localization/Core/ms.json

@@ -1,10 +1,10 @@
 {
-    "Albums": "Albums",
+    "Albums": "Album-album",
     "AppDeviceValues": "App: {0}, Device: {1}",
     "Application": "Application",
-    "Artists": "Artists",
+    "Artists": "Artis-artis",
     "AuthenticationSucceededWithUserName": "{0} successfully authenticated",
-    "Books": "Books",
+    "Books": "Buku-buku",
     "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
     "Channels": "Channels",
     "ChapterNameValue": "Chapter {0}",

+ 18 - 18
Emby.Server.Implementations/Localization/Core/nl.json

@@ -5,28 +5,28 @@
     "Artists": "Artiesten",
     "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
     "Books": "Boeken",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd via {0}",
     "Channels": "Kanalen",
     "ChapterNameValue": "Hoofdstuk {0}",
     "Collections": "Collecties",
-    "DeviceOfflineWithName": "{0} is losgekoppeld",
+    "DeviceOfflineWithName": "{0} heeft de verbinding verbroken",
     "DeviceOnlineWithName": "{0} is verbonden",
     "FailedLoginAttemptWithUserName": "Mislukte aanmeld poging van {0}",
     "Favorites": "Favorieten",
     "Folders": "Mappen",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Album artiesten",
-    "HeaderCameraUploads": "Camera uploads",
+    "HeaderAlbumArtists": "Albumartiesten",
+    "HeaderCameraUploads": "Camera-uploads",
     "HeaderContinueWatching": "Kijken hervatten",
     "HeaderFavoriteAlbums": "Favoriete albums",
     "HeaderFavoriteArtists": "Favoriete artiesten",
     "HeaderFavoriteEpisodes": "Favoriete afleveringen",
     "HeaderFavoriteShows": "Favoriete shows",
-    "HeaderFavoriteSongs": "Favoriete titels",
+    "HeaderFavoriteSongs": "Favoriete nummers",
     "HeaderLiveTV": "Live TV",
     "HeaderNextUp": "Volgende",
     "HeaderRecordingGroups": "Opnamegroepen",
-    "HomeVideos": "Thuis video's",
+    "HomeVideos": "Start video's",
     "Inherit": "Overerven",
     "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
     "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -34,22 +34,22 @@
     "LabelRunningTimeValue": "Looptijd: {0}",
     "Latest": "Nieuwste",
     "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
+    "MessageApplicationUpdatedTo": "Jellyfin Server is bijgewerkt naar {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Sectie {0} van de server configuratie is bijgewerkt",
     "MessageServerConfigurationUpdated": "Server configuratie is bijgewerkt",
     "MixedContent": "Gemengde inhoud",
     "Movies": "Films",
     "Music": "Muziek",
     "MusicVideos": "Muziekvideo's",
-    "NameInstallFailed": "{0} installation failed",
+    "NameInstallFailed": "{0} installatie mislukt",
     "NameSeasonNumber": "Seizoen {0}",
     "NameSeasonUnknown": "Seizoen onbekend",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+    "NewVersionIsAvailable": "Een nieuwe versie van Jellyfin Server is beschikbaar om te downloaden.",
     "NotificationOptionApplicationUpdateAvailable": "Programma-update beschikbaar",
     "NotificationOptionApplicationUpdateInstalled": "Programma-update geïnstalleerd",
-    "NotificationOptionAudioPlayback": "Geluid gestart",
-    "NotificationOptionAudioPlaybackStopped": "Geluid gestopt",
-    "NotificationOptionCameraImageUploaded": "Camera afbeelding geüpload",
+    "NotificationOptionAudioPlayback": "Muziek gestart",
+    "NotificationOptionAudioPlaybackStopped": "Muziek gestopt",
+    "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload",
     "NotificationOptionInstallationFailed": "Installatie mislukt",
     "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd",
     "NotificationOptionPluginError": "Plug-in fout",
@@ -70,12 +70,12 @@
     "ProviderValue": "Aanbieder: {0}",
     "ScheduledTaskFailedWithName": "{0} is mislukt",
     "ScheduledTaskStartedWithName": "{0} is gestart",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
+    "ServerNameNeedsToBeRestarted": "{0} moet herstart worden",
     "Shows": "Series",
-    "Songs": "Titels",
+    "Songs": "Nummers",
     "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.",
     "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}",
     "SubtitlesDownloadedForItem": "Ondertiteling voor {0} is gedownload",
     "Sync": "Synchronisatie",
     "System": "Systeem",
@@ -89,9 +89,9 @@
     "UserOnlineFromDevice": "{0} heeft verbinding met {1}",
     "UserPasswordChangedWithName": "Wachtwoord voor {0} is gewijzigd",
     "UserPolicyUpdatedWithName": "Gebruikersbeleid gewijzigd voor {0}",
-    "UserStartedPlayingItemWithValues": "{0} heeft afspelen van {1} gestart",
-    "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "UserStartedPlayingItemWithValues": "{0} heeft afspelen van {1} gestart op {2}",
+    "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}",
+    "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek",
     "ValueSpecialEpisodeName": "Speciaal - {0}",
     "VersionNumber": "Versie {0}"
 }

+ 5 - 5
Emby.Server.Implementations/Localization/Core/ru.json

@@ -5,7 +5,7 @@
     "Artists": "Исполнители",
     "AuthenticationSucceededWithUserName": "{0} - авторизация успешна",
     "Books": "Литература",
-    "CameraImageUploadedFrom": "Новое фото было выложено с {0}",
+    "CameraImageUploadedFrom": "Новое фото было выложено с камеры {0}",
     "Channels": "Каналы",
     "ChapterNameValue": "Сцена {0}",
     "Collections": "Коллекции",
@@ -31,20 +31,20 @@
     "ItemAddedWithName": "{0} - добавлено в медиатеку",
     "ItemRemovedWithName": "{0} - изъято из медиатеки",
     "LabelIpAddressValue": "IP-адрес: {0}",
-    "LabelRunningTimeValue": "Время выполнения: {0}",
+    "LabelRunningTimeValue": "Длительность: {0}",
     "Latest": "Новейшее",
     "MessageApplicationUpdated": "Jellyfin Server был обновлён",
     "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена",
     "MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена",
-    "MixedContent": "Смешанное содержание",
+    "MixedContent": "Смешанное содержимое",
     "Movies": "Кино",
     "Music": "Музыка",
     "MusicVideos": "Муз. видео",
     "NameInstallFailed": "Установка {0} неудачна",
     "NameSeasonNumber": "Сезон {0}",
     "NameSeasonUnknown": "Сезон неопознан",
-    "NewVersionIsAvailable": "Имеется новая версия Jellyfin Server",
+    "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
     "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
     "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
     "NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но",
@@ -75,7 +75,7 @@
     "Songs": "Композиции",
     "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
     "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
     "SubtitlesDownloadedForItem": "Субтитры к {0} загружены",
     "Sync": "Синхро",
     "System": "Система",

+ 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

+ 21 - 0
Emby.Server.Implementations/Serialization/JsonSerializer.cs

@@ -41,6 +41,27 @@ namespace Emby.Server.Implementations.Serialization
             ServiceStack.Text.JsonSerializer.SerializeToStream(obj, obj.GetType(), stream);
         }
 
+        /// <summary>
+        /// Serializes to stream.
+        /// </summary>
+        /// <param name="obj">The obj.</param>
+        /// <param name="stream">The stream.</param>
+        /// <exception cref="ArgumentNullException">obj</exception>
+        public void SerializeToStream<T>(T obj, Stream stream)
+        {
+            if (obj == null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            if (stream == null)
+            {
+                throw new ArgumentNullException(nameof(stream));
+            }
+
+            ServiceStack.Text.JsonSerializer.SerializeToStream<T>(obj, stream);
+        }
+
         /// <summary>
         /// Serializes to file.
         /// </summary>

+ 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.

+ 15 - 25
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -116,6 +116,7 @@ namespace Emby.Server.Implementations.Updates
         private readonly IApplicationHost _applicationHost;
 
         private readonly ICryptoProvider _cryptographyProvider;
+        private readonly IZipClient _zipClient;
 
         // netframework or netcore
         private readonly string _packageRuntime;
@@ -129,6 +130,7 @@ namespace Emby.Server.Implementations.Updates
             IServerConfigurationManager config,
             IFileSystem fileSystem,
             ICryptoProvider cryptographyProvider,
+            IZipClient zipClient,
             string packageRuntime)
         {
             if (loggerFactory == null)
@@ -146,6 +148,7 @@ namespace Emby.Server.Implementations.Updates
             _config = config;
             _fileSystem = fileSystem;
             _cryptographyProvider = cryptographyProvider;
+            _zipClient = zipClient;
             _packageRuntime = packageRuntime;
             _logger = loggerFactory.CreateLogger(nameof(InstallationManager));
         }
@@ -526,14 +529,18 @@ namespace Emby.Server.Implementations.Updates
 
         private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken)
         {
-            // Target based on if it is an archive or single assembly
-            //  zip archives are assumed to contain directory structures relative to our ProgramDataPath
             var extension = Path.GetExtension(package.targetFilename);
-            var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".rar", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".7z", StringComparison.OrdinalIgnoreCase);
+            var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase);
+
+            if (!isArchive)
+            {
+                _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
+                return;
+            }
 
             if (target == null)
             {
-                target = Path.Combine(isArchive ? _appPaths.TempUpdatePath : _appPaths.PluginsPath, package.targetFilename);
+                target = Path.Combine(_appPaths.PluginsPath, Path.GetFileNameWithoutExtension(package.targetFilename));
             }
 
             // Download to temporary file so that, if interrupted, it won't destroy the existing installation
@@ -547,36 +554,19 @@ namespace Emby.Server.Implementations.Updates
 
             cancellationToken.ThrowIfCancellationRequested();
 
-            // Validate with a checksum
-            var packageChecksum = string.IsNullOrWhiteSpace(package.checksum) ? Guid.Empty : new Guid(package.checksum);
-            if (!packageChecksum.Equals(Guid.Empty)) // support for legacy uploads for now
-            {
-                using (var stream = File.OpenRead(tempFile))
-                {
-                    var check = Guid.Parse(BitConverter.ToString(_cryptographyProvider.ComputeMD5(stream)).Replace("-", string.Empty));
-                    if (check != packageChecksum)
-                    {
-                        throw new Exception(string.Format("Download validation failed for {0}.  Probably corrupted during transfer.", package.name));
-                    }
-                }
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
+            // TODO: Validate with a checksum, *properly*
 
             // Success - move it to the real target
             try
             {
-                Directory.CreateDirectory(Path.GetDirectoryName(target));
-                File.Copy(tempFile, target, true);
-                //If it is an archive - write out a version file so we know what it is
-                if (isArchive)
+                using (var stream = File.OpenRead(tempFile))
                 {
-                    File.WriteAllText(target + ".ver", package.versionStr);
+                    _zipClient.ExtractAllFromZip(stream, target, true);
                 }
             }
             catch (IOException ex)
             {
-                _logger.LogError(ex, "Error attempting to move file from {TempFile} to {TargetFile}", tempFile, target);
+                _logger.LogError(ex, "Error attempting to extract {TempFile} to {TargetFile}", tempFile, target);
                 throw;
             }
 

+ 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;
 

+ 25 - 7
Jellyfin.Server/CoreAppHost.cs

@@ -5,28 +5,47 @@ using Emby.Server.Implementations.HttpServer;
 using Jellyfin.Server.SocketSharp;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.System;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server
 {
     public class CoreAppHost : ApplicationHost
     {
-        public CoreAppHost(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, StartupOptions options, IFileSystem fileSystem, IEnvironmentInfo environmentInfo, MediaBrowser.Controller.Drawing.IImageEncoder imageEncoder, MediaBrowser.Common.Net.INetworkManager networkManager)
-            : base(applicationPaths, loggerFactory, options, fileSystem, environmentInfo, imageEncoder, networkManager)
+        public CoreAppHost(
+            ServerApplicationPaths applicationPaths,
+            ILoggerFactory loggerFactory,
+            StartupOptions options,
+            IFileSystem fileSystem,
+            IEnvironmentInfo environmentInfo,
+            MediaBrowser.Controller.Drawing.IImageEncoder imageEncoder,
+            MediaBrowser.Common.Net.INetworkManager networkManager,
+            IConfiguration configuration)
+            : base(
+                applicationPaths,
+                loggerFactory,
+                options,
+                fileSystem,
+                environmentInfo,
+                imageEncoder,
+                networkManager,
+                configuration)
         {
         }
 
         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 +56,6 @@ namespace Jellyfin.Server
                 CryptographyProvider,
                 SupportsDualModeSockets,
                 FileSystemManager,
-                EnvironmentInfo
-            );
+                EnvironmentInfo);
     }
 }

+ 7 - 0
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" />

+ 161 - 102
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;
@@ -34,6 +35,7 @@ namespace Jellyfin.Server
         private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
         private static ILogger _logger;
         private static bool _restartOnShutdown;
+        private static IConfiguration appConfig;
 
         public static async Task Main(string[] args)
         {
@@ -56,13 +58,32 @@ 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);
+
+            appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
+
+            CreateLogger(appConfig, appPaths);
+
             _logger = _loggerFactory.CreateLogger("Main");
 
             AppDomain.CurrentDomain.UnhandledException += (sender, e)
@@ -75,6 +96,7 @@ namespace Jellyfin.Server
                 {
                     return; // Already shutting down
                 }
+
                 e.Cancel = true;
                 _logger.LogInformation("Ctrl+C, shutting down");
                 Environment.ExitCode = 128 + 2;
@@ -88,6 +110,7 @@ namespace Jellyfin.Server
                 {
                     return; // Already shutting down
                 }
+
                 _logger.LogInformation("Received a SIGTERM signal, shutting down");
                 Environment.ExitCode = 128 + 15;
                 Shutdown();
@@ -101,9 +124,9 @@ 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);
+            var fileSystem = new ManagedFileSystem(_loggerFactory, environmentInfo, appPaths);
 
             using (var appHost = new CoreAppHost(
                 appPaths,
@@ -112,20 +135,21 @@ namespace Jellyfin.Server
                 fileSystem,
                 environmentInfo,
                 new NullImageEncoder(),
-                new NetworkManager(_loggerFactory, environmentInfo)))
+                new NetworkManager(_loggerFactory, environmentInfo),
+                appConfig))
             {
-                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,136 +163,185 @@ 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))
+                    {
+                        // Hang cache folder off already set dataDir
+                        cacheDir = Path.Combine(dataDir, "cache");
+                    }
+                    else
                     {
-                        cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
+                        // $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)
+        private static async Task<IConfiguration> CreateConfiguration(IApplicationPaths appPaths)
         {
-            try
-            {
-                string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
+            string configPath = Path.Combine(appPaths.ConfigurationDirectoryPath, "logging.json");
 
-                if (!File.Exists(configPath))
+            if (!File.Exists(configPath))
+            {
+                // For some reason the csproj name is used instead of the assembly name
+                using (Stream rscstr = typeof(Program).Assembly
+                    .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json"))
+                using (Stream fstr = File.Open(configPath, FileMode.CreateNew))
                 {
-                    // For some reason the csproj name is used instead of the assembly name
-                    using (Stream rscstr = typeof(Program).Assembly
-                        .GetManifestResourceStream("Jellyfin.Server.Resources.Configuration.logging.json"))
-                    using (Stream fstr = File.Open(configPath, FileMode.CreateNew))
-                    {
-                        await rscstr.CopyToAsync(fstr).ConfigureAwait(false);
-                    }
+                    await rscstr.CopyToAsync(fstr).ConfigureAwait(false);
                 }
-                var configuration = new ConfigurationBuilder()
-                    .SetBasePath(appPaths.ConfigurationDirectoryPath)
-                    .AddJsonFile("logging.json")
-                    .AddEnvironmentVariables("JELLYFIN_")
-                    .Build();
+            }
 
+            return new ConfigurationBuilder()
+                .SetBasePath(appPaths.ConfigurationDirectoryPath)
+                .AddJsonFile("logging.json")
+                .AddEnvironmentVariables("JELLYFIN_")
+                .AddInMemoryCollection(ConfigurationOptions.Configuration)
+                .Build();
+        }
+
+        private static void CreateLogger(IConfiguration configuration, IApplicationPaths appPaths)
+        {
+            try
+            {
                 // Serilog.Log is used by SerilogLoggerFactory when no logger is specified
                 Serilog.Log.Logger = new LoggerConfiguration()
                     .ReadFrom.Configuration(configuration)
@@ -290,7 +363,7 @@ namespace Jellyfin.Server
             }
         }
 
-        public static IImageEncoder GetImageEncoder(
+        private static IImageEncoder GetImageEncoder(
             IFileSystem fileSystem,
             IApplicationPaths appPaths,
             ILocalizationManager localizationManager)
@@ -331,26 +404,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;
         }
     }
 }

+ 53 - 39
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,31 +25,7 @@ namespace Jellyfin.Server.SocketSharp
             this.request = httpContext.Request;
             this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
 
-            //HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
-        }
-
-        private static string GetHandlerPathIfAny(string listenerUrl)
-        {
-            if (listenerUrl == null)
-            {
-                return null;
-            }
-
-            var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase);
-            if (pos == -1)
-            {
-                return null;
-            }
-
-            var startHostUrl = listenerUrl.Substring(pos + "://".Length);
-            var endPos = startHostUrl.IndexOf('/');
-            if (endPos == -1)
-            {
-                return null;
-            }
-
-            var endHostUrl = startHostUrl.Substring(endPos + 1);
-            return string.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/');
+            // HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
         }
 
         public HttpListenerRequest HttpRequest => request;
@@ -69,9 +46,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"];
 
@@ -99,7 +78,7 @@ namespace Jellyfin.Server.SocketSharp
             name = name.Trim(HttpTrimCharacters);
 
             // First, check for correctly formed multi-line value
-            // Second, check for absenece of CTL characters
+            // Second, check for absence of CTL characters
             int crlf = 0;
             for (int i = 0; i < name.Length; ++i)
             {
@@ -107,6 +86,7 @@ namespace Jellyfin.Server.SocketSharp
                 switch (crlf)
                 {
                     case 0:
+                    {
                         if (c == '\r')
                         {
                             crlf = 1;
@@ -121,29 +101,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 +146,7 @@ namespace Jellyfin.Server.SocketSharp
                     return true;
                 }
             }
+
             return false;
         }
 
@@ -216,8 +207,15 @@ namespace Jellyfin.Server.SocketSharp
             {
                 foreach (var acceptsType in acceptContentTypes)
                 {
-                    var contentType = HttpResultFactory.GetRealContentType(acceptsType);
-                    acceptsAnything = acceptsAnything || contentType == "*/*";
+                    // TODO: @bond move to Span when Span.Split lands
+                    // https://github.com/dotnet/corefx/issues/26528
+                    var contentType = acceptsType?.Split(';')[0].Trim();
+                    acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase);
+
+                    if (acceptsAnything)
+                    {
+                        break;
+                    }
                 }
 
                 if (acceptsAnything)
@@ -226,7 +224,7 @@ namespace Jellyfin.Server.SocketSharp
                     {
                         return defaultContentType;
                     }
-                    else if (serverDefaultContentType != null)
+                    else
                     {
                         return serverDefaultContentType;
                     }
@@ -269,11 +267,11 @@ namespace Jellyfin.Server.SocketSharp
 
         private static string GetQueryStringContentType(IRequest httpReq)
         {
-            var format = httpReq.QueryString["format"];
+            ReadOnlySpan<char> format = httpReq.QueryString["format"];
             if (format == null)
             {
                 const int formatMaxLength = 4;
-                var pi = httpReq.PathInfo;
+                ReadOnlySpan<char> pi = httpReq.PathInfo;
                 if (pi == null || pi.Length <= formatMaxLength)
                 {
                     return null;
@@ -281,7 +279,7 @@ namespace Jellyfin.Server.SocketSharp
 
                 if (pi[0] == '/')
                 {
-                    pi = pi.Substring(1);
+                    pi = pi.Slice(1);
                 }
 
                 format = LeftPart(pi, '/');
@@ -315,6 +313,17 @@ namespace Jellyfin.Server.SocketSharp
             return pos == -1 ? strVal : strVal.Substring(0, pos);
         }
 
+        public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
+        {
+            if (strVal == null)
+            {
+                return null;
+            }
+
+            var pos = strVal.IndexOf(needle);
+            return pos == -1 ? strVal : strVal.Slice(0, pos);
+        }
+
         public static string HandlerFactoryPath;
 
         private string pathInfo;
@@ -326,7 +335,7 @@ namespace Jellyfin.Server.SocketSharp
                 {
                     var mode = HandlerFactoryPath;
 
-                    var pos = request.RawUrl.IndexOf("?", StringComparison.Ordinal);
+                    var pos = request.RawUrl.IndexOf('?', StringComparison.Ordinal);
                     if (pos != -1)
                     {
                         var path = request.RawUrl.Substring(0, pos);
@@ -343,6 +352,7 @@ namespace Jellyfin.Server.SocketSharp
                     this.pathInfo = System.Net.WebUtility.UrlDecode(pathInfo);
                     this.pathInfo = NormalizePathInfo(pathInfo, mode);
                 }
+
                 return this.pathInfo;
             }
         }
@@ -444,7 +454,7 @@ namespace Jellyfin.Server.SocketSharp
 
         public string ContentType => request.ContentType;
 
-        public Encoding contentEncoding;
+        private Encoding contentEncoding;
         public Encoding ContentEncoding
         {
             get => contentEncoding ?? request.ContentEncoding;
@@ -502,16 +512,20 @@ namespace Jellyfin.Server.SocketSharp
                         i++;
                     }
                 }
+
                 return httpFiles;
             }
         }
 
         public static string NormalizePathInfo(string pathInfo, string handlerPath)
         {
-            var trimmed = pathInfo.TrimStart('/');
-            if (handlerPath != null && trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+            if (handlerPath != null)
             {
-                return trimmed.Substring(handlerPath.Length);
+                var trimmed = pathInfo.TrimStart('/');
+                if (trimmed.StartsWith(handlerPath, StringComparison.OrdinalIgnoreCase))
+                {
+                    return trimmed.Substring(handlerPath.Length);
+                }
             }
 
             return pathInfo;

+ 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;

+ 23 - 9
MediaBrowser.Api/BaseApiService.cs

@@ -9,6 +9,7 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Services;
+using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api
@@ -118,8 +119,7 @@ namespace MediaBrowser.Api
         {
             var options = new DtoOptions();
 
-            var hasFields = request as IHasItemFields;
-            if (hasFields != null)
+            if (request is IHasItemFields hasFields)
             {
                 options.Fields = hasFields.GetItemFields();
             }
@@ -133,9 +133,11 @@ namespace MediaBrowser.Api
                     client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 ||
                     client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    var list = options.Fields.ToList();
-                    list.Add(Model.Querying.ItemFields.RecursiveItemCount);
-                    options.Fields = list.ToArray();
+                    int oldLen = options.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    options.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = Model.Querying.ItemFields.RecursiveItemCount;
+                    options.Fields = arr;
                 }
 
                 if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
@@ -146,9 +148,12 @@ namespace MediaBrowser.Api
                    client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 ||
                    client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    var list = options.Fields.ToList();
-                    list.Add(Model.Querying.ItemFields.ChildCount);
-                    options.Fields = list.ToArray();
+
+                    int oldLen = options.Fields.Length;
+                    var arr = new ItemFields[oldLen + 1];
+                    options.Fields.CopyTo(arr, 0);
+                    arr[oldLen] = Model.Querying.ItemFields.ChildCount;
+                    options.Fields = arr;
                 }
             }
 
@@ -167,7 +172,16 @@ namespace MediaBrowser.Api
 
                 if (!string.IsNullOrWhiteSpace(hasDtoOptions.EnableImageTypes))
                 {
-                    options.ImageTypes = (hasDtoOptions.EnableImageTypes ?? string.Empty).Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToArray();
+                    if (string.IsNullOrEmpty(hasDtoOptions.EnableImageTypes))
+                    {
+                        options.ImageTypes = Array.Empty<ImageType>();
+                    }
+                    else
+                    {
+                        options.ImageTypes = hasDtoOptions.EnableImageTypes.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)
+                                                                            .Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true))
+                                                                            .ToArray();
+                    }
                 }
             }
 

+ 2 - 8
MediaBrowser.Api/EnvironmentService.cs

@@ -173,14 +173,8 @@ namespace MediaBrowser.Api
             _fileSystem.DeleteFile(file);
         }
 
-        public object Get(GetDefaultDirectoryBrowser request)
-        {
-            var result = new DefaultDirectoryBrowserInfo();
-
-            result.Path = _fileSystem.DefaultDirectory;
-
-            return ToOptimizedResult(result);
-        }
+        public object Get(GetDefaultDirectoryBrowser request) =>
+            ToOptimizedResult(new DefaultDirectoryBrowserInfo {Path = null});
 
         /// <summary>
         /// Gets the specified request.

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

@@ -8,7 +8,6 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
@@ -71,7 +70,6 @@ namespace MediaBrowser.Api.Playback
         protected IMediaSourceManager MediaSourceManager { get; private set; }
         protected IJsonSerializer JsonSerializer { get; private set; }
 
-        public static IHttpClient HttpClient;
         protected IAuthorizationContext AuthorizationContext { get; private set; }
 
         protected EncodingHelper EncodingHelper { get; set; }

+ 4 - 1
MediaBrowser.Api/Playback/Progressive/AudioService.cs

@@ -1,4 +1,5 @@
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -33,6 +34,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     public class AudioService : BaseProgressiveStreamingService
     {
         public AudioService(
+            IHttpClient httpClient,
             IServerConfigurationManager serverConfig,
             IUserManager userManager,
             ILibraryManager libraryManager,
@@ -46,7 +48,8 @@ namespace MediaBrowser.Api.Playback.Progressive
             IJsonSerializer jsonSerializer,
             IAuthorizationContext authorizationContext,
             IEnvironmentInfo environmentInfo)
-                : base(serverConfig,
+                : base(httpClient,
+                    serverConfig,
                     userManager,
                     libraryManager,
                     isoManager,

+ 3 - 0
MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs

@@ -26,8 +26,10 @@ namespace MediaBrowser.Api.Playback.Progressive
     public abstract class BaseProgressiveStreamingService : BaseStreamingService
     {
         protected readonly IEnvironmentInfo EnvironmentInfo;
+        protected IHttpClient HttpClient { get; private set; }
 
         public BaseProgressiveStreamingService(
+            IHttpClient httpClient,
             IServerConfigurationManager serverConfig,
             IUserManager userManager,
             ILibraryManager libraryManager,
@@ -55,6 +57,7 @@ namespace MediaBrowser.Api.Playback.Progressive
                 authorizationContext)
         {
             EnvironmentInfo = environmentInfo;
+            HttpClient = httpClient;
         }
 
         /// <summary>

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

@@ -1,4 +1,5 @@
 using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
@@ -69,6 +70,7 @@ namespace MediaBrowser.Api.Playback.Progressive
     public class VideoService : BaseProgressiveStreamingService
     {
         public VideoService(
+            IHttpClient httpClient,
             IServerConfigurationManager serverConfig,
             IUserManager userManager,
             ILibraryManager libraryManager,
@@ -82,7 +84,8 @@ namespace MediaBrowser.Api.Playback.Progressive
             IJsonSerializer jsonSerializer,
             IAuthorizationContext authorizationContext,
             IEnvironmentInfo environmentInfo)
-            : base(serverConfig,
+            : base(httpClient,
+                serverConfig,
                 userManager,
                 libraryManager,
                 isoManager,

+ 5 - 1
MediaBrowser.Api/Playback/UniversalAudioService.cs

@@ -77,6 +77,7 @@ namespace MediaBrowser.Api.Playback
     public class UniversalAudioService : BaseApiService
     {
         public UniversalAudioService(
+            IHttpClient httpClient,
             IServerConfigurationManager serverConfigurationManager,
             IUserManager userManager,
             ILibraryManager libraryManager,
@@ -95,6 +96,7 @@ namespace MediaBrowser.Api.Playback
             IEnvironmentInfo environmentInfo,
             ILoggerFactory loggerFactory)
         {
+            HttpClient = httpClient;
             ServerConfigurationManager = serverConfigurationManager;
             UserManager = userManager;
             LibraryManager = libraryManager;
@@ -115,6 +117,7 @@ namespace MediaBrowser.Api.Playback
             _logger = loggerFactory.CreateLogger(nameof(UniversalAudioService));
         }
 
+        protected IHttpClient HttpClient { get; private set; }
         protected IServerConfigurationManager ServerConfigurationManager { get; private set; }
         protected IUserManager UserManager { get; private set; }
         protected ILibraryManager LibraryManager { get; private set; }
@@ -323,7 +326,8 @@ namespace MediaBrowser.Api.Playback
             }
             else
             {
-                var service = new AudioService(ServerConfigurationManager,
+                var service = new AudioService(HttpClient,
+                    ServerConfigurationManager,
                     UserManager,
                     LibraryManager,
                     IsoManager,

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików