浏览代码

Merge branch 'master' into installationmanager

Bond-009 5 年之前
父节点
当前提交
983d38a43b
共有 62 个文件被更改,包括 887 次插入520 次删除
  1. 1 1
      .ci/azure-pipelines.yml
  2. 59 8
      .copr/Makefile
  3. 1 0
      .github/stale.yml
  4. 3 0
      .gitignore
  5. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  6. 1 6
      Emby.Drawing/Emby.Drawing.csproj
  7. 2 1
      Emby.Naming/Emby.Naming.csproj
  8. 1 1
      Emby.Notifications/Emby.Notifications.csproj
  9. 1 1
      Emby.Photos/Emby.Photos.csproj
  10. 20 5
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  11. 6 10
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  12. 11 6
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  13. 5 0
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  14. 2 2
      Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
  15. 2 2
      Emby.Server.Implementations/Library/UserManager.cs
  16. 4 2
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  17. 19 19
      Emby.Server.Implementations/Localization/Core/hu.json
  18. 1 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  19. 11 5
      Emby.Server.Implementations/Networking/NetworkManager.cs
  20. 47 44
      Emby.Server.Implementations/Updates/InstallationManager.cs
  21. 1 1
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  22. 4 5
      Jellyfin.Server/Jellyfin.Server.csproj
  23. 4 3
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  24. 1 1
      MediaBrowser.Api/MediaBrowser.Api.csproj
  25. 0 6
      MediaBrowser.Api/PackageService.cs
  26. 7 1
      MediaBrowser.Api/Playback/BaseStreamingService.cs
  27. 70 40
      MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs
  28. 14 8
      MediaBrowser.Common/Cryptography/PasswordHash.cs
  29. 0 48
      MediaBrowser.Common/Extensions/CollectionExtensions.cs
  30. 26 0
      MediaBrowser.Common/Extensions/CopyToExtensions.cs
  31. 94 0
      MediaBrowser.Common/Hex.cs
  32. 0 24
      MediaBrowser.Common/HexHelper.cs
  33. 2 2
      MediaBrowser.Common/MediaBrowser.Common.csproj
  34. 1 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  35. 2 1
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  36. 1 1
      MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
  37. 2 2
      MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
  38. 2 2
      MediaBrowser.Model/Globalization/ILocalizationManager.cs
  39. 3 2
      MediaBrowser.Model/MediaBrowser.Model.csproj
  40. 1 1
      MediaBrowser.Model/Net/SocketReceiveResult.cs
  41. 4 4
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  42. 59 59
      MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs
  43. 1 1
      MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj
  44. 1 1
      MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
  45. 1 1
      Mono.Nat/Mono.Nat.csproj
  46. 1 1
      RSSDP/RSSDP.csproj
  47. 45 0
      benches/Jellyfin.Common.Benches/HexDecodeBenches.cs
  48. 32 0
      benches/Jellyfin.Common.Benches/HexEncodeBenches.cs
  49. 16 0
      benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj
  50. 14 0
      benches/Jellyfin.Common.Benches/Program.cs
  51. 5 6
      deployment/centos-package-x64/Dockerfile
  52. 2 69
      deployment/centos-package-x64/docker-build.sh
  53. 2 6
      deployment/fedora-package-x64/Dockerfile
  54. 2 67
      deployment/fedora-package-x64/docker-build.sh
  55. 39 20
      deployment/fedora-package-x64/pkg-src/jellyfin.spec
  56. 22 0
      deployment/windows/build-jellyfin.ps1
  57. 12 0
      deployment/windows/dialogs/setuptype.nsddef
  58. 50 0
      deployment/windows/dialogs/setuptype.nsdinc
  59. 122 18
      deployment/windows/jellyfin.nsi
  60. 2 0
      jellyfin.ruleset
  61. 19 0
      tests/Jellyfin.Common.Tests/HexTests.cs
  62. 3 3
      tests/Jellyfin.Common.Tests/PasswordHashTests.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

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

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

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

+ 2 - 2
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)

+ 4 - 2
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -2304,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);

+ 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",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -5,7 +5,7 @@
     "Artists": "艺术家",
     "AuthenticationSucceededWithUserName": "{0} 认证成功",
     "Books": "书籍",
-    "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相机图像",
+    "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相",
     "Channels": "频道",
     "ChapterNameValue": "章节 {0}",
     "Collections": "合集",

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

+ 47 - 44
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -19,52 +19,18 @@ 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
 {
     /// <summary>
-    /// Manages all install, uninstall and update operations (both plugins and system)
+    /// Manages all install, uninstall and update operations (both plugins and system).
     /// </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 +45,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 +85,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;
+
         /// <inheritdoc />
         public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
         {
@@ -225,7 +228,7 @@ namespace Emby.Server.Implementations.Updates
             var tuple = (installationInfo, innerCancellationTokenSource);
 
             // Add it to the in-progress list
-            lock (_currentInstallations)
+            lock (_currentInstallationsLock)
             {
                 _currentInstallations.Add(tuple);
             }
@@ -244,7 +247,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
 
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -255,7 +258,7 @@ namespace Emby.Server.Implementations.Updates
             }
             catch (OperationCanceledException)
             {
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -270,7 +273,7 @@ namespace Emby.Server.Implementations.Updates
             {
                 _logger.LogError(ex, "Package installation failed");
 
-                lock (_currentInstallations)
+                lock (_currentInstallationsLock)
                 {
                     _currentInstallations.Remove(tuple);
                 }
@@ -334,7 +337,7 @@ namespace Emby.Server.Implementations.Updates
             // Always override the passed-in target (which is a file) and figure it out again
             string targetDir = Path.Combine(_appPaths.PluginsPath, package.name);
 
-// CA5351: Do Not Use Broken Cryptographic Algorithms
+            // CA5351: Do Not Use Broken Cryptographic Algorithms
 #pragma warning disable CA5351
             using (var res = await _httpClient.SendAsync(
                 new HttpRequestOptions
@@ -350,7 +353,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(
@@ -430,7 +433,7 @@ namespace Emby.Server.Implementations.Updates
         /// <inheritdoc/>
         public bool CancelInstallation(Guid id)
         {
-            lock (_currentInstallations)
+            lock (_currentInstallationsLock)
             {
                 var install = _currentInstallations.Find(x => x.info.Id == id);
                 if (install == default((InstallationInfo, CancellationTokenSource)))
@@ -459,7 +462,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" />

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

+ 0 - 6
MediaBrowser.Api/PackageService.cs

@@ -126,12 +126,6 @@ namespace MediaBrowser.Api
             _appHost = appHost;
         }
 
-        /// <summary>
-        /// Gets the specified request.
-        /// </summary>
-        /// <param name="request">The request.</param>
-        /// <returns>System.Object.</returns>
-        ///
         /// <summary>
         /// Gets the specified request.
         /// </summary>

+ 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();
         }
     }

+ 14 - 8
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
 {
@@ -97,12 +96,19 @@ namespace MediaBrowser.Common.Cryptography
                 }
             }
 
+            byte[] hash;
+            byte[] salt;
             // Check if the string also contains a salt
-            byte[] salt = splitted.Length - index == 2
-                ? FromHexString(splitted[index++])
-                : Array.Empty<byte>();
-
-            byte[] hash = FromHexString(splitted[index]);
+            if (splitted.Length - index == 2)
+            {
+                salt = Hex.Decode(splitted[index++]);
+                hash = Hex.Decode(splitted[index++]);
+            }
+            else
+            {
+                salt = Array.Empty<byte>();
+                hash = Hex.Decode(splitted[index++]);
+            }
 
             return new PasswordHash(id, hash, salt, parameters);
         }
@@ -138,11 +144,11 @@ namespace MediaBrowser.Common.Cryptography
             if (Salt.Length != 0)
             {
                 str.Append('$')
-                    .Append(ToHexString(Salt));
+                    .Append(Hex.Encode(Salt, false));
             }
 
             return str.Append('$')
-                .Append(ToHexString(Hash)).ToString();
+                .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/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";
                 }

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

+ 2 - 2
MediaBrowser.Model/Globalization/ILocalizationManager.cs

@@ -64,10 +64,10 @@ namespace MediaBrowser.Model.Globalization
         bool HasUnicodeCategory(string value, UnicodeCategory category);
 
         /// <summary>
-        /// Returns the correct <see cref="Cultureinfo" /> for the given language.
+        /// Returns the correct <see cref="CultureInfo" /> for the given language.
         /// </summary>
         /// <param name="language">The language.</param>
-        /// <returns>The correct <see cref="Cultureinfo" /> for the given language.</returns>
+        /// <returns>The correct <see cref="CultureInfo" /> for the given language.</returns>
         CultureDto FindLanguageInfo(string language);
     }
 }

+ 3 - 2
MediaBrowser.Model/MediaBrowser.Model.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <Authors>Jellyfin Contributors</Authors>
@@ -15,7 +15,8 @@
 
   <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.Globalization" Version="4.3.0" />
     <PackageReference Include="System.Text.Json" Version="4.6.0" />
   </ItemGroup>
 

+ 1 - 1
MediaBrowser.Model/Net/SocketReceiveResult.cs

@@ -18,7 +18,7 @@ namespace MediaBrowser.Model.Net
         public int ReceivedBytes { get; set; }
 
         /// <summary>
-        /// The <see cref="IpEndPointInfo"/> the data was received from.
+        /// The <see cref="IPEndPoint"/> the data was received from.
         /// </summary>
         public IPEndPoint RemoteEndPoint { get; set; }
         public IPAddress LocalIPAddress { get; set; }

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

+ 59 - 59
MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs

@@ -2,64 +2,64 @@ namespace MediaBrowser.Providers.Tmdb.Models.Search
 {
     public class MovieResult
     {
-            /// <summary>
-            /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult.
-            /// </summary>
-            /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
-            public bool Adult { get; set; }
-            /// <summary>
-            /// Gets or sets the backdrop_path.
-            /// </summary>
-            /// <value>The backdrop_path.</value>
-            public string Backdrop_Path { get; set; }
-            /// <summary>
-            /// Gets or sets the id.
-            /// </summary>
-            /// <value>The id.</value>
-            public int Id { get; set; }
-            /// <summary>
-            /// Gets or sets the original_title.
-            /// </summary>
-            /// <value>The original_title.</value>
-            public string Original_Title { get; set; }
-            /// <summary>
-            /// Gets or sets the original_name.
-            /// </summary>
-            /// <value>The original_name.</value>
-            public string Original_Name { get; set; }
-            /// <summary>
-            /// Gets or sets the release_date.
-            /// </summary>
-            /// <value>The release_date.</value>
-            public string Release_Date { get; set; }
-            /// <summary>
-            /// Gets or sets the poster_path.
-            /// </summary>
-            /// <value>The poster_path.</value>
-            public string Poster_Path { get; set; }
-            /// <summary>
-            /// Gets or sets the popularity.
-            /// </summary>
-            /// <value>The popularity.</value>
-            public double Popularity { get; set; }
-            /// <summary>
-            /// Gets or sets the title.
-            /// </summary>
-            /// <value>The title.</value>
-            public string Title { get; set; }
-            /// <summary>
-            /// Gets or sets the vote_average.
-            /// </summary>
-            /// <value>The vote_average.</value>
-            public double Vote_Average { get; set; }
-            /// <summary>
-            /// For collection search results
-            /// </summary>
-            public string Name { get; set; }
-            /// <summary>
-            /// Gets or sets the vote_count.
-            /// </summary>
-            /// <value>The vote_count.</value>
-            public int Vote_Count { get; set; }
+        /// <summary>
+        /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult.
+        /// </summary>
+        /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
+        public bool Adult { get; set; }
+        /// <summary>
+        /// Gets or sets the backdrop_path.
+        /// </summary>
+        /// <value>The backdrop_path.</value>
+        public string Backdrop_Path { get; set; }
+        /// <summary>
+        /// Gets or sets the id.
+        /// </summary>
+        /// <value>The id.</value>
+        public int Id { get; set; }
+        /// <summary>
+        /// Gets or sets the original_title.
+        /// </summary>
+        /// <value>The original_title.</value>
+        public string Original_Title { get; set; }
+        /// <summary>
+        /// Gets or sets the original_name.
+        /// </summary>
+        /// <value>The original_name.</value>
+        public string Original_Name { get; set; }
+        /// <summary>
+        /// Gets or sets the release_date.
+        /// </summary>
+        /// <value>The release_date.</value>
+        public string Release_Date { get; set; }
+        /// <summary>
+        /// Gets or sets the poster_path.
+        /// </summary>
+        /// <value>The poster_path.</value>
+        public string Poster_Path { get; set; }
+        /// <summary>
+        /// Gets or sets the popularity.
+        /// </summary>
+        /// <value>The popularity.</value>
+        public double Popularity { get; set; }
+        /// <summary>
+        /// Gets or sets the title.
+        /// </summary>
+        /// <value>The title.</value>
+        public string Title { get; set; }
+        /// <summary>
+        /// Gets or sets the vote_average.
+        /// </summary>
+        /// <value>The vote_average.</value>
+        public double Vote_Average { get; set; }
+        /// <summary>
+        /// For collection search results
+        /// </summary>
+        public string Name { get; set; }
+        /// <summary>
+        /// Gets or sets the vote_count.
+        /// </summary>
+        /// <value>The vote_count.</value>
+        public int Vote_Count { get; set; }
     }
 }

+ 1 - 1
MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj

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

+ 1 - 1
MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj

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

+ 1 - 1
Mono.Nat/Mono.Nat.csproj

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

+ 1 - 1
RSSDP/RSSDP.csproj

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

+ 45 - 0
benches/Jellyfin.Common.Benches/HexDecodeBenches.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Globalization;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using MediaBrowser.Common;
+
+namespace Jellyfin.Common.Benches
+{
+    [MemoryDiagnoser]
+    public class HexDecodeBenches
+    {
+        private string _data;
+
+        [Params(0, 10, 100, 1000, 10000, 1000000)]
+        public int N { get; set; }
+
+        [GlobalSetup]
+        public void GlobalSetup()
+        {
+            var bytes = new byte[N];
+            new Random(42).NextBytes(bytes);
+            _data = Hex.Encode(bytes);
+        }
+
+        [Benchmark]
+        public byte[] Decode() => Hex.Decode(_data);
+
+        [Benchmark]
+        public byte[] DecodeSubString() => DecodeSubString(_data);
+
+        private static byte[] DecodeSubString(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;
+        }
+    }
+}

+ 32 - 0
benches/Jellyfin.Common.Benches/HexEncodeBenches.cs

@@ -0,0 +1,32 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Running;
+using MediaBrowser.Common;
+
+namespace Jellyfin.Common.Benches
+{
+    [MemoryDiagnoser]
+    public class HexEncodeBenches
+    {
+        private byte[] _data;
+
+        [Params(0, 10, 100, 1000, 10000, 1000000)]
+        public int N { get; set; }
+
+        [GlobalSetup]
+        public void GlobalSetup()
+        {
+            _data = new byte[N];
+            new Random(42).NextBytes(_data);
+        }
+
+        [Benchmark]
+        public string HexEncode() => Hex.Encode(_data);
+
+        [Benchmark]
+        public string BitConverterToString() => BitConverter.ToString(_data);
+
+        [Benchmark]
+        public string BitConverterToStringWithReplace() => BitConverter.ToString(_data).Replace("-", "");
+    }
+}

+ 16 - 0
benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj

@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" />
+  </ItemGroup>
+
+</Project>

+ 14 - 0
benches/Jellyfin.Common.Benches/Program.cs

@@ -0,0 +1,14 @@
+using System;
+using BenchmarkDotNet.Running;
+
+namespace Jellyfin.Common.Benches
+{
+    public static class Program
+    {
+        public static void Main(string[] args)
+        {
+            _ = BenchmarkRunner.Run<HexEncodeBenches>();
+            _ = BenchmarkRunner.Run<HexDecodeBenches>();
+        }
+    }
+}

+ 5 - 6
deployment/centos-package-x64/Dockerfile

@@ -3,7 +3,7 @@ FROM centos:7
 ARG SOURCE_DIR=/jellyfin
 ARG PLATFORM_DIR=/jellyfin/deployment/centos-package-x64
 ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=2.2
+ARG SDK_VERSION=3.0
 # Docker run environment
 ENV SOURCE_DIR=/jellyfin
 ENV ARTIFACT_DIR=/dist
@@ -13,13 +13,12 @@ RUN yum update -y \
  && yum install -y epel-release
 
 # Install build dependencies
-RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel wget git
+RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git
 
 # Install recent NodeJS and Yarn
-RUN wget -O- https://raw.githubusercontent.com/creationix/nvm/v0.35.0/install.sh | /bin/bash \
- && source "$HOME/.nvm/nvm.sh" \
- && nvm install v8 \
- && npm install -g yarn
+RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
+ && rpm -i https://rpm.nodesource.com/pub_8.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \
+ && yum install -y yarn
 
 # Install DotNET SDK
 RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \

+ 2 - 69
deployment/centos-package-x64/docker-build.sh

@@ -8,76 +8,9 @@ set -o xtrace
 # Move to source directory
 pushd ${SOURCE_DIR}
 
-VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
-    checkout -b origin/${web_branch}
-fi
-source "$HOME/.nvm/nvm.sh"
-nvm use v8
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Create RPM source archive
-GNU_TAR=1
-echo "Bundling all sources for RPM build."
-tar \
---transform "s,^\.,jellyfin-${VERSION}," \
---exclude='.git*' \
---exclude='**/.git' \
---exclude='**/.hg' \
---exclude='**/.vs' \
---exclude='**/.vscode' \
---exclude='deployment' \
---exclude='**/bin' \
---exclude='**/obj' \
---exclude='**/.nuget' \
---exclude='*.deb' \
---exclude='*.rpm' \
--czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \
--C ${SOURCE_DIR} ./ || GNU_TAR=0
-
-if [ $GNU_TAR -eq 0 ]; then
-    echo "The installed tar binary did not support --transform. Using workaround."
-    package_temporary_dir="$( mktemp -d )"
-    mkdir -p "${package_temporary_dir}/jellyfin"
-    # Not GNU tar
-    tar \
-    --exclude='.git*' \
-    --exclude='**/.git' \
-    --exclude='**/.hg' \
-    --exclude='**/.vs' \
-    --exclude='**/.vscode' \
-    --exclude='deployment' \
-    --exclude='**/bin' \
-    --exclude='**/obj' \
-    --exclude='**/.nuget' \
-    --exclude='*.deb' \
-    --exclude='*.rpm' \
-    -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
-    -C ${SOURCE_DIR} ./
-    echo "Extracting filtered package."
-    mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}"
-    tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
-    echo "Removing filtered package."
-    rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
-    echo "Repackaging package into final tarball."
-    tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
-    rm -rf ${package_temporary_dir}
-fi
-
 # Build RPM
-spectool -g -R SPECS/jellyfin.spec
-rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
-rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS
+rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
 
 # Move the artifacts out
 mkdir -p ${ARTIFACT_DIR}/rpm

+ 2 - 6
deployment/fedora-package-x64/Dockerfile

@@ -3,7 +3,7 @@ FROM fedora:29
 ARG SOURCE_DIR=/jellyfin
 ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64
 ARG ARTIFACT_DIR=/dist
-ARG SDK_VERSION=2.2
+ARG SDK_VERSION=3.0
 # Docker run environment
 ENV SOURCE_DIR=/jellyfin
 ENV ARTIFACT_DIR=/dist
@@ -12,17 +12,13 @@ ENV ARTIFACT_DIR=/dist
 RUN dnf update -y
 
 # Install build dependencies
-RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs wget git
+RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn
 
 # Install DotNET SDK
 RUN dnf copr enable -y @dotnet-sig/dotnet \
  && rpmdev-setuptree \
  && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION}
 
-# Install yarn package manager
-RUN wget -q -O /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \
- && dnf install -y yarn
-
 # Create symlinks and directories
 RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \
  && mkdir -p ${SOURCE_DIR}/SPECS \

+ 2 - 67
deployment/fedora-package-x64/docker-build.sh

@@ -8,74 +8,9 @@ set -o xtrace
 # Move to source directory
 pushd ${SOURCE_DIR}
 
-VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )"
-
-# Clone down and build Web frontend
-web_build_dir="$( mktemp -d )"
-web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web"
-git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/
-pushd ${web_build_dir}
-if [[ -n ${web_branch} ]]; then
-    checkout -b origin/${web_branch}
-fi
-yarn install
-mkdir -p ${web_target}
-mv dist/* ${web_target}/
-popd
-rm -rf ${web_build_dir}
-
-# Create RPM source archive
-GNU_TAR=1
-echo "Bundling all sources for RPM build."
-tar \
---transform "s,^\.,jellyfin-${VERSION}," \
---exclude='.git*' \
---exclude='**/.git' \
---exclude='**/.hg' \
---exclude='**/.vs' \
---exclude='**/.vscode' \
---exclude='deployment' \
---exclude='**/bin' \
---exclude='**/obj' \
---exclude='**/.nuget' \
---exclude='*.deb' \
---exclude='*.rpm' \
--czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \
--C ${SOURCE_DIR} ./ || GNU_TAR=0
-
-if [ $GNU_TAR -eq 0 ]; then
-    echo "The installed tar binary did not support --transform. Using workaround."
-    package_temporary_dir="$( mktemp -d )"
-    mkdir -p "${package_temporary_dir}/jellyfin"
-    # Not GNU tar
-    tar \
-    --exclude='.git*' \
-    --exclude='**/.git' \
-    --exclude='**/.hg' \
-    --exclude='**/.vs' \
-    --exclude='**/.vscode' \
-    --exclude='deployment' \
-    --exclude='**/bin' \
-    --exclude='**/obj' \
-    --exclude='**/.nuget' \
-    --exclude='*.deb' \
-    --exclude='*.rpm' \
-    -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \
-    -C ${SOURCE_DIR} ./
-    echo "Extracting filtered package."
-    mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}"
-    tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}"
-    echo "Removing filtered package."
-    rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz"
-    echo "Repackaging package into final tarball."
-    tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}"
-    rm -rf ${package_temporary_dir}
-fi
-
 # Build RPM
-spectool -g -R SPECS/jellyfin.spec
-rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
-rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/"
+make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS
+rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
 
 # Move the artifacts out
 mkdir -p ${ARTIFACT_DIR}/rpm

+ 39 - 20
deployment/fedora-package-x64/pkg-src/jellyfin.spec

@@ -12,28 +12,36 @@ Release:        1%{?dist}
 Summary:        The Free Software Media Browser
 License:        GPLv2
 URL:            https://jellyfin.media
-Source0:        %{name}-%{version}.tar.gz
-Source1:        jellyfin.service
-Source2:        jellyfin.env
-Source3:        jellyfin.sudoers
-Source4:        restart.sh
-Source5:        jellyfin.override.conf
-Source6:        jellyfin-firewalld.xml
+# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
+Source0:        https://github.com/%{name}/%{name}/archive/%{name}-%{version}.tar.gz
+# Jellyfin Webinterface downloaded by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
+Source1:        https://github.com/%{name}/%{name}-web/archive/%{name}-web-%{version}.tar.gz
+Source11:       jellyfin.service
+Source12:       jellyfin.env
+Source13:       jellyfin.sudoers
+Source14:       restart.sh
+Source15:       jellyfin.override.conf
+Source16:       jellyfin-firewalld.xml
 
 %{?systemd_requires}
 BuildRequires:  systemd
 Requires(pre):  shadow-utils
 BuildRequires:  libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel
+%if 0%{?fedora}
+BuildRequires:  nodejs-yarn
+%else
+# Requirements not packaged in main repos
+# From https://rpm.nodesource.com/pub_8.x/el/7/x86_64/
+BuildRequires:  nodejs >= 8 yarn
+%endif
 Requires:       libcurl, fontconfig, freetype, openssl, glibc libicu
 # Requirements not packaged in main repos
-# COPR @dotnet-sig/dotnet
-BuildRequires:  dotnet-runtime-2.2, dotnet-sdk-2.2
+# COPR @dotnet-sig/dotnet or
+# https://packages.microsoft.com/rhel/7/prod/
+BuildRequires:  dotnet-runtime-3.0, dotnet-sdk-3.0
 # RPMfusion free
 Requires:       ffmpeg
 
-# Fedora has openssl1.1 which is incompatible with dotnet 
-%{?fedora:Requires: compat-openssl10}
-
 # Disable Automatic Dependency Processing
 AutoReqProv:    no
 
@@ -42,7 +50,18 @@ Jellyfin is a free software media system that puts you in control of managing an
 
 
 %prep
-%autosetup -n %{name}-%{version}
+%autosetup -n %{name}-%{version} -b 0 -b 1
+web_build_dir="$(mktemp -d)"
+web_target="$PWD/MediaBrowser.WebDashboard/jellyfin-web"
+pushd ../jellyfin-web-%{version} || pushd ../jellyfin-web-master
+%if 0%{?fedora}
+nodejs-yarn install
+%else
+yarn install
+%endif
+mkdir -p ${web_target}
+mv dist/* ${web_target}/
+popd
 
 %build
 
@@ -52,7 +71,7 @@ export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \
     "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE
-%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
+%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json
 %{__mkdir} -p %{buildroot}%{_bindir}
 tee %{buildroot}%{_bindir}/jellyfin << EOF
@@ -64,11 +83,11 @@ EOF
 %{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
 %{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin
 
-%{__install} -D -m 0644 %{SOURCE1} %{buildroot}%{_unitdir}/%{name}.service
-%{__install} -D -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
-%{__install} -D -m 0600 %{SOURCE3} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers
-%{__install} -D -m 0755 %{SOURCE4} %{buildroot}%{_libexecdir}/%{name}/restart.sh
-%{__install} -D -m 0644 %{SOURCE6} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml
+%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/%{name}.service
+%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
+%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers
+%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/%{name}/restart.sh
+%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml
 
 %files
 %{_libdir}/%{name}/jellyfin-web/*
@@ -80,7 +99,7 @@ EOF
 %{_libdir}/%{name}/createdump
 # Needs 755 else only root can run it since binary build by dotnet is 722
 %attr(755,root,root) %{_libdir}/%{name}/jellyfin
-%{_libdir}/%{name}/sosdocsunix.txt
+%{_libdir}/%{name}/SOS_README.md
 %{_unitdir}/%{name}.service
 %{_libexecdir}/%{name}/restart.sh
 %{_prefix}/lib/firewalld/services/%{name}.xml

+ 22 - 0
deployment/windows/build-jellyfin.ps1

@@ -8,6 +8,7 @@ param(
     [switch]$GenerateZip,
     [string]$InstallLocation = "./dist/jellyfin-win-nsis",
     [string]$UXLocation = "../jellyfin-ux",
+    [switch]$InstallTrayApp,
     [ValidateSet('Debug','Release')][string]$BuildType = 'Release',
     [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal',
     [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win',
@@ -132,6 +133,23 @@ function Cleanup-NSIS {
     Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose
     Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose
 }
+
+function Install-TrayApp {
+    param(
+        [string]$ResolvedInstallLocation,
+        [string]$Architecture
+    )
+    Write-Verbose "Checking Architecture"
+    if($Architecture -ne 'x64'){
+        Write-Warning "No builds available for your selected architecture of $Architecture"
+        Write-Warning "The tray app will not be available."
+    }else{
+        Write-Verbose "Downloading Tray App and copying to Jellyfin location"
+        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+        Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose
+    }
+}
+
 if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){
     Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture"
     Build-JellyFin
@@ -144,6 +162,10 @@ if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){
     Write-Verbose "Starting NSSM Install"
     Install-NSSM $ResolvedInstallLocation $Architecture
 }
+if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){
+    Write-Verbose "Downloading Windows Tray App"
+    Install-TrayApp $ResolvedInstallLocation $Architecture
+}
 #Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1
 #Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat
 Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE

+ 12 - 0
deployment/windows/dialogs/setuptype.nsddef

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+This file was created by NSISDialogDesigner 1.4.4.0
+http://coolsoft.altervista.org/nsisdialogdesigner
+Do not edit manually!
+-->
+<Dialog Name="setuptype" Title="Setup Type" Subtitle="Control how Jellyfin is installed.">
+  <Label Name="InstallasaServiceLabel" Location="12, 115" Size="426, 46" Text="Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." TabIndex="0" />
+  <RadioButton Name="InstallasaService" Location="12, 88" Size="426, 24" Text="Install as a Service (Advanced Users)" TabIndex="1" />
+  <Label Name="BasicInstallLabel" Location="12, 39" Size="426, 46" Text="The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." TabIndex="2" />
+  <RadioButton Name="BasicInstall" Location="12, 12" Size="426, 24" Text="Basic Install (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="3" />
+</Dialog>

+ 50 - 0
deployment/windows/dialogs/setuptype.nsdinc

@@ -0,0 +1,50 @@
+; =========================================================
+; This file was generated by NSISDialogDesigner 1.4.4.0
+; http://coolsoft.altervista.org/nsisdialogdesigner
+;
+; Do not edit it manually, use NSISDialogDesigner instead!
+; =========================================================
+
+; handle variables
+Var hCtl_setuptype
+Var hCtl_setuptype_InstallasaServiceLabel
+Var hCtl_setuptype_InstallasaService
+Var hCtl_setuptype_BasicInstallLabel
+Var hCtl_setuptype_BasicInstall
+Var hCtl_setuptype_Font1
+
+
+; dialog create function
+Function fnc_setuptype_Create
+  
+  ; custom font definitions
+  CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700"
+  
+  ; === setuptype (type: Dialog) ===
+  nsDialogs::Create 1018
+  Pop $hCtl_setuptype
+  ${If} $hCtl_setuptype == error
+    Abort
+  ${EndIf}
+  !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed."
+  
+  ; === InstallasaServiceLabel (type: Label) ===
+  ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares."
+  Pop $hCtl_setuptype_InstallasaServiceLabel
+  
+  ; === InstallasaService (type: RadioButton) ===
+  ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)"
+  Pop $hCtl_setuptype_InstallasaService
+  ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP}
+  
+  ; === BasicInstallLabel (type: Label) ===
+  ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4."
+  Pop $hCtl_setuptype_BasicInstallLabel
+  
+  ; === BasicInstall (type: RadioButton) ===
+  ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)"
+  Pop $hCtl_setuptype_BasicInstall
+  SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0
+  ${NSD_Check} $hCtl_setuptype_BasicInstall
+  
+FunctionEnd

+ 122 - 18
deployment/windows/jellyfin.nsi

@@ -16,11 +16,14 @@ ShowUninstDetails show
 ; Global variables that we'll use
     Var _JELLYFINVERSION_
     Var _JELLYFINDATADIR_
+    Var _SETUPTYPE_
     Var _INSTALLSERVICE_
     Var _SERVICESTART_
     Var _SERVICEACCOUNTTYPE_
     Var _EXISTINGINSTALLATION_
     Var _EXISTINGSERVICE_
+    Var _MAKESHORTCUTS_
+    Var _FOLDEREXISTS_
 ;
 !ifdef x64
     !define ARCH "x64"
@@ -86,7 +89,12 @@ ShowUninstDetails show
     !insertmacro MUI_PAGE_WELCOME
 ; License Page
     !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL
+
+; Setup Type Page
+    Page custom ShowSetupTypePage SetupTypePage_Config
+    
 ; Components Page
+    !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage
     !insertmacro MUI_PAGE_COMPONENTS
     !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show
     !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog
@@ -102,6 +110,7 @@ ShowUninstDetails show
     !insertmacro MUI_PAGE_DIRECTORY
 
 ; Custom Dialogs
+    !include "dialogs\setuptype.nsdinc"
     !include "dialogs\service-config.nsdinc"
     !include "dialogs\confirmation.nsdinc"
 
@@ -155,7 +164,9 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer
 
     SetOutPath "$INSTDIR"
 
+    File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico"
     File /r $%InstallLocation%\*
+    
 
 ; Write the InstallFolder, DataFolder, Network Service info into the registry for later use
     WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR"
@@ -170,7 +181,7 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer
     WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
     WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0'
     WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project"
-    WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.media/"
+    WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/"
     WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_"
     WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1
     WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1
@@ -180,12 +191,12 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer
 SectionEnd
 
 Section "Jellyfin Server Service" InstallService
-
+${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service!
     ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
     DetailPrint "Jellyfin Server service statuscode, $0"
     ${If} $0 == 0
         InstallRetry:
-        ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --datadir \"$_JELLYFINDATADIR_\"' $0
+        ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0
         ${If} $0 <> 0
             !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry
         ${EndIf}
@@ -201,7 +212,7 @@ Section "Jellyfin Server Service" InstallService
         DetailPrint "Jellyfin Server Service setting (Application), $0"
 
         ConfigureAppParametersRetry:
-        ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --datadir \"$_JELLYFINDATADIR_\"' $0
+        ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0
         ${If} $0 <> 0
             !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry
         ${EndIf}
@@ -241,6 +252,15 @@ Section "Jellyfin Server Service" InstallService
         DetailPrint "Jellyfin Server service account change, $0"
     ${EndIf}
 
+    Sleep 3000
+    ConfigureDefaultAppExit:
+        ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0
+        ${If} $0 <> 0
+            !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit
+        ${EndIf}
+        DetailPrint "Jellyfin Server service exit action set, $0"
+${EndIf}
+
 SectionEnd
 
 Section "-start service" StartService
@@ -255,6 +275,16 @@ ${AndIf} $_INSTALLSERVICE_ == "Yes"
 ${EndIf}
 SectionEnd
 
+Section "Create Shortcuts" CreateWinShortcuts
+    ${If} $_MAKESHORTCUTS_ == "Yes"
+        CreateDirectory "$SMPROGRAMS\Jellyfin Server"
+        CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED
+        CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
+        ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED
+        CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0
+    ${EndIf}
+SectionEnd
+
 ;--------------------------------
 ;Descriptions
 
@@ -275,6 +305,7 @@ Section "Uninstall"
 
     ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder"  ; read the installation folder
     ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder"  ; read the data folder
+    ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType"  ; read the account name
 
     DetailPrint "Jellyfin Install location: $INSTDIR"
     DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_"
@@ -307,13 +338,18 @@ Section "Uninstall"
 
     Sleep 3000 ; Give time for Windows to catchup
 
-    NoServiceUninstall: ; existing install was present but no service was detected
+    NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none
+        ${If} $_SERVICEACCOUNTTYPE_ == "None"
+            RMDir /r "$SMPROGRAMS\Jellyfin Server"
+            Delete "$DESKTOP\Jellyfin Server.lnk"
+            DetailPrint "Removed old shortcuts..."
+        ${EndIf}
 
     Delete "$INSTDIR\*.*"
     RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web"
     Delete "$INSTDIR\Uninstall.exe"
     RMDir /r /REBOOTOK "$INSTDIR"
-
+    
     DeleteRegKey HKLM "Software\Jellyfin"
     DeleteRegKey HKLM "${REG_UNINST_KEY}"
 
@@ -326,6 +362,7 @@ Function .onInit
     StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService"
     StrCpy $_EXISTINGINSTALLATION_ "No"
     StrCpy $_EXISTINGSERVICE_ "No"
+    StrCpy $_MAKESHORTCUTS_ "No"
 
     SetShellVarContext current
     StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
@@ -353,6 +390,16 @@ Function .onInit
     StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later
     SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade"
 
+  ; check if service was run using Network Service account
+    ClearErrors
+    ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default
+
+    ClearErrors
+    ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds
+
+    ; Hide sections which will not be needed in case of previous install
+    ; SectionSetText ${InstallService} ""
+
 ; check if there is a service called Jellyfin, there should be
 ; hack : nssm statuscode Jellyfin will return non zero return code in case it exists
     ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0
@@ -363,18 +410,17 @@ Function .onInit
     StrCpy $_EXISTINGSERVICE_ "Yes"
     StrCpy $_INSTALLSERVICE_ "Yes"
     StrCpy $_SERVICESTART_ "Yes"
+    StrCpy $_MAKESHORTCUTS_ "No"
+    SectionSetText ${CreateWinShortcuts} ""
 
-    ; check if service was run using Network Service account
-    ClearErrors
-    ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default
-
-    ClearErrors
-    ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds
-
-    ; Hide sections which will not be needed in case of previous install
-    ; SectionSetText ${InstallService} ""
-
+  
     NoService: ; existing install was present but no service was detected
+        ${If} $_SERVICEACCOUNTTYPE_ == "None"
+            StrCpy $_SETUPTYPE_ "Basic"
+            StrCpy $_INSTALLSERVICE_ "No"
+            StrCpy $_SERVICESTART_ "No"
+            StrCpy $_MAKESHORTCUTS_ "Yes"
+        ${EndIf}
 
 ; Let the user know that we'll upgrade and provide an option to quit.
     MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \
@@ -383,8 +429,7 @@ Function .onInit
 
     ProceedWithUpgrade:
 
-    NoExisitingInstall:
-; by this time, the variables have been correctly set to reflect previous install details
+    NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details
 
 FunctionEnd
 
@@ -413,6 +458,25 @@ Function HideConfirmationPage
     ${EndIf}
 FunctionEnd
 
+Function HideSetupTypePage
+    ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType
+        Abort
+    ${EndIf}
+FunctionEnd
+
+Function HideComponentsPage
+     ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice
+        Abort
+    ${EndIf}
+FunctionEnd
+
+; Setup Type dialog show function
+Function ShowSetupTypePage
+  Call HideSetupTypePage
+  Call fnc_setuptype_Create
+  nsDialogs::Show
+FunctionEnd
+
 ; Service Config dialog show function
 Function ShowServiceConfigPage
   Call HideServiceConfigPage
@@ -431,6 +495,46 @@ FunctionEnd
 Var StartServiceAfterInstall
 Var UseNetworkServiceAccount
 Var UseLocalSystemAccount
+Var BasicInstall
+
+
+Function SetupTypePage_Config
+${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall
+ IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default
+        folderfound:
+            StrCpy $_FOLDEREXISTS_ "Yes"
+            Goto InstallCheck
+        foldernotfound:
+            StrCpy $_FOLDEREXISTS_ "No"
+            Goto InstallCheck
+
+InstallCheck:
+${If} $BasicInstall == 1
+    StrCpy $_SETUPTYPE_ "Basic"
+    StrCpy $_INSTALLSERVICE_ "No"
+    StrCpy $_SERVICESTART_ "No"
+    StrCpy $_SERVICEACCOUNTTYPE_ "None"
+    StrCpy $_MAKESHORTCUTS_ "Yes"
+    ${If} $_FOLDEREXISTS_ == "Yes"
+        StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\"
+    ${EndIf}
+${Else}
+    StrCpy $_SETUPTYPE_ "Advanced"
+    StrCpy $_INSTALLSERVICE_ "Yes"
+    StrCpy $_MAKESHORTCUTS_ "No"
+    ${If} $_FOLDEREXISTS_ == "Yes"
+            MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\
+            $\r$\nBasic Setup is highly recommended.\
+            $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack
+        GoBack:
+            Abort
+    ${EndIf}
+        GoAhead:
+            StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server"
+            SectionSetText ${CreateWinShortcuts} ""
+${EndIf}
+    
+FunctionEnd
 
 Function ServiceConfigPage_Config
 ${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall

+ 2 - 0
jellyfin.ruleset

@@ -10,6 +10,8 @@
     <Rule Id="SA1101" Action="None" />
     <!-- disable warning SA1108: Block statements should not contain embedded comments -->
     <Rule Id="SA1108" Action="None" />
+    <!-- disable warning SA1128:: Put constructor initializers on their own line -->
+    <Rule Id="SA1128" Action="None" />
     <!-- disable warning SA1130: Use lambda syntax -->
     <Rule Id="SA1130" Action="None" />
     <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration -->

+ 19 - 0
tests/Jellyfin.Common.Tests/HexTests.cs

@@ -0,0 +1,19 @@
+using MediaBrowser.Common;
+using Xunit;
+
+namespace Jellyfin.Common.Tests
+{
+    public class HexTests
+    {
+        [Theory]
+        [InlineData("")]
+        [InlineData("00")]
+        [InlineData("01")]
+        [InlineData("000102030405060708090a0b0c0d0e0f")]
+        [InlineData("0123456789abcdef")]
+        public void RoundTripTest(string data)
+        {
+            Assert.Equal(data, Hex.Encode(Hex.Decode(data)));
+        }
+    }
+}

+ 3 - 3
tests/Jellyfin.Common.Tests/PasswordHashTests.cs

@@ -1,6 +1,6 @@
+using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using Xunit;
-using static MediaBrowser.Common.HexHelper;
 
 namespace Jellyfin.Common.Tests
 {
@@ -15,8 +15,8 @@ namespace Jellyfin.Common.Tests
         {
             var pass = PasswordHash.Parse(passwordHash);
             Assert.Equal(id, pass.Id);
-            Assert.Equal(salt, ToHexString(pass.Salt));
-            Assert.Equal(hash, ToHexString(pass.Hash));
+            Assert.Equal(salt, Hex.Encode(pass.Salt, false));
+            Assert.Equal(hash, Hex.Encode(pass.Hash, false));
         }
 
         [Theory]