Browse Source

Merge remote-tracking branch 'origin/master' into media-attachments-clean

Andrew Mahone 5 years ago
parent
commit
1600d5b53f
100 changed files with 1020 additions and 781 deletions
  1. 1 1
      .ci/azure-pipelines.yml
  2. 59 8
      .copr/Makefile
  3. 1 0
      .github/stale.yml
  4. 3 0
      .gitignore
  5. 2 2
      .vscode/launch.json
  6. 14 8
      Dockerfile
  7. 5 5
      Dockerfile.arm
  8. 5 5
      Dockerfile.arm64
  9. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  10. 1 6
      Emby.Drawing/Emby.Drawing.csproj
  11. 2 1
      Emby.Naming/Emby.Naming.csproj
  12. 1 1
      Emby.Notifications/Emby.Notifications.csproj
  13. 1 1
      Emby.Photos/Emby.Photos.csproj
  14. 20 5
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  15. 2 2
      Emby.Server.Implementations/ApplicationHost.cs
  16. 2 2
      Emby.Server.Implementations/Channels/ChannelManager.cs
  17. 5 5
      Emby.Server.Implementations/Collections/CollectionManager.cs
  18. 5 5
      Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs
  19. 52 64
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  20. 24 21
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  21. 5 10
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  22. 1 1
      Emby.Server.Implementations/Data/SqliteUserRepository.cs
  23. 6 10
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  24. 11 6
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  25. 1 1
      Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs
  26. 5 0
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  27. 2 2
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  28. 0 1
      Emby.Server.Implementations/Library/InvalidAuthProvider.cs
  29. 8 8
      Emby.Server.Implementations/Library/LibraryManager.cs
  30. 7 6
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  31. 3 3
      Emby.Server.Implementations/Library/UserManager.cs
  32. 18 10
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  33. 11 9
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  34. 20 15
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  35. 6 6
      Emby.Server.Implementations/Localization/Core/fr.json
  36. 19 19
      Emby.Server.Implementations/Localization/Core/hu.json
  37. 24 24
      Emby.Server.Implementations/Localization/Core/tr.json
  38. 11 5
      Emby.Server.Implementations/Networking/NetworkManager.cs
  39. 13 16
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  40. 8 7
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  41. 5 31
      Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
  42. 1 1
      Emby.Server.Implementations/ServerApplicationPaths.cs
  43. 45 43
      Emby.Server.Implementations/Updates/InstallationManager.cs
  44. 1 1
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  45. 4 5
      Jellyfin.Server/Jellyfin.Server.csproj
  46. 1 1
      Jellyfin.Server/Program.cs
  47. 2 1
      MediaBrowser.Api/IHasItemFields.cs
  48. 14 17
      MediaBrowser.Api/ItemLookupService.cs
  49. 1 1
      MediaBrowser.Api/ItemRefreshService.cs
  50. 9 7
      MediaBrowser.Api/ItemUpdateService.cs
  51. 4 3
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  52. 1 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  53. 7 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  54. 70 40
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  55. 1 2
      MediaBrowser.Api/Subtitles/SubtitleService.cs
  56. 5 0
      MediaBrowser.Api/UserLibrary/PersonsService.cs
  57. 1 1
      MediaBrowser.Api/UserLibrary/UserLibraryService.cs
  58. 14 17
      MediaBrowser.Common/Cryptography/PasswordHash.cs
  59. 0 48
      MediaBrowser.Common/Extensions/CollectionExtensions.cs
  60. 26 0
      MediaBrowser.Common/Extensions/CopyToExtensions.cs
  61. 94 0
      MediaBrowser.Common/Hex.cs
  62. 0 24
      MediaBrowser.Common/HexHelper.cs
  63. 2 2
      MediaBrowser.Common/MediaBrowser.Common.csproj
  64. 1 1
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  65. 2 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  66. 1 1
      MediaBrowser.Controller/Entities/CollectionFolder.cs
  67. 1 1
      MediaBrowser.Controller/Entities/Folder.cs
  68. 1 1
      MediaBrowser.Controller/Entities/User.cs
  69. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  70. 2 1
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  71. 2 7
      MediaBrowser.Controller/Providers/DirectoryService.cs
  72. 6 4
      MediaBrowser.Controller/Subtitles/ISubtitleManager.cs
  73. 1 1
      MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
  74. 2 2
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  75. 3 3
      MediaBrowser.Model/IO/StreamDefaults.cs
  76. 1 1
      MediaBrowser.Model/MediaBrowser.Model.csproj
  77. 1 2
      MediaBrowser.Providers/Books/AudioBookMetadataService.cs
  78. 11 4
      MediaBrowser.Providers/Books/BookMetadataService.cs
  79. 25 12
      MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
  80. 10 3
      MediaBrowser.Providers/Channels/ChannelMetadataService.cs
  81. 10 3
      MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
  82. 12 4
      MediaBrowser.Providers/Folders/FolderMetadataService.cs
  83. 10 3
      MediaBrowser.Providers/Folders/UserViewMetadataService.cs
  84. 10 3
      MediaBrowser.Providers/Genres/GenreMetadataService.cs
  85. 10 3
      MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs
  86. 1 1
      MediaBrowser.Providers/Manager/ItemImageProvider.cs
  87. 2 4
      MediaBrowser.Providers/Manager/MetadataService.cs
  88. 2 2
      MediaBrowser.Providers/Manager/ProviderManager.cs
  89. 4 4
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  90. 10 7
      MediaBrowser.Providers/Movies/MovieExternalIds.cs
  91. 12 36
      MediaBrowser.Providers/Movies/MovieMetadataService.cs
  92. 49 0
      MediaBrowser.Providers/Movies/TrailerMetadataService.cs
  93. 8 14
      MediaBrowser.Providers/Music/AlbumMetadataService.cs
  94. 20 11
      MediaBrowser.Providers/Music/ArtistMetadataService.cs
  95. 12 8
      MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs
  96. 23 22
      MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs
  97. 11 8
      MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs
  98. 29 35
      MediaBrowser.Providers/Music/AudioDbArtistProvider.cs
  99. 20 15
      MediaBrowser.Providers/Music/AudioDbExternalIds.cs
  100. 1 2
      MediaBrowser.Providers/Music/AudioMetadataService.cs

+ 1 - 1
.ci/azure-pipelines.yml

@@ -245,7 +245,7 @@ jobs:
       inputs:
         targetType: 'filePath' # Optional. Options: filePath, inline
         filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
-        arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
+        arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
         #script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline
         errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
         #failOnStderr: false # Optional

+ 59 - 8
.copr/Makefile

@@ -1,8 +1,59 @@
-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)"
+VERSION := $(shell sed -ne '/^Version:/s/.*  *//p'                      \
+                   deployment/fedora-package-x64/pkg-src/jellyfin.spec)
+
+deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz:
+	curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
+         https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \
+	|| curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
+         https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \
+
+srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz
+	cd deployment/fedora-package-x64;                                             \
+    SOURCE_DIR=../..                                                              \
+    WORKDIR="$${PWD}";                                                            \
+    package_temporary_dir="$${WORKDIR}/pkg-dist-tmp";                             \
+    pkg_src_dir="$${WORKDIR}/pkg-src";                                            \
+    GNU_TAR=1;                                                                    \
+    tar                                                                           \
+    --transform "s,^\.,jellyfin-$(VERSION),"                                      \
+    --exclude='.git*'                                                             \
+    --exclude='**/.git'                                                           \
+    --exclude='**/.hg'                                                            \
+    --exclude='**/.vs'                                                            \
+    --exclude='**/.vscode'                                                        \
+    --exclude='deployment'                                                        \
+    --exclude='**/bin'                                                            \
+    --exclude='**/obj'                                                            \
+    --exclude='**/.nuget'                                                         \
+    --exclude='*.deb'                                                             \
+    --exclude='*.rpm'                                                             \
+    -czf "pkg-src/jellyfin-$(VERSION).tar.gz"                                     \
+    -C $${SOURCE_DIR} ./ || GNU_TAR=0;                                            \
+    if [ $${GNU_TAR} -eq 0 ]; then                                                \
+        package_temporary_dir="$$(mktemp -d)";                                    \
+        mkdir -p "$${package_temporary_dir}/jellyfin";                            \
+        tar                                                                       \
+        --exclude='.git*'                                                         \
+        --exclude='**/.git'                                                       \
+        --exclude='**/.hg'                                                        \
+        --exclude='**/.vs'                                                        \
+        --exclude='**/.vscode'                                                    \
+        --exclude='deployment'                                                    \
+        --exclude='**/bin'                                                        \
+        --exclude='**/obj'                                                        \
+        --exclude='**/.nuget'                                                     \
+        --exclude='*.deb'                                                         \
+        --exclude='*.rpm'                                                         \
+        -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"      \
+        -C $${SOURCE_DIR} ./;                                                     \
+        mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)";                 \
+        tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"  \
+            -C "$${package_temporary_dir}/jellyfin-$(VERSION);                    \
+        rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz";    \
+        tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz"      \
+            -C "$${package_temporary_dir}" "jellyfin-$(VERSION);                  \
+        rm -rf $${package_temporary_dir};                                         \
+	fi;                                                                           \
+	rpmbuild -bs pkg-src/jellyfin.spec                                            \
+	         --define "_sourcedir $$PWD/pkg-src/"                                 \
+	         --define "_srcrpmdir $(outdir)"

+ 1 - 0
.github/stale.yml

@@ -11,6 +11,7 @@ exemptLabels:
   - future
   - feature
   - enhancement
+  - confirmed
 # Label to use when marking an issue as stale
 staleLabel: stale
 # Comment to post when marking an issue as stale. Set to `false` to disable

+ 3 - 0
.gitignore

@@ -268,3 +268,6 @@ doc/
 # Deployment artifacts
 dist
 *.exe
+
+# BenchmarkDotNet artifacts
+BenchmarkDotNet.Artifacts

+ 2 - 2
.vscode/launch.json

@@ -10,7 +10,7 @@
             "request": "launch",
             "preLaunchTask": "build",
             // If you have changed target frameworks, make sure to update the program path.
-            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin.dll",
+            "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.0/jellyfin.dll",
             "args": [],
             "cwd": "${workspaceFolder}/Jellyfin.Server",
             // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
@@ -25,4 +25,4 @@
             "processId": "${command:pickProcess}"
         }
     ,]
-}
+}

+ 14 - 8
Dockerfile

@@ -1,8 +1,8 @@
-ARG DOTNET_VERSION=2.2
+ARG DOTNET_VERSION=3.0
 ARG FFMPEG_VERSION=latest
 
 FROM node:alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=v10.5.0
+ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
@@ -10,33 +10,39 @@ RUN apk add curl \
  && yarn build \
  && mv dist /dist
 
-FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
+FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder
 WORKDIR /repo
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"
 
 FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg
+FROM debian:buster-slim
 
-FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}
-COPY --from=ffmpeg / /
+COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 # Install dependencies:
 #   libfontconfig1: needed for Skia
+#   libgomp1: needed for ffmpeg
+#   libva-drm2: needed for ffmpeg
 #   mesa-va-drivers: needed for VAAPI
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
-   libfontconfig1 mesa-va-drivers \
+   libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl \
  && apt-get clean autoclean \
  && apt-get autoremove \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media
+ && chmod 777 /cache /config /media \
+ && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \
+ && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin
+
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
 
 EXPOSE 8096
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ENTRYPOINT ./jellyfin/jellyfin \
     --datadir /config \
     --cachedir /cache \
     --ffmpeg /usr/local/bin/ffmpeg

+ 5 - 5
Dockerfile.arm

@@ -4,7 +4,7 @@ ARG DOTNET_VERSION=3.0
 
 
 FROM node:alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=v10.5.0
+ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
@@ -17,8 +17,6 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 COPY . .
 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
@@ -26,7 +24,7 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
+FROM debian:stretch-slim-arm32v7
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
@@ -36,9 +34,11 @@ RUN apt-get update \
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+
 EXPOSE 8096
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ENTRYPOINT ./jellyfin/jellyfin \
     --datadir /config \
     --cachedir /cache \
     --ffmpeg /usr/bin/ffmpeg

+ 5 - 5
Dockerfile.arm64

@@ -4,7 +4,7 @@ ARG DOTNET_VERSION=3.0
 
 
 FROM node:alpine as web-builder
-ARG JELLYFIN_WEB_VERSION=v10.5.0
+ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
@@ -17,8 +17,6 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 COPY . .
 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
@@ -26,7 +24,7 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin"
 
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
+FROM debian:stretch-slim-arm64v8
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
 RUN apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
@@ -36,9 +34,11 @@ RUN apt-get update \
 COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 
+ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
+
 EXPOSE 8096
 VOLUME /cache /config /media
-ENTRYPOINT dotnet /jellyfin/jellyfin.dll \
+ENTRYPOINT ./jellyfin/jellyfin \
     --datadir /config \
     --cachedir /cache \
     --ffmpeg /usr/bin/ffmpeg

+ 1 - 1
Emby.Dlna/Emby.Dlna.csproj

@@ -12,7 +12,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 6
Emby.Drawing/Emby.Drawing.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -17,9 +17,4 @@
     <Compile Include="..\SharedVersion.cs" />
   </ItemGroup>
 
-  <PropertyGroup>
-      <!-- We need at least C# 7.1 for the "default literal" feature-->
-    <LangVersion>latest</LangVersion>
-  </PropertyGroup>
-
 </Project>

+ 2 - 1
Emby.Naming/Emby.Naming.csproj

@@ -24,8 +24,9 @@
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
   </ItemGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 1 - 1
Emby.Notifications/Emby.Notifications.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 1 - 1
Emby.Photos/Emby.Photos.csproj

@@ -14,7 +14,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 20 - 5
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.AppBase
         /// <summary>
         /// The _configuration sync lock.
         /// </summary>
-        private object _configurationSyncLock = new object();
+        private readonly object _configurationSyncLock = new object();
 
         /// <summary>
         /// The _configuration.
@@ -98,16 +98,31 @@ namespace Emby.Server.Implementations.AppBase
         public IApplicationPaths CommonApplicationPaths { get; private set; }
 
         /// <summary>
-        /// Gets the system configuration
+        /// Gets the system configuration.
         /// </summary>
         /// <value>The configuration.</value>
         public BaseApplicationConfiguration CommonConfiguration
         {
             get
             {
-                // Lazy load
-                LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer));
-                return _configuration;
+                if (_configurationLoaded)
+                {
+                    return _configuration;
+                }
+
+                lock (_configurationSyncLock)
+                {
+                    if (_configurationLoaded)
+                    {
+                        return _configuration;
+                    }
+
+                    _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
+
+                    _configurationLoaded = true;
+
+                    return _configuration;
+                }
             }
             protected set
             {

+ 2 - 2
Emby.Server.Implementations/ApplicationHost.cs

@@ -364,7 +364,7 @@ namespace Emby.Server.Implementations
         {
             _configuration = configuration;
 
-            XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
+            XmlSerializer = new MyXmlSerializer();
 
             NetworkManager = networkManager;
             networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
@@ -912,7 +912,7 @@ namespace Emby.Server.Implementations
 
             _displayPreferencesRepository.Initialize();
 
-            var userDataRepo = new SqliteUserDataRepository(LoggerFactory, ApplicationPaths);
+            var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths);
 
             SetStaticProperties();
 

+ 2 - 2
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.Channels
                 _libraryManager.CreateItem(item, null);
             }
 
-            await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+            await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem))
             {
                 ForceSave = !isNew && forceUpdate
             }, cancellationToken);
@@ -1156,7 +1156,7 @@ namespace Emby.Server.Implementations.Channels
 
             if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime))
             {
-                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal);
+                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
             }
 
             return item;

+ 5 - 5
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Collections
 
                 if (options.ItemIdList.Length > 0)
                 {
-                    AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+                    AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         // The initial adding of items is going to create a local metadata file
                         // This will cause internet metadata to be skipped as a result
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Collections
                 }
                 else
                 {
-                    _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.High);
+                    _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
                 }
 
                 CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs
@@ -178,12 +178,12 @@ namespace Emby.Server.Implementations.Collections
 
         public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
         {
-            AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)));
+            AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
         }
 
         public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
         {
-            AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)));
+            AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
         }
 
         private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
@@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.Collections
             }
 
             collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
-            _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+            _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
             {
                 ForceSave = true
             }, RefreshPriority.High);

+ 5 - 5
Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs

@@ -110,8 +110,8 @@ namespace Emby.Server.Implementations.Data
 
             using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
             {
-                statement.TryBind("@id", displayPreferences.Id.ToGuidBlob());
-                statement.TryBind("@userId", userId.ToGuidBlob());
+                statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
+                statement.TryBind("@userId", userId.ToByteArray());
                 statement.TryBind("@client", client);
                 statement.TryBind("@data", serialized);
 
@@ -170,8 +170,8 @@ namespace Emby.Server.Implementations.Data
             {
                 using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
                 {
-                    statement.TryBind("@id", guidId.ToGuidBlob());
-                    statement.TryBind("@userId", userId.ToGuidBlob());
+                    statement.TryBind("@id", guidId.ToByteArray());
+                    statement.TryBind("@userId", userId.ToByteArray());
                     statement.TryBind("@client", client);
 
                     foreach (var row in statement.ExecuteQuery())
@@ -200,7 +200,7 @@ namespace Emby.Server.Implementations.Data
             using (var connection = GetConnection(true))
             using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
             {
-                statement.TryBind("@userId", userId.ToGuidBlob());
+                statement.TryBind("@userId", userId.ToByteArray());
 
                 foreach (var row in statement.ExecuteQuery())
                 {

+ 52 - 64
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -9,6 +9,47 @@ namespace Emby.Server.Implementations.Data
 {
     public static class SqliteExtensions
     {
+        private const string DatetimeFormatUtc = "yyyy-MM-dd HH:mm:ss.FFFFFFFK";
+        private const string DatetimeFormatLocal = "yyyy-MM-dd HH:mm:ss.FFFFFFF";
+
+        /// <summary>
+        /// An array of ISO-8601 DateTime formats that we support parsing.
+        /// </summary>
+        private static readonly string[] _datetimeFormats = new string[]
+        {
+            "THHmmssK",
+            "THHmmK",
+            "HH:mm:ss.FFFFFFFK",
+            "HH:mm:ssK",
+            "HH:mmK",
+            DatetimeFormatUtc,
+            "yyyy-MM-dd HH:mm:ssK",
+            "yyyy-MM-dd HH:mmK",
+            "yyyy-MM-ddTHH:mm:ss.FFFFFFFK",
+            "yyyy-MM-ddTHH:mmK",
+            "yyyy-MM-ddTHH:mm:ssK",
+            "yyyyMMddHHmmssK",
+            "yyyyMMddHHmmK",
+            "yyyyMMddTHHmmssFFFFFFFK",
+            "THHmmss",
+            "THHmm",
+            "HH:mm:ss.FFFFFFF",
+            "HH:mm:ss",
+            "HH:mm",
+            DatetimeFormatLocal,
+            "yyyy-MM-dd HH:mm:ss",
+            "yyyy-MM-dd HH:mm",
+            "yyyy-MM-ddTHH:mm:ss.FFFFFFF",
+            "yyyy-MM-ddTHH:mm",
+            "yyyy-MM-ddTHH:mm:ss",
+            "yyyyMMddHHmmss",
+            "yyyyMMddHHmm",
+            "yyyyMMddTHHmmssFFFFFFF",
+            "yyyy-MM-dd",
+            "yyyyMMdd",
+            "yy-MM-dd"
+        };
+
         public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
         {
             if (queries == null)
@@ -22,16 +63,6 @@ namespace Emby.Server.Implementations.Data
             });
         }
 
-        public static byte[] ToGuidBlob(this string str)
-        {
-            return ToGuidBlob(new Guid(str));
-        }
-
-        public static byte[] ToGuidBlob(this Guid guid)
-        {
-            return guid.ToByteArray();
-        }
-
         public static Guid ReadGuidFromBlob(this IResultSetValue result)
         {
             return new Guid(result.ToBlob());
@@ -50,58 +81,16 @@ namespace Emby.Server.Implementations.Data
                     CultureInfo.InvariantCulture);
         }
 
-        private static string GetDateTimeKindFormat(
-           DateTimeKind kind)
-        {
-            return (kind == DateTimeKind.Utc) ? _datetimeFormatUtc : _datetimeFormatLocal;
-        }
-
-        /// <summary>
-        /// An array of ISO-8601 DateTime formats that we support parsing.
-        /// </summary>
-        private static string[] _datetimeFormats = new string[] {
-      "THHmmssK",
-      "THHmmK",
-      "HH:mm:ss.FFFFFFFK",
-      "HH:mm:ssK",
-      "HH:mmK",
-      "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */
-      "yyyy-MM-dd HH:mm:ssK",
-      "yyyy-MM-dd HH:mmK",
-      "yyyy-MM-ddTHH:mm:ss.FFFFFFFK",
-      "yyyy-MM-ddTHH:mmK",
-      "yyyy-MM-ddTHH:mm:ssK",
-      "yyyyMMddHHmmssK",
-      "yyyyMMddHHmmK",
-      "yyyyMMddTHHmmssFFFFFFFK",
-      "THHmmss",
-      "THHmm",
-      "HH:mm:ss.FFFFFFF",
-      "HH:mm:ss",
-      "HH:mm",
-      "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */
-      "yyyy-MM-dd HH:mm:ss",
-      "yyyy-MM-dd HH:mm",
-      "yyyy-MM-ddTHH:mm:ss.FFFFFFF",
-      "yyyy-MM-ddTHH:mm",
-      "yyyy-MM-ddTHH:mm:ss",
-      "yyyyMMddHHmmss",
-      "yyyyMMddHHmm",
-      "yyyyMMddTHHmmssFFFFFFF",
-      "yyyy-MM-dd",
-      "yyyyMMdd",
-      "yy-MM-dd"
-    };
-
-        private static string _datetimeFormatUtc = _datetimeFormats[5];
-        private static string _datetimeFormatLocal = _datetimeFormats[19];
+        private static string GetDateTimeKindFormat(DateTimeKind kind)
+            => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
 
         public static DateTime ReadDateTime(this IResultSetValue result)
         {
             var dateText = result.ToString();
 
             return DateTime.ParseExact(
-                dateText, _datetimeFormats,
+                dateText,
+                _datetimeFormats,
                 DateTimeFormatInfo.InvariantInfo,
                 DateTimeStyles.None).ToUniversalTime();
         }
@@ -139,7 +128,10 @@ namespace Emby.Server.Implementations.Data
 
         public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
         {
-            var commandText = string.Format("attach @path as {0};", alias);
+            var commandText = string.Format(
+                CultureInfo.InvariantCulture,
+                "attach @path as {0};",
+                alias);
 
             using (var statement = db.PrepareStatement(commandText))
             {
@@ -186,10 +178,7 @@ namespace Emby.Server.Implementations.Data
         private static void CheckName(string name)
         {
 #if DEBUG
-            //if (!name.IndexOf("@", StringComparison.OrdinalIgnoreCase) != 0)
-            {
-                throw new Exception("Invalid param name: " + name);
-            }
+            throw new ArgumentException("Invalid param name: " + name, nameof(name));
 #endif
         }
 
@@ -264,7 +253,7 @@ namespace Emby.Server.Implementations.Data
         {
             if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
             {
-                bindParam.Bind(value.ToGuidBlob());
+                bindParam.Bind(value.ToByteArray());
             }
             else
             {
@@ -392,8 +381,7 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(
-            this IStatement This)
+        public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This)
         {
             while (This.MoveNext())
             {

+ 24 - 21
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -563,7 +563,7 @@ namespace Emby.Server.Implementations.Data
                 {
                     using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
                     {
-                        saveImagesStatement.TryBind("@Id", item.Id.ToGuidBlob());
+                        saveImagesStatement.TryBind("@Id", item.Id.ToByteArray());
                         saveImagesStatement.TryBind("@Images", SerializeImages(item));
 
                         saveImagesStatement.MoveNext();
@@ -2003,7 +2003,7 @@ namespace Emby.Server.Implementations.Data
                 throw new ArgumentNullException(nameof(chapters));
             }
 
-            var idBlob = id.ToGuidBlob();
+            var idBlob = id.ToByteArray();
 
             using (var connection = GetConnection())
             {
@@ -3782,7 +3782,7 @@ namespace Emby.Server.Implementations.Data
 
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, personId.ToGuidBlob());
+                        statement.TryBind(paramName, personId.ToByteArray());
                     }
                     index++;
                 }
@@ -3993,7 +3993,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, artistId.ToGuidBlob());
+                        statement.TryBind(paramName, artistId.ToByteArray());
                     }
                     index++;
                 }
@@ -4012,7 +4012,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, artistId.ToGuidBlob());
+                        statement.TryBind(paramName, artistId.ToByteArray());
                     }
                     index++;
                 }
@@ -4031,7 +4031,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, artistId.ToGuidBlob());
+                        statement.TryBind(paramName, artistId.ToByteArray());
                     }
                     index++;
                 }
@@ -4050,7 +4050,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, albumId.ToGuidBlob());
+                        statement.TryBind(paramName, albumId.ToByteArray());
                     }
                     index++;
                 }
@@ -4069,7 +4069,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, artistId.ToGuidBlob());
+                        statement.TryBind(paramName, artistId.ToByteArray());
                     }
                     index++;
                 }
@@ -4088,7 +4088,7 @@ namespace Emby.Server.Implementations.Data
                     clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, genreId.ToGuidBlob());
+                        statement.TryBind(paramName, genreId.ToByteArray());
                     }
                     index++;
                 }
@@ -4159,7 +4159,7 @@ namespace Emby.Server.Implementations.Data
 
                     if (statement != null)
                     {
-                        statement.TryBind(paramName, studioId.ToGuidBlob());
+                        statement.TryBind(paramName, studioId.ToByteArray());
                     }
                     index++;
                 }
@@ -4935,7 +4935,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             {
                 connection.RunInTransaction(db =>
                 {
-                    var idBlob = id.ToGuidBlob();
+                    var idBlob = id.ToByteArray();
 
                     // Delete people
                     ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
@@ -5054,7 +5054,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 whereClauses.Add("ItemId=@ItemId");
                 if (statement != null)
                 {
-                    statement.TryBind("@ItemId", query.ItemId.ToGuidBlob());
+                    statement.TryBind("@ItemId", query.ItemId.ToByteArray());
                 }
             }
             if (!query.AppearsInItemId.Equals(Guid.Empty))
@@ -5062,7 +5062,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
                 if (statement != null)
                 {
-                    statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidBlob());
+                    statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
                 }
             }
             var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@@ -5131,7 +5131,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             CheckDisposed();
 
-            var itemIdBlob = itemId.ToGuidBlob();
+            var itemIdBlob = itemId.ToByteArray();
 
             // First delete
             deleteAncestorsStatement.Reset();
@@ -5165,7 +5165,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                     var ancestorId = ancestorIds[i];
 
-                    statement.TryBind("@AncestorId" + index, ancestorId.ToGuidBlob());
+                    statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray());
                     statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
                 }
 
@@ -5630,7 +5630,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
             CheckDisposed();
 
-            var guidBlob = itemId.ToGuidBlob();
+            var guidBlob = itemId.ToByteArray();
 
             // First delete
             db.Execute("delete from ItemValues where ItemId=@Id", guidBlob);
@@ -5654,10 +5654,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
                 {
                     if (isSubsequentRow)
                     {
-                        insertText.Append(",");
+                        insertText.Append(',');
                     }
 
-                    insertText.AppendFormat("(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})", i.ToString(CultureInfo.InvariantCulture));
+                    insertText.AppendFormat(
+                        CultureInfo.InvariantCulture,
+                        "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})",
+                        i);
                     isSubsequentRow = true;
                 }
 
@@ -5710,7 +5713,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             {
                 connection.RunInTransaction(db =>
                 {
-                    var itemIdBlob = itemId.ToGuidBlob();
+                    var itemIdBlob = itemId.ToByteArray();
 
                     // First delete chapters
                     db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
@@ -5829,7 +5832,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
                 using (var statement = PrepareStatement(connection, cmdText))
                 {
-                    statement.TryBind("@ItemId", query.ItemId.ToGuidBlob());
+                    statement.TryBind("@ItemId", query.ItemId.ToByteArray());
 
                     if (query.Type.HasValue)
                     {
@@ -5871,7 +5874,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             {
                 connection.RunInTransaction(db =>
                 {
-                    var itemIdBlob = id.ToGuidBlob();
+                    var itemIdBlob = id.ToByteArray();
 
                     // First delete chapters
                     db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);

+ 5 - 10
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using System.Threading;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -15,23 +14,19 @@ namespace Emby.Server.Implementations.Data
     public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
     {
         public SqliteUserDataRepository(
-            ILoggerFactory loggerFactory,
+            ILogger<SqliteUserDataRepository> logger,
             IApplicationPaths appPaths)
-            : base(loggerFactory.CreateLogger(nameof(SqliteUserDataRepository)))
+            : base(logger)
         {
             DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
         }
 
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
+        /// <inheritdoc />
         public string Name => "SQLite";
 
         /// <summary>
-        /// Opens the connection to the database
+        /// Opens the connection to the database.
         /// </summary>
-        /// <returns>Task.</returns>
         public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
         {
             WriteLock.Dispose();
@@ -97,7 +92,7 @@ namespace Emby.Server.Implementations.Data
                         continue;
                     }
 
-                    statement.TryBind("@UserId", user.Id.ToGuidBlob());
+                    statement.TryBind("@UserId", user.Id.ToByteArray());
                     statement.TryBind("@InternalUserId", user.InternalId);
 
                     statement.MoveNext();

+ 1 - 1
Emby.Server.Implementations/Data/SqliteUserRepository.cs

@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.Data
                 {
                     using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
                     {
-                        statement.TryBind("@guid", user.Id.ToGuidBlob());
+                        statement.TryBind("@guid", user.Id.ToByteArray());
                         statement.TryBind("@data", serialized);
 
                         statement.MoveNext();

+ 6 - 10
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <ItemGroup>
     <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
@@ -29,9 +29,9 @@
     <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
-    <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="Microsoft.Extensions.Logging" Version="3.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.0" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" />
     <PackageReference Include="sharpcompress" Version="0.24.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" />
@@ -47,16 +47,12 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
 
-  <PropertyGroup>
-    <!-- We need at least C# 7.3 to compare tuples-->
-    <LangVersion>latest</LangVersion>
-  </PropertyGroup>
-
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
   </ItemGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 11 - 6
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -27,6 +27,11 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private NatManager _natManager;
 
+        private readonly object _createdRulesLock = new object();
+        private List<string> _createdRules = new List<string>();
+        private readonly object _usnsHandledLock = new object();
+        private List<string> _usnsHandled = new List<string>();
+
         public ExternalPortForwarding(ILoggerFactory loggerFactory, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient)
         {
             _logger = loggerFactory.CreateLogger("PortMapper");
@@ -127,12 +132,13 @@ namespace Emby.Server.Implementations.EntryPoints
                 return;
             }
 
-            lock (_usnsHandled)
+            lock (_usnsHandledLock)
             {
                 if (_usnsHandled.Contains(identifier))
                 {
                     return;
                 }
+
                 _usnsHandled.Add(identifier);
             }
 
@@ -186,11 +192,12 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private void ClearCreatedRules(object state)
         {
-            lock (_createdRules)
+            lock (_createdRulesLock)
             {
                 _createdRules.Clear();
             }
-            lock (_usnsHandled)
+
+            lock (_usnsHandledLock)
             {
                 _usnsHandled.Clear();
             }
@@ -216,8 +223,6 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
-        private List<string> _createdRules = new List<string>();
-        private List<string> _usnsHandled = new List<string>();
         private async void CreateRules(INatDevice device)
         {
             if (_disposed)
@@ -231,7 +236,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             var addressString = address.ToString();
 
-            lock (_createdRules)
+            lock (_createdRulesLock)
             {
                 if (!_createdRules.Contains(addressString))
                 {

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

@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.EntryPoints
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false);
+                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
             }
         }
 

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

@@ -539,6 +539,11 @@ namespace Emby.Server.Implementations.HttpServer
             }
             finally
             {
+                if (httpRes.StatusCode >= 500)
+                {
+                    _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
+                }
+
                 stopWatch.Stop();
                 var elapsed = stopWatch.Elapsed;
                 if (elapsed.TotalMilliseconds > 500)

+ 2 - 2
Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs

@@ -2,11 +2,11 @@ using System;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Library
         {
             return string.IsNullOrEmpty(user.EasyPassword)
                 ? null
-                : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
+                : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
         }
 
         /// <summary>

+ 0 - 1
Emby.Server.Implementations/Library/InvalidAuthProvider.cs

@@ -1,7 +1,6 @@
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Net;
 
 namespace Emby.Server.Implementations.Library
 {

+ 8 - 8
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -519,7 +519,7 @@ namespace Emby.Server.Implementations.Library
         }
 
         public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
-            => ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent);
+            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
 
         private BaseItem ResolvePath(
             FileSystemMetadata fileInfo,
@@ -1045,7 +1045,7 @@ namespace Emby.Server.Implementations.Library
             await RootFolder.ValidateChildren(
                 new SimpleProgress<double>(),
                 cancellationToken,
-                new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)),
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
                 recursive: false).ConfigureAwait(false);
 
             await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
@@ -1053,7 +1053,7 @@ namespace Emby.Server.Implementations.Library
             await GetUserRootFolder().ValidateChildren(
                 new SimpleProgress<double>(),
                 cancellationToken,
-                new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)),
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
                 recursive: false).ConfigureAwait(false);
 
             // Quickly scan CollectionFolders for changes
@@ -1074,7 +1074,7 @@ namespace Emby.Server.Implementations.Library
             innerProgress.RegisterAction(pct => progress.Report(pct * .96));
 
             // Now validate the entire media library
-            await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: true).ConfigureAwait(false);
+            await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
 
             progress.Report(96);
 
@@ -2135,7 +2135,7 @@ namespace Emby.Server.Implementations.Library
             if (refresh)
             {
                 item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
-                _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal);
+                _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
             }
 
             return item;
@@ -2193,7 +2193,7 @@ namespace Emby.Server.Implementations.Library
             {
                 _providerManagerFactory().QueueRefresh(
                     item.Id,
-                    new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         // Need to force save to increment DateLastSaved
                         ForceSave = true
@@ -2261,7 +2261,7 @@ namespace Emby.Server.Implementations.Library
             {
                 _providerManagerFactory().QueueRefresh(
                     item.Id,
-                    new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         // Need to force save to increment DateLastSaved
                         ForceSave = true
@@ -2338,7 +2338,7 @@ namespace Emby.Server.Implementations.Library
             {
                 _providerManagerFactory().QueueRefresh(
                     item.Id,
-                    new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         // Need to force save to increment DateLastSaved
                         ForceSave = true

+ 7 - 6
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.Library
 
             if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
             {
-                await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
-                {
-                    EnableRemoteContentProbe = true,
-                    MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh
-
-                }, cancellationToken).ConfigureAwait(false);
+                await item.RefreshMetadata(
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                    {
+                        EnableRemoteContentProbe = true,
+                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh
+                    },
+                    cancellationToken).ConfigureAwait(false);
 
                 mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
             }

+ 3 - 3
Emby.Server.Implementations/Library/UserManager.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Net;
@@ -31,7 +32,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Users;
 using Microsoft.Extensions.Logging;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -490,7 +490,7 @@ namespace Emby.Server.Implementations.Library
         {
             return string.IsNullOrEmpty(user.EasyPassword)
                 ? null
-                : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash);
+                : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
         }
 
         private void ResetInvalidLoginAttemptCount(User user)
@@ -639,7 +639,7 @@ namespace Emby.Server.Implementations.Library
         {
             foreach (var user in Users)
             {
-                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false);
+                await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
             }
         }
 

+ 18 - 10
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -11,16 +11,17 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.Library.Validators
 {
     /// <summary>
-    /// Class PeopleValidator
+    /// Class PeopleValidator.
     /// </summary>
     public class PeopleValidator
     {
         /// <summary>
-        /// The _library manager
+        /// The _library manager.
         /// </summary>
         private readonly ILibraryManager _libraryManager;
+
         /// <summary>
-        /// The _logger
+        /// The _logger.
         /// </summary>
         private readonly ILogger _logger;
 
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 {
                     var item = _libraryManager.GetPerson(person);
 
-                    var options = new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
                         MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
@@ -96,12 +97,19 @@ namespace Emby.Server.Implementations.Library.Validators
 
             foreach (var item in deadEntities)
             {
-                _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
-
-                _libraryManager.DeleteItem(item, new DeleteOptions
-                {
-                    DeleteFileLocation = false
-                }, false);
+                _logger.LogInformation(
+                    "Deleting dead {2} {0} {1}.",
+                    item.Id.ToString("N", CultureInfo.InvariantCulture),
+                    item.Name,
+                    item.GetType().Name);
+
+                _libraryManager.DeleteItem(
+                    item,
+                    new DeleteOptions
+                    {
+                        DeleteFileLocation = false
+                    },
+                    false);
             }
 
             progress.Report(100);

+ 11 - 9
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1489,16 +1489,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 _logger.LogInformation("Refreshing recording parent {path}", item.Path);
 
-                _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
-                {
-                    RefreshPaths = new string[]
+                _providerManager.QueueRefresh(
+                    item.Id,
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
-                        path,
-                        Path.GetDirectoryName(path),
-                        Path.GetDirectoryName(Path.GetDirectoryName(path))
-                    }
-
-                }, RefreshPriority.High);
+                        RefreshPaths = new string[]
+                        {
+                            path,
+                            Path.GetDirectoryName(path),
+                            Path.GetDirectoryName(Path.GetDirectoryName(path))
+                        }
+                    },
+                    RefreshPriority.High);
             }
         }
 

+ 20 - 15
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1226,12 +1226,13 @@ namespace Emby.Server.Implementations.LiveTv
                         currentChannel.AddTag("Kids");
                     }
 
-                    //currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
-                    await currentChannel.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
-                    {
-                        ForceSave = true
-
-                    }, cancellationToken).ConfigureAwait(false);
+                    currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken);
+                    await currentChannel.RefreshMetadata(
+                        new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                        {
+                            ForceSave = true
+                        },
+                        cancellationToken).ConfigureAwait(false);
                 }
                 catch (OperationCanceledException)
                 {
@@ -1245,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv
                 numComplete++;
                 double percent = numComplete / (double)allChannelsList.Count;
 
-                progress.Report(85 * percent + 15);
+                progress.Report((85 * percent) + 15);
             }
 
             progress.Report(100);
@@ -1278,12 +1279,14 @@ namespace Emby.Server.Implementations.LiveTv
 
                     if (item != null)
                     {
-                        _libraryManager.DeleteItem(item, new DeleteOptions
-                        {
-                            DeleteFileLocation = false,
-                            DeleteFromExternalProvider = false
-
-                        }, false);
+                        _libraryManager.DeleteItem(
+                            item,
+                            new DeleteOptions
+                            {
+                                DeleteFileLocation = false,
+                                DeleteFromExternalProvider = false
+                            },
+                            false);
                     }
                 }
 
@@ -2301,8 +2304,10 @@ namespace Emby.Server.Implementations.LiveTv
             if (provider == null)
             {
                 throw new ResourceNotFoundException(
-                    string.Format("Couldn't find provider of type: '{0}'", info.Type)
-                );
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "Couldn't find provider of type: '{0}'",
+                        info.Type));
             }
 
             await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false);

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

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une image de caméra a été chargée depuis {0}",
+    "CameraImageUploadedFrom": "Une nouvelle image de caméra a été chargée depuis {0}",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
@@ -17,7 +17,7 @@
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes de l'album",
     "HeaderCameraUploads": "Photos transférées",
-    "HeaderContinueWatching": "Reprendre",
+    "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes favoris",
     "HeaderFavoriteEpisodes": "Épisodes favoris",
@@ -34,14 +34,14 @@
     "LabelRunningTimeValue": "Durée : {0}",
     "Latest": "Derniers",
     "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour",
-    "MessageApplicationUpdatedTo": "Jellyfin Serveur a été mis à jour en version {0}",
+    "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour",
     "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour",
     "MixedContent": "Contenu mixte",
     "Movies": "Films",
     "Music": "Musique",
     "MusicVideos": "Vidéos musicales",
-    "NameInstallFailed": "{0} échec d'installation",
+    "NameInstallFailed": "{0} échec de l'installation",
     "NameSeasonNumber": "Saison {0}",
     "NameSeasonUnknown": "Saison Inconnue",
     "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Lecture audio démarrée",
     "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
     "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
-    "NotificationOptionInstallationFailed": "Échec d'installation",
+    "NotificationOptionInstallationFailed": "Échec de l'installation",
     "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
     "NotificationOptionPluginError": "Erreur d'extension",
     "NotificationOptionPluginInstalled": "Extension installée",
@@ -91,7 +91,7 @@
     "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}",
     "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}",
     "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}",
-    "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie",
+    "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque",
     "ValueSpecialEpisodeName": "Spécial - {0}",
     "VersionNumber": "Version {0}"
 }

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

@@ -5,7 +5,7 @@
     "Artists": "Előadók",
     "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva",
     "Books": "Könyvek",
-    "CameraImageUploadedFrom": "Új kamerakép került feltöltésre {0}",
+    "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}",
     "Channels": "Csatornák",
     "ChapterNameValue": "Jelenet {0}",
     "Collections": "Gyűjtemények",
@@ -15,14 +15,14 @@
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
-    "HeaderAlbumArtists": "Album Előadók",
+    "HeaderAlbumArtists": "Album előadók",
     "HeaderCameraUploads": "Kamera feltöltések",
     "HeaderContinueWatching": "Folyamatban lévő filmek",
-    "HeaderFavoriteAlbums": "Kedvenc Albumok",
-    "HeaderFavoriteArtists": "Kedvenc Előadók",
-    "HeaderFavoriteEpisodes": "Kedvenc Epizódok",
-    "HeaderFavoriteShows": "Kedvenc Sorozatok",
-    "HeaderFavoriteSongs": "Kedvenc Dalok",
+    "HeaderFavoriteAlbums": "Kedvenc albumok",
+    "HeaderFavoriteArtists": "Kedvenc előadók",
+    "HeaderFavoriteEpisodes": "Kedvenc epizódok",
+    "HeaderFavoriteShows": "Kedvenc sorozatok",
+    "HeaderFavoriteSongs": "Kedvenc dalok",
     "HeaderLiveTV": "Élő TV",
     "HeaderNextUp": "Következik",
     "HeaderRecordingGroups": "Felvételi csoportok",
@@ -34,21 +34,21 @@
     "LabelRunningTimeValue": "Futási idő: {0}",
     "Latest": "Legújabb",
     "MessageApplicationUpdated": "Jellyfin Szerver frissítve",
-    "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész {0} frissítve",
+    "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}",
     "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve",
     "MixedContent": "Vegyes tartalom",
     "Movies": "Filmek",
     "Music": "Zene",
-    "MusicVideos": "Zenei Videók",
+    "MusicVideos": "Zenei videók",
     "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",
+    "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
+    "NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
     "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
-    "NotificationOptionAudioPlaybackStopped": "Audió lejátszás befejezve",
+    "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
     "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
     "NotificationOptionInstallationFailed": "Telepítési hiba",
     "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
@@ -60,15 +60,15 @@
     "NotificationOptionTaskFailed": "Ütemezett feladat hiba",
     "NotificationOptionUserLockedOut": "Felhasználó tiltva",
     "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve",
-    "NotificationOptionVideoPlaybackStopped": "Videó lejátszás befejezve",
+    "NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva",
     "Photos": "Fényképek",
     "Playlists": "Lejátszási listák",
     "Plugin": "Bővítmény",
     "PluginInstalledWithName": "{0} telepítve",
     "PluginUninstalledWithName": "{0} eltávolítva",
     "PluginUpdatedWithName": "{0} frissítve",
-    "ProviderValue": "Provider: {0}",
-    "ScheduledTaskFailedWithName": "{0} hiba",
+    "ProviderValue": "Szolgáltató: {0}",
+    "ScheduledTaskFailedWithName": "{0} sikertelen",
     "ScheduledTaskStartedWithName": "{0} elkezdve",
     "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
     "Shows": "Műsorok",
@@ -76,10 +76,10 @@
     "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download 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}",
+    "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz: {0}",
     "Sync": "Szinkronizál",
     "System": "Rendszer",
-    "TvShows": "TV Műsorok",
+    "TvShows": "TV műsorok",
     "User": "Felhasználó",
     "UserCreatedWithName": "{0} felhasználó létrehozva",
     "UserDeletedWithName": "{0} felhasználó törölve",
@@ -88,7 +88,7 @@
     "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}",
+    "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {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",

+ 24 - 24
Emby.Server.Implementations/Localization/Core/tr.json

@@ -5,26 +5,26 @@
     "Artists": "Sanatçılar",
     "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
     "Books": "Kitaplar",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
     "Channels": "Kanallar",
-    "ChapterNameValue": "Chapter {0}",
-    "Collections": "Collections",
-    "DeviceOfflineWithName": "{0} has disconnected",
-    "DeviceOnlineWithName": "{0} is connected",
-    "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
-    "Favorites": "Favorites",
-    "Folders": "Folders",
-    "Genres": "Genres",
-    "HeaderAlbumArtists": "Album Artists",
-    "HeaderCameraUploads": "Camera Uploads",
+    "ChapterNameValue": "Bölüm {0}",
+    "Collections": "Koleksiyonlar",
+    "DeviceOfflineWithName": "{0} bağlantısı kesildi",
+    "DeviceOnlineWithName": "{0} bağlı",
+    "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
+    "Favorites": "Favoriler",
+    "Folders": "Klasörler",
+    "Genres": "Türler",
+    "HeaderAlbumArtists": "Albüm Sanatçıları",
+    "HeaderCameraUploads": "Kamera Yüklemeleri",
     "HeaderContinueWatching": "İzlemeye Devam Et",
     "HeaderFavoriteAlbums": "Favori Albümler",
-    "HeaderFavoriteArtists": "Favorite Artists",
-    "HeaderFavoriteEpisodes": "Favorite Episodes",
-    "HeaderFavoriteShows": "Favori Showlar",
-    "HeaderFavoriteSongs": "Favorite Songs",
-    "HeaderLiveTV": "Live TV",
-    "HeaderNextUp": "Next Up",
+    "HeaderFavoriteArtists": "Favori Sanatçılar",
+    "HeaderFavoriteEpisodes": "Favori Bölümler",
+    "HeaderFavoriteShows": "Favori Diziler",
+    "HeaderFavoriteSongs": "Favori Şarkılar",
+    "HeaderLiveTV": "Canlı TV",
+    "HeaderNextUp": "Sonraki hafta",
     "HeaderRecordingGroups": "Recording Groups",
     "HomeVideos": "Home videos",
     "Inherit": "Inherit",
@@ -38,7 +38,7 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
     "MessageServerConfigurationUpdated": "Server configuration has been updated",
     "MixedContent": "Mixed content",
-    "Movies": "Movies",
+    "Movies": "Filmler",
     "Music": "Müzik",
     "MusicVideos": "Müzik videoları",
     "NameInstallFailed": "{0} kurulum başarısız",
@@ -61,8 +61,8 @@
     "NotificationOptionUserLockedOut": "User locked out",
     "NotificationOptionVideoPlayback": "Video playback started",
     "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
-    "Photos": "Photos",
-    "Playlists": "Playlists",
+    "Photos": "Fotoğraflar",
+    "Playlists": "Çalma listeleri",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} was installed",
     "PluginUninstalledWithName": "{0} was uninstalled",
@@ -71,13 +71,13 @@
     "ScheduledTaskFailedWithName": "{0} failed",
     "ScheduledTaskStartedWithName": "{0} started",
     "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
-    "Shows": "Shows",
-    "Songs": "Songs",
+    "Shows": "Diziler",
+    "Songs": "Şarkılar",
     "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
     "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
-    "Sync": "Sync",
+    "Sync": "Eşitle",
     "System": "System",
     "TvShows": "TV Shows",
     "User": "User",
@@ -92,6 +92,6 @@
     "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}",
     "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
     "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
-    "ValueSpecialEpisodeName": "Special - {0}",
+    "ValueSpecialEpisodeName": "Özel -{0}",
     "VersionNumber": "Version {0}"
 }

+ 11 - 5
Emby.Server.Implementations/Networking/NetworkManager.cs

@@ -20,6 +20,9 @@ namespace Emby.Server.Implementations.Networking
         private IPAddress[] _localIpAddresses;
         private readonly object _localIpAddressSyncLock = new object();
 
+        private readonly object _subnetLookupLock = new object();
+        private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
+
         public NetworkManager(ILogger<NetworkManager> logger)
         {
             _logger = logger;
@@ -28,10 +31,10 @@ namespace Emby.Server.Implementations.Networking
             NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
         }
 
-        public Func<string[]> LocalSubnetsFn { get; set; }
-
         public event EventHandler NetworkChanged;
 
+        public Func<string[]> LocalSubnetsFn { get; set; }
+
         private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e)
         {
             _logger.LogDebug("NetworkAvailabilityChanged");
@@ -179,10 +182,9 @@ namespace Emby.Server.Implementations.Networking
             return false;
         }
 
-        private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
         private List<string> GetSubnets(string endpointFirstPart)
         {
-            lock (_subnetLookup)
+            lock (_subnetLookupLock)
             {
                 if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets))
                 {
@@ -200,7 +202,11 @@ namespace Emby.Server.Implementations.Networking
                             int subnet_Test = 0;
                             foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.'))
                             {
-                                if (part.Equals("0")) break;
+                                if (part.Equals("0", StringComparison.Ordinal))
+                                {
+                                    break;
+                                }
+
                                 subnet_Test++;
                             }
 

+ 13 - 16
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.Playlists
                     }
                     else
                     {
-                        var folder = item as Folder;
-                        if (folder != null)
+                        if (item is Folder folder)
                         {
                             options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
                                 .Select(i => i.MediaType)
@@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.Playlists
 
                 parentFolder.AddChild(playlist, CancellationToken.None);
 
-                await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) { ForceSave = true }, CancellationToken.None)
+                await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
                     .ConfigureAwait(false);
 
                 if (options.ItemIdList.Length > 0)
@@ -201,7 +200,7 @@ namespace Emby.Server.Implementations.Playlists
 
             var list = new List<LinkedChild>();
 
-            var items = (GetPlaylistItems(itemIds, playlist.MediaType, user, options))
+            var items = GetPlaylistItems(itemIds, playlist.MediaType, user, options)
                 .Where(i => i.SupportsAddingToPlaylist)
                 .ToList();
 
@@ -221,18 +220,18 @@ namespace Emby.Server.Implementations.Playlists
                 SavePlaylistFile(playlist);
             }
 
-            _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
-            {
-                ForceSave = true
-
-            }, RefreshPriority.High);
+            _providerManager.QueueRefresh(
+                playlist.Id,
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    ForceSave = true
+                },
+                RefreshPriority.High);
         }
 
         public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds)
         {
-            var playlist = _libraryManager.GetItemById(playlistId) as Playlist;
-
-            if (playlist == null)
+            if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
             {
                 throw new ArgumentException("No Playlist exists with the supplied Id");
             }
@@ -254,7 +253,7 @@ namespace Emby.Server.Implementations.Playlists
                 SavePlaylistFile(playlist);
             }
 
-            _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+            _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
             {
                 ForceSave = true
 
@@ -263,9 +262,7 @@ namespace Emby.Server.Implementations.Playlists
 
         public void MoveItem(string playlistId, string entryId, int newIndex)
         {
-            var playlist = _libraryManager.GetItemById(playlistId) as Playlist;
-
-            if (playlist == null)
+            if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist))
             {
                 throw new ArgumentException("No Playlist exists with the supplied Id");
             }

+ 8 - 7
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -19,16 +19,17 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.ScheduledTasks
 {
     /// <summary>
-    /// Class ChapterImagesTask
+    /// Class ChapterImagesTask.
     /// </summary>
     public class ChapterImagesTask : IScheduledTask
     {
         /// <summary>
-        /// The _logger
+        /// The _logger.
         /// </summary>
         private readonly ILogger _logger;
+
         /// <summary>
-        /// The _library manager
+        /// The _library manager.
         /// </summary>
         private readonly ILibraryManager _libraryManager;
 
@@ -53,12 +54,12 @@ namespace Emby.Server.Implementations.ScheduledTasks
         }
 
         /// <summary>
-        /// Creates the triggers that define when the task will run
+        /// Creates the triggers that define when the task will run.
         /// </summary>
         public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
         {
-            return new[] {
-
+            return new[]
+            {
                 new TaskTriggerInfo
                 {
                     Type = TaskTriggerInfo.TriggerDaily,
@@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
                 previouslyFailedImages = new List<string>();
             }
 
-            var directoryService = new DirectoryService(_logger, _fileSystem);
+            var directoryService = new DirectoryService(_fileSystem);
 
             foreach (var video in videos)
             {

+ 5 - 31
Emby.Server.Implementations/Serialization/XmlSerializer.cs → Emby.Server.Implementations/Serialization/MyXmlSerializer.cs

@@ -1,11 +1,9 @@
 using System;
-using System.Collections.Generic;
+using System.Collections.Concurrent;
 using System.IO;
 using System.Xml;
 using System.Xml.Serialization;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Serialization
 {
@@ -14,35 +12,13 @@ namespace Emby.Server.Implementations.Serialization
     /// </summary>
     public class MyXmlSerializer : IXmlSerializer
     {
-        private readonly IFileSystem _fileSystem;
-        private readonly ILogger _logger;
-
-        public MyXmlSerializer(
-            IFileSystem fileSystem,
-            ILoggerFactory loggerFactory)
-        {
-            _fileSystem = fileSystem;
-            _logger = loggerFactory.CreateLogger("XmlSerializer");
-        }
-
         // Need to cache these
         // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html
-        private readonly Dictionary<string, XmlSerializer> _serializers =
-            new Dictionary<string, XmlSerializer>();
+        private static readonly ConcurrentDictionary<string, XmlSerializer> _serializers =
+            new ConcurrentDictionary<string, XmlSerializer>();
 
-        private XmlSerializer GetSerializer(Type type)
-        {
-            var key = type.FullName;
-            lock (_serializers)
-            {
-                if (!_serializers.TryGetValue(key, out var serializer))
-                {
-                    serializer = new XmlSerializer(type);
-                    _serializers[key] = serializer;
-                }
-                return serializer;
-            }
-        }
+        private static XmlSerializer GetSerializer(Type type)
+            => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type));
 
         /// <summary>
         /// Serializes to writer.
@@ -91,7 +67,6 @@ namespace Emby.Server.Implementations.Serialization
         /// <param name="file">The file.</param>
         public void SerializeToFile(object obj, string file)
         {
-            _logger.LogDebug("Serializing to file {0}", file);
             using (var stream = new FileStream(file, FileMode.Create))
             {
                 SerializeToStream(obj, stream);
@@ -106,7 +81,6 @@ namespace Emby.Server.Implementations.Serialization
         /// <returns>System.Object.</returns>
         public object DeserializeFromFile(Type type, string file)
         {
-            _logger.LogDebug("Deserializing file {0}", file);
             using (var stream = File.OpenRead(file))
             {
                 return DeserializeFromStream(type, stream);

+ 1 - 1
Emby.Server.Implementations/ServerApplicationPaths.cs

@@ -6,7 +6,7 @@ using MediaBrowser.Controller;
 namespace Emby.Server.Implementations
 {
     /// <summary>
-    /// Extends BaseApplicationPaths to add paths that are only applicable on the server
+    /// Extends BaseApplicationPaths to add paths that are only applicable on the server.
     /// </summary>
     public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths
     {

+ 45 - 43
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
-using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Net.Http;
@@ -19,7 +18,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Emby.Server.Implementations.Updates
 {
@@ -28,43 +26,10 @@ namespace Emby.Server.Implementations.Updates
     /// </summary>
     public class InstallationManager : IInstallationManager
     {
-        public event EventHandler<InstallationEventArgs> PackageInstalling;
-        public event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
-        public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
-        public event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
-
-        /// <summary>
-        /// The current installations
-        /// </summary>
-        private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations { get; set; }
-
         /// <summary>
-        /// The completed installations
-        /// </summary>
-        private ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
-
-        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
-
-        /// <summary>
-        /// Occurs when [plugin uninstalled].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
-
-        /// <summary>
-        /// Occurs when [plugin updated].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated;
-
-        /// <summary>
-        /// Occurs when [plugin updated].
-        /// </summary>
-        public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
-
-        /// <summary>
-        /// The _logger
+        /// The _logger.
         /// </summary>
         private readonly ILogger _logger;
-
         private readonly IApplicationPaths _appPaths;
         private readonly IHttpClient _httpClient;
         private readonly IJsonSerializer _jsonSerializer;
@@ -79,6 +44,18 @@ namespace Emby.Server.Implementations.Updates
 
         private readonly IZipClient _zipClient;
 
+        private readonly object _currentInstallationsLock = new object();
+
+        /// <summary>
+        /// The current installations.
+        /// </summary>
+        private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations;
+
+        /// <summary>
+        /// The completed installations.
+        /// </summary>
+        private ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
+
         public InstallationManager(
             ILogger<InstallationManager> logger,
             IApplicationHost appHost,
@@ -107,6 +84,31 @@ namespace Emby.Server.Implementations.Updates
             _zipClient = zipClient;
         }
 
+        public event EventHandler<InstallationEventArgs> PackageInstalling;
+
+        public event EventHandler<InstallationEventArgs> PackageInstallationCompleted;
+
+        public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
+
+        public event EventHandler<InstallationEventArgs> PackageInstallationCancelled;
+
+        /// <summary>
+        /// Occurs when a plugin is uninstalled.
+        /// </summary>
+        public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
+
+        /// <summary>
+        /// Occurs when a plugin plugin is updated.
+        /// </summary>
+        public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated;
+
+        /// <summary>
+        /// Occurs when a plugin plugin is installed.
+        /// </summary>
+        public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+
+        public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
+
         /// <summary>
         /// Gets all available packages.
         /// </summary>
@@ -330,7 +332,7 @@ namespace Emby.Server.Implementations.Updates
             var tuple = (installationInfo, innerCancellationTokenSource);
 
             // Add it to the in-progress list
-            lock (_currentInstallations)
+            lock (_currentInstallationsLock)
             {
                 _currentInstallations.Add(tuple);
             }
@@ -349,7 +351,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
 
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -360,7 +362,7 @@ namespace Emby.Server.Implementations.Updates
             }
             catch (OperationCanceledException)
             {
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -375,7 +377,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 _logger.LogError(ex, "Package installation failed");
 
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -455,7 +457,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                var hash = ToHexString(md5.ComputeHash(stream));
+                var hash = Hex.Encode(md5.ComputeHash(stream));
                 if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase))
                 {
                     _logger.LogError(
@@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Updates
         /// <inheritdoc/>
         public bool CancelInstallation(Guid id)
         {
-            lock (_currentInstallations)
+            lock (_currentInstallationsLock)
             {
                 var install = _currentInstallations.Find(x => x.Item1.Id == id);
                 if (install == default((InstallationInfo, CancellationTokenSource)))
@@ -563,7 +565,7 @@ namespace Emby.Server.Implementations.Updates
         {
             if (dispose)
             {
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     foreach (var tuple in _currentInstallations)
                     {

+ 1 - 1
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 4 - 5
Jellyfin.Server/Jellyfin.Server.csproj

@@ -9,8 +9,6 @@
   </PropertyGroup>
 
   <PropertyGroup>
-    <!-- We need at least C# 7.1 for async main-->
-    <LangVersion>latest</LangVersion>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
@@ -25,8 +23,9 @@
   <!-- Code analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
+    <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
   </ItemGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -35,8 +34,8 @@
 
   <ItemGroup>
     <PackageReference Include="CommandLineParser" Version="2.6.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="3.0.0" />
     <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
     <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />

+ 1 - 1
Jellyfin.Server/Program.cs

@@ -376,7 +376,7 @@ namespace Jellyfin.Server
 
             return new ConfigurationBuilder()
                 .SetBasePath(appPaths.ConfigurationDirectoryPath)
-                .AddJsonFile("logging.json")
+                .AddJsonFile("logging.json", false, true)
                 .AddEnvironmentVariables("JELLYFIN_")
                 .AddInMemoryCollection(ConfigurationOptions.Configuration)
                 .Build();

+ 2 - 1
MediaBrowser.Api/IHasItemFields.cs

@@ -32,7 +32,7 @@ namespace MediaBrowser.Api
 
             if (string.IsNullOrEmpty(val))
             {
-                return new ItemFields[] { };
+                return Array.Empty<ItemFields>();
             }
 
             return val.Split(',').Select(v =>
@@ -41,6 +41,7 @@ namespace MediaBrowser.Api
                 {
                     return (ItemFields?)value;
                 }
+
                 return null;
 
             }).Where(i => i.HasValue).Select(i => i.Value).ToArray();

+ 14 - 17
MediaBrowser.Api/ItemLookupService.cs

@@ -227,15 +227,17 @@ namespace MediaBrowser.Api
             //item.ProductionYear = request.ProductionYear;
             //item.Name = request.Name;
 
-            return _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem))
-            {
-                MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                ReplaceAllMetadata = true,
-                ReplaceAllImages = request.ReplaceAllImages,
-                SearchResult = request
-
-            }, CancellationToken.None);
+            return _providerManager.RefreshFullItem(
+                item,
+                new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                    ReplaceAllMetadata = true,
+                    ReplaceAllImages = request.ReplaceAllImages,
+                    SearchResult = request
+                },
+                CancellationToken.None);
         }
 
         /// <summary>
@@ -294,11 +296,9 @@ namespace MediaBrowser.Api
 
             Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
             using (var stream = result.Content)
+            using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
             {
-                using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
-                {
-                    await stream.CopyToAsync(filestream).ConfigureAwait(false);
-                }
+                await stream.CopyToAsync(filestream).ConfigureAwait(false);
             }
 
             Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
@@ -311,9 +311,6 @@ namespace MediaBrowser.Api
         /// <param name="filename">The filename.</param>
         /// <returns>System.String.</returns>
         private string GetFullCachePath(string filename)
-        {
-            return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
-        }
-
+            => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
     }
 }

+ 1 - 1
MediaBrowser.Api/ItemRefreshService.cs

@@ -63,7 +63,7 @@ namespace MediaBrowser.Api
 
         private MetadataRefreshOptions GetRefreshOptions(RefreshItem request)
         {
-            return new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))
+            return new MetadataRefreshOptions(new DirectoryService(_fileSystem))
             {
                 MetadataRefreshMode = request.MetadataRefreshMode,
                 ImageRefreshMode = request.ImageRefreshMode,

+ 9 - 7
MediaBrowser.Api/ItemUpdateService.cs

@@ -225,13 +225,15 @@ namespace MediaBrowser.Api
 
             if (displayOrderChanged)
             {
-                _providerManager.QueueRefresh(series.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem))
-                {
-                    MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ImageRefreshMode = MetadataRefreshMode.FullRefresh,
-                    ReplaceAllMetadata = true
-
-                }, RefreshPriority.High);
+                _providerManager.QueueRefresh(
+                    series.Id,
+                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                    {
+                        MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+                        ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+                        ReplaceAllMetadata = true
+                    },
+                    RefreshPriority.High);
             }
         }
 

+ 4 - 3
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -8,6 +8,7 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Api.UserLibrary;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -25,7 +26,6 @@ using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Services;
 using Microsoft.Net.Http.Headers;
-using static MediaBrowser.Common.HexHelper;
 
 namespace MediaBrowser.Api.LiveTv
 {
@@ -887,8 +887,9 @@ namespace MediaBrowser.Api.LiveTv
         {
             // SchedulesDirect requires a SHA1 hash of the user's password
             // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
-            using (SHA1 sha = SHA1.Create()) {
-                return ToHexString(
+            using (SHA1 sha = SHA1.Create())
+            {
+                return Hex.Encode(
                     sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
             }
         }

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

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 7 - 1
MediaBrowser.Api/Playback/BaseStreamingService.cs

@@ -289,17 +289,22 @@ namespace MediaBrowser.Api.Playback
                 throw;
             }
 
+            Logger.LogDebug("Launched ffmpeg process");
             state.TranscodingJob = transcodingJob;
 
             // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
             _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
 
             // Wait for the file to exist before proceeeding
-            while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
+            var ffmpegTargetFile = state.WaitForPath ?? outputPath;
+            Logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
+            while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
             {
                 await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false);
             }
 
+            Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile);
+
             if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited)
             {
                 await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false);
@@ -314,6 +319,7 @@ namespace MediaBrowser.Api.Playback
             {
                 StartThrottler(state, transcodingJob);
             }
+            Logger.LogDebug("StartFfMpeg() finished successfully");
 
             return transcodingJob;
         }

+ 70 - 40
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -192,6 +192,7 @@ namespace MediaBrowser.Api.Playback.Hls
             if (File.Exists(segmentPath))
             {
                 job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+                Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
                 return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
             }
 
@@ -207,6 +208,7 @@ namespace MediaBrowser.Api.Playback.Hls
                     job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
                     transcodingLock.Release();
                     released = true;
+                    Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
                     return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
                 }
                 else
@@ -243,6 +245,7 @@ namespace MediaBrowser.Api.Playback.Hls
 
                             request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
 
+                            state.WaitForPath = segmentPath;
                             job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
                         }
                         catch
@@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback.Hls
             //    await Task.Delay(50, cancellationToken).ConfigureAwait(false);
             //}
 
-            Logger.LogInformation("returning {0}", segmentPath);
+            Logger.LogDebug("returning {0} [general case]", segmentPath);
             job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
             return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
         }
@@ -458,56 +461,68 @@ namespace MediaBrowser.Api.Playback.Hls
             TranscodingJob transcodingJob,
             CancellationToken cancellationToken)
         {
-            var segmentFileExists = File.Exists(segmentPath);
-
-            // If all transcoding has completed, just return immediately
-            if (transcodingJob != null && transcodingJob.HasExited && segmentFileExists)
+            var segmentExists = File.Exists(segmentPath);
+            if (segmentExists)
             {
-                return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
-            }
+                if (transcodingJob != null && transcodingJob.HasExited)
+                {
+                    // Transcoding job is over, so assume all existing files are ready
+                    Logger.LogDebug("serving up {0} as transcode is over", segmentPath);
+                    return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
+                }
 
-            if (segmentFileExists)
-            {
                 var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
 
                 // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready
                 if (segmentIndex < currentTranscodingIndex)
                 {
+                    Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex);
                     return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
                 }
             }
 
-            var segmentFilename = Path.GetFileName(segmentPath);
-
-            while (!cancellationToken.IsCancellationRequested)
+            var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1);
+            if (transcodingJob != null)
             {
-                try
+                while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited)
                 {
-                    var text = File.ReadAllText(playlistPath, Encoding.UTF8);
-
-                    // If it appears in the playlist, it's done
-                    if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
+                    // To be considered ready, the segment file has to exist AND
+                    // either the transcoding job should be done or next segment should also exist
+                    if (segmentExists)
                     {
-                        if (!segmentFileExists)
+                        if (transcodingJob.HasExited || File.Exists(nextSegmentPath))
                         {
-                            segmentFileExists = File.Exists(segmentPath);
+                            Logger.LogDebug("serving up {0} as it deemed ready", segmentPath);
+                            return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
                         }
-                        if (segmentFileExists)
+                    }
+                    else
+                    {
+                        segmentExists = File.Exists(segmentPath);
+                        if (segmentExists)
                         {
-                            return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
+                            continue; // avoid unnecessary waiting if segment just became available
                         }
-                        //break;
                     }
+
+                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                }
+
+                if (!File.Exists(segmentPath))
+                {
+                    Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath);
                 }
-                catch (IOException)
+                else
                 {
-                    // May get an error if the file is locked
+                    Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath);
                 }
-
-                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+                cancellationToken.ThrowIfCancellationRequested();
+            }
+            else
+            {
+                Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath);
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
             return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
         }
 
@@ -521,6 +536,7 @@ namespace MediaBrowser.Api.Playback.Hls
                 FileShare = FileShareMode.ReadWrite,
                 OnComplete = () =>
                 {
+                    Logger.LogDebug("finished serving {0}", segmentPath);
                     if (transcodingJob != null)
                     {
                         transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
@@ -909,9 +925,23 @@ namespace MediaBrowser.Api.Playback.Hls
             else
             {
                 var keyFrameArg = string.Format(
+                    CultureInfo.InvariantCulture,
                     " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
                     GetStartNumber(state) * state.SegmentLength,
-                    state.SegmentLength.ToString(CultureInfo.InvariantCulture));
+                    state.SegmentLength);
+                if (state.TargetFramerate.HasValue)
+                {
+                    // This is to make sure keyframe interval is limited to our segment,
+                    // as forcing keyframes is not enough.
+                    // Example: we encoded half of desired length, then codec detected
+                    // scene cut and inserted a keyframe; next forced keyframe would
+                    // be created outside of segment, which breaks seeking.
+                    keyFrameArg += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " -g {0} -keyint_min {0}",
+                        (int)(state.SegmentLength * state.TargetFramerate)
+                    );
+                }
 
                 var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
 
@@ -955,6 +985,15 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
 
+            if (state.BaseRequest.BreakOnNonKeyFrames)
+            {
+                // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe
+                //        breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable
+                //        to produce a missing part of video stream before first keyframe is encountered, which may lead to
+                //        awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js
+                Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request");
+                state.BaseRequest.BreakOnNonKeyFrames = false;
+            }
             var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
 
             // If isEncoding is true we're actually starting ffmpeg
@@ -965,14 +1004,6 @@ namespace MediaBrowser.Api.Playback.Hls
 
             var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request);
 
-            var timeDeltaParam = string.Empty;
-
-            if (isEncoding && state.TargetFramerate > 0)
-            {
-                float startTime = 1 / (state.TargetFramerate.Value * 2);
-                timeDeltaParam = string.Format(CultureInfo.InvariantCulture, "-segment_time_delta {0:F3}", startTime);
-            }
-
             var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.');
             if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
             {
@@ -980,7 +1011,7 @@ namespace MediaBrowser.Api.Playback.Hls
             }
 
             return string.Format(
-                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
+                "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f hls -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"",
                 inputModifier,
                 EncodingHelper.GetInputArgument(state, encodingOptions),
                 threads,
@@ -988,11 +1019,10 @@ namespace MediaBrowser.Api.Playback.Hls
                 GetVideoArguments(state, encodingOptions),
                 GetAudioArguments(state, encodingOptions),
                 state.SegmentLength.ToString(CultureInfo.InvariantCulture),
+                segmentFormat,
                 startNumberParam,
-                outputPath,
                 outputTsArg,
-                timeDeltaParam,
-                segmentFormat
+                outputPath
             ).Trim();
         }
     }

+ 1 - 2
MediaBrowser.Api/Subtitles/SubtitleService.cs

@@ -279,13 +279,12 @@ namespace MediaBrowser.Api.Subtitles
                     await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None)
                         .ConfigureAwait(false);
 
-                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)), RefreshPriority.High);
+                    _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
                 }
                 catch (Exception ex)
                 {
                     Logger.LogError(ex, "Error downloading subtitles");
                 }
-
             });
         }
     }

+ 5 - 0
MediaBrowser.Api/UserLibrary/PersonsService.cs

@@ -109,6 +109,11 @@ namespace MediaBrowser.Api.UserLibrary
                 NameContains = query.NameContains ?? query.SearchTerm
             });
 
+            if (query.IsFavorite ?? false && query.User != null)
+            {
+                items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList();
+            }
+
             return new QueryResult<(BaseItem, ItemCounts)>
             {
                 TotalRecordCount = items.Count,

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

@@ -413,7 +413,7 @@ namespace MediaBrowser.Api.UserLibrary
 
                 if (!hasMetdata)
                 {
-                    var options = new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem))
+                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
                     {
                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
                         ImageRefreshMode = MetadataRefreshMode.FullRefresh,

+ 14 - 17
MediaBrowser.Common/Cryptography/PasswordHash.cs

@@ -4,7 +4,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using static MediaBrowser.Common.HexHelper;
 
 namespace MediaBrowser.Common.Cryptography
 {
@@ -102,13 +101,13 @@ namespace MediaBrowser.Common.Cryptography
             // Check if the string also contains a salt
             if (splitted.Length - index == 2)
             {
-                salt = FromHexString(splitted[index++]);
-                hash = FromHexString(splitted[index++]);
+                salt = Hex.Decode(splitted[index++]);
+                hash = Hex.Decode(splitted[index++]);
             }
             else
             {
                 salt = Array.Empty<byte>();
-                hash = FromHexString(splitted[index++]);
+                hash = Hex.Decode(splitted[index++]);
             }
 
             return new PasswordHash(id, hash, salt, parameters);
@@ -124,10 +123,10 @@ namespace MediaBrowser.Common.Cryptography
             stringBuilder.Append('$');
             foreach (var pair in _parameters)
             {
-                stringBuilder.Append(pair.Key);
-                stringBuilder.Append('=');
-                stringBuilder.Append(pair.Value);
-                stringBuilder.Append(',');
+                stringBuilder.Append(pair.Key)
+                    .Append('=')
+                    .Append(pair.Value)
+                    .Append(',');
             }
 
             // Remove last ','
@@ -137,21 +136,19 @@ namespace MediaBrowser.Common.Cryptography
         /// <inheritdoc />
         public override string ToString()
         {
-            var str = new StringBuilder();
-            str.Append('$');
-            str.Append(Id);
+            var str = new StringBuilder()
+                .Append('$')
+                .Append(Id);
             SerializeParameters(str);
 
             if (Salt.Length != 0)
             {
-                str.Append('$');
-                str.Append(ToHexString(Salt));
+                str.Append('$')
+                    .Append(Hex.Encode(Salt, false));
             }
 
-            str.Append('$');
-            str.Append(ToHexString(Hash));
-
-            return str.ToString();
+            return str.Append('$')
+                .Append(Hex.Encode(Hash, false)).ToString();
         }
     }
 }

+ 0 - 48
MediaBrowser.Common/Extensions/CollectionExtensions.cs

@@ -1,48 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Common.Extensions
-{
-    // The MS CollectionExtensions are only available in netcoreapp
-    public static class CollectionExtensions
-    {
-        public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
-        {
-            dictionary.TryGetValue(key, out var ret);
-            return ret;
-        }
-
-        /// <summary>
-        /// Copies all the elements of the current collection to the specified list
-        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
-        /// </summary>
-        /// <param name="source">The current collection that is the source of the elements.</param>
-        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
-        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
-        /// <typeparam name="T"></typeparam>
-        public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
-        {
-            for (int i = 0; i < source.Count; i++)
-            {
-                destination[index + i] = source[i];
-            }
-        }
-
-        /// <summary>
-        /// Copies all the elements of the current collection to the specified list
-        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
-        /// </summary>
-        /// <param name="source">The current collection that is the source of the elements.</param>
-        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
-        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
-        /// <typeparam name="T"></typeparam>
-        public static void CopyTo<T>(this IReadOnlyCollection<T> source, IList<T> destination, int index = 0)
-        {
-            foreach (T item in source)
-            {
-                destination[index++] = item;
-            }
-        }
-    }
-}

+ 26 - 0
MediaBrowser.Common/Extensions/CopyToExtensions.cs

@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+
+namespace MediaBrowser.Common.Extensions
+{
+    /// <summary>
+    /// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
+    /// </summary>
+    public static class CollectionExtensions
+    {
+        /// <summary>
+        /// Copies all the elements of the current collection to the specified list
+        /// starting at the specified destination array index. The index is specified as a 32-bit integer.
+        /// </summary>
+        /// <param name="source">The current collection that is the source of the elements.</param>
+        /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
+        /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
+        /// <typeparam name="T"></typeparam>
+        public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
+        {
+            for (int i = 0; i < source.Count; i++)
+            {
+                destination[index + i] = source[i];
+            }
+        }
+    }
+}

+ 94 - 0
MediaBrowser.Common/Hex.cs

@@ -0,0 +1,94 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace MediaBrowser.Common
+{
+    /// <summary>
+    /// Encoding and decoding hex strings.
+    /// </summary>
+    public static class Hex
+    {
+        internal const string HexCharsLower = "0123456789abcdef";
+        internal const string HexCharsUpper = "0123456789ABCDEF";
+
+        internal const int LastHexSymbol = 0x66; // 102: f
+
+        /// <summary>
+        /// Map from an ASCII char to its hex value shifted,
+        /// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol.
+        /// </summary>
+        /// <value></value>
+        internal static ReadOnlySpan<byte> HexLookup => new byte[] {
+            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+            0xFF, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
+        };
+
+        /// <summary>
+        /// Encodes <c>bytes</c> as a hex string.
+        /// </summary>
+        /// <param name="bytes"></param>
+        /// <param name="lowercase"></param>
+        /// <returns><c>bytes</c> as a hex string.</returns>
+        public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true)
+        {
+            var hexChars = lowercase ? HexCharsLower : HexCharsUpper;
+
+            // TODO: use string.Create when it's supports spans
+            // Ref: https://github.com/dotnet/corefx/issues/29120
+            char[] s = new char[bytes.Length * 2];
+            int j = 0;
+            for (int i = 0; i < bytes.Length; i++)
+            {
+                s[j++] = hexChars[bytes[i] >> 4];
+                s[j++] = hexChars[bytes[i] & 0x0f];
+            }
+
+            return new string(s);
+        }
+
+        /// <summary>
+        /// Decodes a hex string into bytes.
+        /// </summary>
+        /// <param name="str">The <see cref="string" />.</param>
+        /// <returns>The decoded bytes.</returns>
+        public static byte[] Decode(ReadOnlySpan<char> str)
+        {
+            if (str.Length == 0)
+            {
+                return Array.Empty<byte>();
+            }
+
+            var unHex = HexLookup;
+
+            int byteLen = str.Length / 2;
+            byte[] bytes = new byte[byteLen];
+            int i = 0;
+            for (int j = 0; j < byteLen; j++)
+            {
+                byte a;
+                byte b;
+                if (str[i] > LastHexSymbol
+                    || (a = unHex[str[i++]]) == 0xFF
+                    || str[i] > LastHexSymbol
+                    || (b = unHex[str[i++]]) == 0xFF)
+                {
+                    ThrowArgumentException(nameof(str));
+                    break; // Unreachable
+                }
+
+                bytes[j] = (byte)((a * 16) | b);
+            }
+
+            return bytes;
+        }
+
+        [DoesNotReturn]
+        private static void ThrowArgumentException(string paramName)
+            => throw new ArgumentException("Character is not a hex symbol.", paramName);
+    }
+}

+ 0 - 24
MediaBrowser.Common/HexHelper.cs

@@ -1,24 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-
-namespace MediaBrowser.Common
-{
-    public static class HexHelper
-    {
-        public static byte[] FromHexString(string str)
-        {
-            byte[] bytes = new byte[str.Length / 2];
-            for (int i = 0; i < str.Length; i += 2)
-            {
-                bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
-            }
-
-            return bytes;
-        }
-
-        public static string ToHexString(byte[] bytes)
-            => BitConverter.ToString(bytes).Replace("-", "");
-    }
-}

+ 2 - 2
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -12,7 +12,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.0" />
     <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.0" />
   </ItemGroup>
 
@@ -21,7 +21,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

+ 1 - 1
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 var locations = PhysicalLocations;
 
-                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+                var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations;
 
                 if (!locations.SequenceEqual(newLocations))
                 {

+ 2 - 2
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -1345,7 +1345,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Task RefreshMetadata(CancellationToken cancellationToken)
         {
-            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken);
+            return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken);
         }
 
         protected virtual void TriggerOnRefreshStart()
@@ -2198,7 +2198,7 @@ namespace MediaBrowser.Controller.Entities
         /// <returns>Task.</returns>
         public virtual void ChangedExternally()
         {
-            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
+            ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem))
             {
 
             }, RefreshPriority.High);

+ 1 - 1
MediaBrowser.Controller/Entities/CollectionFolder.cs

@@ -171,7 +171,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 var locations = PhysicalLocations;
 
-                var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations;
+                var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations;
 
                 if (!locations.SequenceEqual(newLocations))
                 {

+ 1 - 1
MediaBrowser.Controller/Entities/Folder.cs

@@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken)
         {
-            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)));
+            return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem)));
         }
 
         /// <summary>

+ 1 - 1
MediaBrowser.Controller/Entities/User.cs

@@ -148,7 +148,7 @@ namespace MediaBrowser.Controller.Entities
             Name = newName;
 
             return RefreshMetadata(
-                new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))
+                new MetadataRefreshOptions(new DirectoryService(FileSystem))
                 {
                     ReplaceAllMetadata = true,
                     ImageRefreshMode = MetadataRefreshMode.FullRefresh,

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

@@ -17,7 +17,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 2 - 1
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -2168,7 +2168,8 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking
                 if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)
                     && state.TranscodingType != TranscodingJobType.Progressive
-                    && state.EnableBreakOnNonKeyFrames(outputVideoCodec))
+                    && !state.EnableBreakOnNonKeyFrames(outputVideoCodec)
+                    && (state.BaseRequest.StartTimeTicks ?? 0) > 0)
                 {
                     inputModifier += " -noaccurate_seek";
                 }

+ 2 - 7
MediaBrowser.Controller/Providers/DirectoryService.cs

@@ -2,13 +2,11 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Providers
 {
     public class DirectoryService : IDirectoryService
     {
-        private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
 
         private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase);
@@ -17,9 +15,8 @@ namespace MediaBrowser.Controller.Providers
 
         private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
 
-        public DirectoryService(ILogger logger, IFileSystem fileSystem)
+        public DirectoryService(IFileSystem fileSystem)
         {
-            _logger = logger;
             _fileSystem = fileSystem;
         }
 
@@ -27,8 +24,6 @@ namespace MediaBrowser.Controller.Providers
         {
             if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries))
             {
-                //_logger.LogDebug("Getting files for " + path);
-
                 entries = _fileSystem.GetFileSystemEntries(path).ToArray();
 
                 //_cache.TryAdd(path, entries);
@@ -49,6 +44,7 @@ namespace MediaBrowser.Controller.Providers
                     list.Add(item);
                 }
             }
+
             return list;
         }
 
@@ -89,6 +85,5 @@ namespace MediaBrowser.Controller.Providers
 
             return result;
         }
-
     }
 }

+ 6 - 4
MediaBrowser.Controller/Subtitles/ISubtitleManager.cs

@@ -24,7 +24,8 @@ namespace MediaBrowser.Controller.Subtitles
         /// <summary>
         /// Searches the subtitles.
         /// </summary>
-        Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video,
+        Task<RemoteSubtitleInfo[]> SearchSubtitles(
+            Video video,
             string language,
             bool? isPerfectMatch,
             CancellationToken cancellationToken);
@@ -34,8 +35,9 @@ namespace MediaBrowser.Controller.Subtitles
         /// </summary>
         /// <param name="request">The request.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns>
-        Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request,
+        /// <returns>Task{RemoteSubtitleInfo[]}.</returns>
+        Task<RemoteSubtitleInfo[]> SearchSubtitles(
+            SubtitleSearchRequest request,
             CancellationToken cancellationToken);
 
         /// <summary>
@@ -53,7 +55,7 @@ namespace MediaBrowser.Controller.Subtitles
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task{SubtitleResponse}.</returns>
+        /// <returns><see cref="Task{SubtitleResponse}" />.</returns>
         Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken);
 
         /// <summary>

+ 1 - 1
MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj

@@ -10,7 +10,7 @@
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>

+ 2 - 2
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
@@ -18,7 +18,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
+    <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" />
     <PackageReference Include="UTF.Unknown" Version="2.1.0" />
   </ItemGroup>
 

+ 3 - 3
MediaBrowser.Model/IO/StreamDefaults.cs

@@ -1,17 +1,17 @@
 namespace MediaBrowser.Model.IO
 {
     /// <summary>
-    /// Class StreamDefaults
+    /// Class StreamDefaults.
     /// </summary>
     public static class StreamDefaults
     {
         /// <summary>
-        /// The default copy to buffer size
+        /// The default copy to buffer size.
         /// </summary>
         public const int DefaultCopyToBufferSize = 81920;
 
         /// <summary>
-        /// The default file stream buffer size
+        /// The default file stream buffer size.
         /// </summary>
         public const int DefaultFileStreamBufferSize = 4096;
     }

+ 1 - 1
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -15,7 +15,7 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.0" />
     <PackageReference Include="System.Text.Json" Version="4.6.0" />
   </ItemGroup>
 

+ 1 - 2
MediaBrowser.Providers/Books/AudioBookMetadataService.cs

@@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Books
             ILogger logger,
             IProviderManager providerManager,
             IFileSystem fileSystem,
-            IUserDataManager userDataManager,
             ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
         }
 

+ 11 - 4
MediaBrowser.Providers/Books/BookMetadataService.cs

@@ -11,6 +11,17 @@ namespace MediaBrowser.Providers.Books
 {
     public class BookMetadataService : MetadataService<Book, BookInfo>
     {
+        public BookMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        {
+        }
+
+        /// <inheritdoc />
         protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
@@ -20,9 +31,5 @@ namespace MediaBrowser.Providers.Books
                 target.Item.SeriesName = source.Item.SeriesName;
             }
         }
-
-        public BookMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
-        {
-        }
     }
 }

+ 25 - 12
MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs

@@ -14,11 +14,35 @@ namespace MediaBrowser.Providers.BoxSets
 {
     public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo>
     {
+        public BoxSetMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override bool EnableUpdatingGenresFromChildren => true;
+
+        /// <inheritdoc />
+        protected override bool EnableUpdatingOfficialRatingFromChildren => true;
+
+        /// <inheritdoc />
+        protected override bool EnableUpdatingStudiosFromChildren => true;
+
+        /// <inheritdoc />
+        protected override bool EnableUpdatingPremiereDateFromChildren => true;
+
+        /// <inheritdoc />
         protected override IList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item)
         {
             return item.GetLinkedChildren();
         }
 
+        /// <inheritdoc />
         protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
@@ -32,6 +56,7 @@ namespace MediaBrowser.Providers.BoxSets
             }
         }
 
+        /// <inheritdoc />
         protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType)
         {
             var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
@@ -47,17 +72,5 @@ namespace MediaBrowser.Providers.BoxSets
 
             return updateType;
         }
-
-        public BoxSetMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
-        {
-        }
-
-        protected override bool EnableUpdatingGenresFromChildren => true;
-
-        protected override bool EnableUpdatingOfficialRatingFromChildren => true;
-
-        protected override bool EnableUpdatingStudiosFromChildren => true;
-
-        protected override bool EnableUpdatingPremiereDateFromChildren => true;
     }
 }

+ 10 - 3
MediaBrowser.Providers/Channels/ChannelMetadataService.cs

@@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Channels
 {
     public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo>
     {
-        protected override void MergeData(MetadataResult<Channel> source, MetadataResult<Channel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        public ChannelMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
 
-        public ChannelMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<Channel> source, MetadataResult<Channel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 10 - 3
MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs

@@ -12,13 +12,20 @@ namespace MediaBrowser.Providers.Folders
 {
     public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo>
     {
-        protected override void MergeData(MetadataResult<CollectionFolder> source, MetadataResult<CollectionFolder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        public CollectionFolderMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
 
-        public CollectionFolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<CollectionFolder> source, MetadataResult<CollectionFolder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 12 - 4
MediaBrowser.Providers/Folders/FolderMetadataService.cs

@@ -11,16 +11,24 @@ namespace MediaBrowser.Providers.Folders
 {
     public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo>
     {
+        public FolderMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        {
+        }
+
+        /// <inheritdoc />
         // Make sure the type-specific services get picked first
         public override int Order => 10;
 
+        /// <inheritdoc />
         protected override void MergeData(MetadataResult<Folder> source, MetadataResult<Folder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
-
-        public FolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
-        {
-        }
     }
 }

+ 10 - 3
MediaBrowser.Providers/Folders/UserViewMetadataService.cs

@@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Folders
 {
     public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo>
     {
-        protected override void MergeData(MetadataResult<UserView> source, MetadataResult<UserView> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        public UserViewMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
 
-        public UserViewMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<UserView> source, MetadataResult<UserView> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 10 - 3
MediaBrowser.Providers/Genres/GenreMetadataService.cs

@@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Genres
 {
     public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo>
     {
-        protected override void MergeData(MetadataResult<Genre> source, MetadataResult<Genre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        public GenreMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
 
-        public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<Genre> source, MetadataResult<Genre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 10 - 3
MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs

@@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.LiveTv
 {
     public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo>
     {
-        protected override void MergeData(MetadataResult<LiveTvChannel> source, MetadataResult<LiveTvChannel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        public LiveTvMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
 
-        public LiveTvMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<LiveTvChannel> source, MetadataResult<LiveTvChannel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 1 - 1
MediaBrowser.Providers/Manager/ItemImageProvider.cs

@@ -340,7 +340,7 @@ namespace MediaBrowser.Providers.Manager
 
             if (deleted)
             {
-                item.ValidateImages(new DirectoryService(_logger, _fileSystem));
+                item.ValidateImages(new DirectoryService(_fileSystem));
             }
         }
 

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

@@ -23,16 +23,14 @@ namespace MediaBrowser.Providers.Manager
         protected readonly ILogger Logger;
         protected readonly IProviderManager ProviderManager;
         protected readonly IFileSystem FileSystem;
-        protected readonly IUserDataManager UserDataManager;
         protected readonly ILibraryManager LibraryManager;
 
-        protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager)
+        protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
         {
             ServerConfigurationManager = serverConfigurationManager;
             Logger = logger;
             ProviderManager = providerManager;
             FileSystem = fileSystem;
-            UserDataManager = userDataManager;
             LibraryManager = libraryManager;
         }
 
@@ -44,7 +42,7 @@ namespace MediaBrowser.Providers.Manager
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error getting file {path}", path);
+                Logger.LogError(ex, "Error getting file {Path}", path);
                 return null;
             }
         }

+ 2 - 2
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -328,7 +328,7 @@ namespace MediaBrowser.Providers.Manager
 
             return GetImageProviders(item, libraryOptions, options,
                     new ImageRefreshOptions(
-                        new DirectoryService(_logger, _fileSystem)),
+                        new DirectoryService(_fileSystem)),
                     includeDisabled)
                 .OfType<IRemoteImageProvider>();
         }
@@ -507,7 +507,7 @@ namespace MediaBrowser.Providers.Manager
 
             var imageProviders = GetImageProviders(dummy, libraryOptions, options,
                                     new ImageRefreshOptions(
-                                        new DirectoryService(_logger, _fileSystem)),
+                                        new DirectoryService(_fileSystem)),
                                     true)
                                 .ToList();
 

+ 4 - 4
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -11,15 +11,15 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.0" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.0.4" />
     <PackageReference Include="TvDbSharper" Version="2.0.0" />
   </ItemGroup>
 
   <PropertyGroup>
-    <TargetFramework>netstandard2.0</TargetFramework>
+    <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
@@ -28,5 +28,5 @@
     <!-- We need at least C# 7.1 -->
     <LangVersion>latest</LangVersion>
   </PropertyGroup>
-   
+
 </Project>

+ 10 - 7
MediaBrowser.Providers/Movies/MovieExternalIds.cs

@@ -9,17 +9,20 @@ namespace MediaBrowser.Providers.Movies
 {
     public class ImdbExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "IMDb";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.Imdb.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/title/{0}";
 
+        /// <inheritdoc />
         public bool Supports(IHasProviderIds item)
         {
             // Supports images for tv movies
-            var tvProgram = item as LiveTvProgram;
-            if (tvProgram != null && tvProgram.IsMovie)
+            if (item is LiveTvProgram tvProgram && tvProgram.IsMovie)
             {
                 return true;
             }
@@ -28,18 +31,18 @@ namespace MediaBrowser.Providers.Movies
         }
     }
 
-
     public class ImdbPersonExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "IMDb";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.Imdb.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/name/{0}";
 
-        public bool Supports(IHasProviderIds item)
-        {
-            return item is Person;
-        }
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Person;
     }
 }

+ 12 - 36
MediaBrowser.Providers/Movies/MovieMetadataService.cs

@@ -1,5 +1,4 @@
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -12,6 +11,17 @@ namespace MediaBrowser.Providers.Movies
 {
     public class MovieMetadataService : MetadataService<Movie, MovieInfo>
     {
+        public MovieMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        {
+        }
+
+        /// <inheritdoc />
         protected override bool IsFullLocalMetadata(Movie item)
         {
             if (string.IsNullOrWhiteSpace(item.Overview))
@@ -25,6 +35,7 @@ namespace MediaBrowser.Providers.Movies
             return base.IsFullLocalMetadata(item);
         }
 
+        /// <inheritdoc />
         protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
             ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
@@ -37,40 +48,5 @@ namespace MediaBrowser.Providers.Movies
                 targetItem.CollectionName = sourceItem.CollectionName;
             }
         }
-
-        public MovieMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
-        {
-        }
     }
-
-    public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
-    {
-        protected override bool IsFullLocalMetadata(Trailer item)
-        {
-            if (string.IsNullOrWhiteSpace(item.Overview))
-            {
-                return false;
-            }
-            if (!item.ProductionYear.HasValue)
-            {
-                return false;
-            }
-            return base.IsFullLocalMetadata(item);
-        }
-
-        protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
-        {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
-
-            if (replaceData || target.Item.TrailerTypes.Length == 0)
-            {
-                target.Item.TrailerTypes = source.Item.TrailerTypes;
-            }
-        }
-
-        public TrailerMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
-        {
-        }
-    }
-
 }

+ 49 - 0
MediaBrowser.Providers/Movies/TrailerMetadataService.cs

@@ -0,0 +1,49 @@
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Providers.Manager;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Movies
+{
+    public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo>
+    {
+        public TrailerMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
+        {
+        }
+
+        /// <inheritdoc />
+        protected override bool IsFullLocalMetadata(Trailer item)
+        {
+            if (string.IsNullOrWhiteSpace(item.Overview))
+            {
+                return false;
+            }
+            if (!item.ProductionYear.HasValue)
+            {
+                return false;
+            }
+            return base.IsFullLocalMetadata(item);
+        }
+
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+
+            if (replaceData || target.Item.TrailerTypes.Length == 0)
+            {
+                target.Item.TrailerTypes = source.Item.TrailerTypes;
+            }
+        }
+    }
+}

+ 8 - 14
MediaBrowser.Providers/Music/AlbumMetadataService.cs

@@ -20,9 +20,8 @@ namespace MediaBrowser.Providers.Music
             ILogger logger,
             IProviderManager providerManager,
             IFileSystem fileSystem,
-            IUserDataManager userDataManager,
             ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
         }
 
@@ -37,10 +36,7 @@ namespace MediaBrowser.Providers.Music
 
         /// <inheritdoc />
         protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item)
-        {
-            return item.GetRecursiveChildren(i => i is Audio)
-                        .ToList();
-        }
+            => item.GetRecursiveChildren(i => i is Audio);
 
         /// <inheritdoc />
         protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType)
@@ -53,20 +49,18 @@ namespace MediaBrowser.Providers.Music
                 {
                     var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i));
 
-                    if (!string.IsNullOrEmpty(name))
+                    if (!string.IsNullOrEmpty(name)
+                        && !string.Equals(item.Name, name, StringComparison.Ordinal))
                     {
-                        if (!string.Equals(item.Name, name, StringComparison.Ordinal))
-                        {
-                            item.Name = name;
-                            updateType = updateType | ItemUpdateType.MetadataEdit;
-                        }
+                        item.Name = name;
+                        updateType |= ItemUpdateType.MetadataEdit;
                     }
                 }
 
                 var songs = children.Cast<Audio>().ToArray();
 
-                updateType = updateType | SetAlbumArtistFromSongs(item, songs);
-                updateType = updateType | SetArtistsFromSongs(item, songs);
+                updateType |= SetAlbumArtistFromSongs(item, songs);
+                updateType |= SetArtistsFromSongs(item, songs);
             }
 
             return updateType;

+ 20 - 11
MediaBrowser.Providers/Music/ArtistMetadataService.cs

@@ -13,26 +13,35 @@ namespace MediaBrowser.Providers.Music
 {
     public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo>
     {
-        protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
+        public ArtistMetadataService(
+            IServerConfigurationManager serverConfigurationManager,
+            ILogger logger,
+            IProviderManager providerManager,
+            IFileSystem fileSystem,
+            ILibraryManager libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
-            return item.IsAccessedByName ?
-                item.GetTaggedItems(new InternalItemsQuery
-                {
-                    Recursive = true,
-                    IsFolder = false
-                }) :
-                item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
         }
 
+        /// <inheritdoc />
         protected override bool EnableUpdatingGenresFromChildren => true;
 
-        protected override void MergeData(MetadataResult<MusicArtist> source, MetadataResult<MusicArtist> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
+        /// <inheritdoc />
+        protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item)
         {
-            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+            return item.IsAccessedByName
+                ? item.GetTaggedItems(new InternalItemsQuery
+                    {
+                        Recursive = true,
+                        IsFolder = false
+                    })
+                : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
         }
 
-        public ArtistMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+        /// <inheritdoc />
+        protected override void MergeData(MetadataResult<MusicArtist> source, MetadataResult<MusicArtist> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings)
         {
+            ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
         }
     }
 }

+ 12 - 8
MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs

@@ -25,6 +25,14 @@ namespace MediaBrowser.Providers.Music
             _json = json;
         }
 
+        /// <inheritdoc />
+        public string Name => "TheAudioDB";
+
+        /// <inheritdoc />
+        // After embedded and fanart
+        public int Order => 2;
+
+        /// <inheritdoc />
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return new List<ImageType>
@@ -34,6 +42,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
+        /// <inheritdoc />
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
         {
             var id = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup);
@@ -82,6 +91,7 @@ namespace MediaBrowser.Providers.Music
             return list;
         }
 
+        /// <inheritdoc />
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClient.GetResponse(new HttpRequestOptions
@@ -91,13 +101,7 @@ namespace MediaBrowser.Providers.Music
             });
         }
 
-        public string Name => "TheAudioDB";
-        // After embedded and fanart
-        public int Order => 2;
-
-        public bool Supports(BaseItem item)
-        {
-            return item is MusicAlbum;
-        }
+        /// <inheritdoc />
+        public bool Supports(BaseItem item) => item is MusicAlbum;
     }
 }

+ 23 - 22
MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
@@ -26,8 +27,6 @@ namespace MediaBrowser.Providers.Music
 
         public static AudioDbAlbumProvider Current;
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
         public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json)
         {
             _config = config;
@@ -38,11 +37,18 @@ namespace MediaBrowser.Providers.Music
             Current = this;
         }
 
+        /// <inheritdoc />
+        public string Name => "TheAudioDB";
+
+        /// <inheritdoc />
+        // After music brainz
+        public int Order => 1;
+
+        /// <inheritdoc />
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken)
-        {
-            return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
-        }
+            => Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
 
+        /// <inheritdoc />
         public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
         {
             var result = new MetadataResult<MusicAlbum>();
@@ -77,7 +83,7 @@ namespace MediaBrowser.Providers.Music
 
             if (!string.IsNullOrEmpty(result.intYearReleased))
             {
-                item.ProductionYear = int.Parse(result.intYearReleased, _usCulture);
+                item.ProductionYear = int.Parse(result.intYearReleased, CultureInfo.InvariantCulture);
             }
 
             if (!string.IsNullOrEmpty(result.strGenre))
@@ -126,8 +132,6 @@ namespace MediaBrowser.Providers.Music
             item.Overview = (overview ?? string.Empty).StripHtml();
         }
 
-        public string Name => "TheAudioDB";
-
         internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken)
         {
             var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId);
@@ -155,20 +159,18 @@ namespace MediaBrowser.Providers.Music
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
-            using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken
+            using (var httpResponse = await _httpClient.SendAsync(
+                new HttpRequestOptions
+                {
+                    Url = url,
+                    CancellationToken = cancellationToken
 
-            }, "GET").ConfigureAwait(false))
+                },
+                "GET").ConfigureAwait(false))
+            using (var response = httpResponse.Content)
+            using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
             {
-                using (var response = httpResponse.Content)
-                {
-                    using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
-                    {
-                        await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
-                    }
-                }
+                await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
             }
         }
 
@@ -192,8 +194,6 @@ namespace MediaBrowser.Providers.Music
 
             return Path.Combine(dataPath, "album.json");
         }
-        // After music brainz
-        public int Order => 1;
 
         public class Album
         {
@@ -242,6 +242,7 @@ namespace MediaBrowser.Providers.Music
             public List<Album> album { get; set; }
         }
 
+        /// <inheritdoc />
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();

+ 11 - 8
MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs

@@ -25,6 +25,14 @@ namespace MediaBrowser.Providers.Music
             _httpClient = httpClient;
         }
 
+        /// <inheritdoc />
+        public string Name => "TheAudioDB";
+
+        /// <inheritdoc />
+        // After fanart
+        public int Order => 1;
+
+        /// <inheritdoc />
         public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
         {
             return new List<ImageType>
@@ -36,6 +44,7 @@ namespace MediaBrowser.Providers.Music
             };
         }
 
+        /// <inheritdoc />
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
         {
             var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist);
@@ -133,13 +142,7 @@ namespace MediaBrowser.Providers.Music
             });
         }
 
-        public string Name => "TheAudioDB";
-
-        public bool Supports(BaseItem item)
-        {
-            return item is MusicArtist;
-        }
-        // After fanart
-        public int Order => 1;
+        /// <inheritdoc />
+        public bool Supports(BaseItem item) => item is MusicArtist;
     }
 }

+ 29 - 35
MediaBrowser.Providers/Music/AudioDbArtistProvider.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
@@ -37,11 +38,18 @@ namespace MediaBrowser.Providers.Music
             Current = this;
         }
 
+        /// <inheritdoc />
+        public string Name => "TheAudioDB";
+
+        /// <inheritdoc />
+        // After musicbrainz
+        public int Order => 1;
+
+        /// <inheritdoc />
         public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
-        {
-            return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
-        }
+            => Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
 
+        /// <inheritdoc />
         public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
         {
             var result = new MetadataResult<MusicArtist>();
@@ -114,20 +122,16 @@ namespace MediaBrowser.Providers.Music
             item.Overview = (overview ?? string.Empty).StripHtml();
         }
 
-        public string Name => "TheAudioDB";
-
         internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken)
         {
             var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
 
             var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath);
 
-            if (fileInfo.Exists)
+            if (fileInfo.Exists
+                && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
             {
-                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
-                {
-                    return Task.CompletedTask;
-                }
+                return Task.CompletedTask;
             }
 
             return DownloadArtistInfo(musicBrainzId, cancellationToken);
@@ -141,22 +145,21 @@ namespace MediaBrowser.Providers.Music
 
             var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId);
 
-            using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions
+            using (var httpResponse = await _httpClient.SendAsync(
+                new HttpRequestOptions
+                {
+                    Url = url,
+                    CancellationToken = cancellationToken,
+                    BufferContent = true
+                },
+                "GET").ConfigureAwait(false))
+            using (var response = httpResponse.Content)
             {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = true
+                Directory.CreateDirectory(Path.GetDirectoryName(path));
 
-            }, "GET").ConfigureAwait(false))
-            {
-                using (var response = httpResponse.Content)
+                using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
                 {
-                    Directory.CreateDirectory(Path.GetDirectoryName(path));
-
-                    using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
-                    {
-                        await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
-                    }
+                    await response.CopyToAsync(xmlFileStream).ConfigureAwait(false);
                 }
             }
         }
@@ -168,11 +171,7 @@ namespace MediaBrowser.Providers.Music
         /// <param name="musicBrainzArtistId">The music brainz artist identifier.</param>
         /// <returns>System.String.</returns>
         private static string GetArtistDataPath(IApplicationPaths appPaths, string musicBrainzArtistId)
-        {
-            var dataPath = Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId);
-
-            return dataPath;
-        }
+            => Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId);
 
         /// <summary>
         /// Gets the artist data path.
@@ -180,11 +179,7 @@ namespace MediaBrowser.Providers.Music
         /// <param name="appPaths">The application paths.</param>
         /// <returns>System.String.</returns>
         private static string GetArtistDataPath(IApplicationPaths appPaths)
-        {
-            var dataPath = Path.Combine(appPaths.CachePath, "audiodb-artist");
-
-            return dataPath;
-        }
+            => Path.Combine(appPaths.CachePath, "audiodb-artist");
 
         internal static string GetArtistInfoPath(IApplicationPaths appPaths, string musicBrainzArtistId)
         {
@@ -242,9 +237,8 @@ namespace MediaBrowser.Providers.Music
         {
             public List<Artist> artists { get; set; }
         }
-        // After musicbrainz
-        public int Order => 1;
 
+        /// <inheritdoc />
         public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             throw new NotImplementedException();

+ 20 - 15
MediaBrowser.Providers/Music/AudioDbExternalIds.cs

@@ -6,58 +6,63 @@ namespace MediaBrowser.Providers.Music
 {
     public class AudioDbAlbumExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "TheAudioDb";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.AudioDbAlbum.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
+        /// <inheritdoc />
         public bool Supports(IHasProviderIds item)
-        {
-            return item is MusicAlbum;
-        }
+            => item is MusicAlbum;
     }
 
     public class AudioDbOtherAlbumExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "TheAudioDb Album";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.AudioDbAlbum.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
-        public bool Supports(IHasProviderIds item)
-        {
-            return item is Audio;
-        }
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is Audio;
     }
 
     public class AudioDbArtistExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "TheAudioDb";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.AudioDbArtist.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
 
-        public bool Supports(IHasProviderIds item)
-        {
-            return item is MusicArtist;
-        }
+        /// <inheritdoc />
+        public bool Supports(IHasProviderIds item) => item is MusicArtist;
     }
 
     public class AudioDbOtherArtistExternalId : IExternalId
     {
+        /// <inheritdoc />
         public string Name => "TheAudioDb Artist";
 
+        /// <inheritdoc />
         public string Key => MetadataProviders.AudioDbArtist.ToString();
 
+        /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
 
+        /// <inheritdoc />
         public bool Supports(IHasProviderIds item)
-        {
-            return item is Audio || item is MusicAlbum;
-        }
+            => item is Audio || item is MusicAlbum;
     }
-
 }

+ 1 - 2
MediaBrowser.Providers/Music/AudioMetadataService.cs

@@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Music
             ILogger logger,
             IProviderManager providerManager,
             IFileSystem fileSystem,
-            IUserDataManager userDataManager,
             ILibraryManager libraryManager)
-            : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager)
+            : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
         {
         }
 

Some files were not shown because too many files changed in this diff