Parcourir la source

Merge pull request from jellyfin/master

Nyanmisaka il y a 5 ans
Parent
commit
83a344b627
93 fichiers modifiés avec 725 ajouts et 485 suppressions
  1. 15 5
      .ci/azure-pipelines-package.yml
  2. 4 0
      .ci/azure-pipelines.yml
  3. 1 1
      Emby.Dlna/Profiles/DefaultProfile.cs
  4. 1 1
      Emby.Dlna/Profiles/DenonAvrProfile.cs
  5. 1 1
      Emby.Dlna/Profiles/DirectTvProfile.cs
  6. 1 1
      Emby.Dlna/Profiles/Foobar2000Profile.cs
  7. 1 1
      Emby.Dlna/Profiles/MarantzProfile.cs
  8. 2 1
      Emby.Dlna/Profiles/MediaMonkeyProfile.cs
  9. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs
  10. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs
  11. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs
  12. 2 1
      Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs
  13. 17 18
      Emby.Server.Implementations/ApplicationHost.cs
  14. 0 56
      Emby.Server.Implementations/Collections/CollectionManager.cs
  15. 0 57
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  16. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  17. 11 2
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  18. 11 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  19. 39 5
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  20. 19 10
      Emby.Server.Implementations/Library/LibraryManager.cs
  21. 7 3
      Emby.Server.Implementations/Localization/Core/af.json
  22. 3 3
      Emby.Server.Implementations/Localization/Core/ar.json
  23. 1 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  24. 3 1
      Emby.Server.Implementations/Localization/Core/mr.json
  25. 10 1
      Emby.Server.Implementations/Localization/Core/pt.json
  26. 3 0
      Emby.Server.Implementations/Net/SocketFactory.cs
  27. 3 2
      Emby.Server.Implementations/Session/SessionManager.cs
  28. 15 17
      Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
  29. 6 4
      Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
  30. 0 1
      Jellyfin.Api/Controllers/StartupController.cs
  31. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  32. 0 1
      Jellyfin.Data/Jellyfin.Data.csproj
  33. 0 25
      Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs
  34. 0 12
      Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
  35. 76 21
      Jellyfin.Server.Implementations/Users/UserManager.cs
  36. 1 2
      Jellyfin.Server/CoreAppHost.cs
  37. 2 2
      Jellyfin.Server/Program.cs
  38. 5 0
      Jellyfin.Server/StartupOptions.cs
  39. 1 1
      MediaBrowser.Api/FilterService.cs
  40. 32 72
      MediaBrowser.Api/SyncPlay/SyncPlayService.cs
  41. 2 9
      MediaBrowser.Api/UserService.cs
  42. 0 2
      MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs
  43. 0 2
      MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
  44. 1 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  45. 1 1
      MediaBrowser.Controller/Entities/Movies/BoxSet.cs
  46. 5 0
      MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
  47. 1 3
      MediaBrowser.Controller/Library/ILibraryManager.cs
  48. 2 2
      MediaBrowser.Controller/Library/IUserManager.cs
  49. 1 1
      MediaBrowser.Controller/Library/TVUtils.cs
  50. 26 18
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  51. 31 1
      MediaBrowser.Controller/Providers/IExternalId.cs
  52. 15 0
      MediaBrowser.Controller/Session/SessionInfo.cs
  53. 11 11
      MediaBrowser.Controller/SyncPlay/GroupInfo.cs
  54. 1 1
      MediaBrowser.Controller/SyncPlay/GroupMember.cs
  55. 1 1
      MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs
  56. 1 1
      MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
  57. 1 1
      MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
  58. 1 0
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  59. 4 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  60. 2 2
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  61. 6 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  62. 1 1
      MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs
  63. 7 7
      MediaBrowser.Model/Dlna/StreamInfo.cs
  64. 21 11
      MediaBrowser.Model/Providers/ExternalIdInfo.cs
  65. 71 0
      MediaBrowser.Model/Providers/ExternalIdMediaType.cs
  66. 2 1
      MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs
  67. 0 6
      MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs
  68. 3 3
      MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs
  69. 2 2
      MediaBrowser.Providers/Manager/MetadataService.cs
  70. 4 3
      MediaBrowser.Providers/Manager/ProviderManager.cs
  71. 9 2
      MediaBrowser.Providers/Movies/MovieExternalIds.cs
  72. 5 1
      MediaBrowser.Providers/Music/MusicExternalIds.cs
  73. 17 4
      MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs
  74. 25 6
      MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
  75. 39 0
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
  76. 2 2
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs
  77. 3 2
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs
  78. 12 3
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
  79. 2 1
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs
  80. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
  81. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
  82. 6 6
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
  83. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
  84. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
  85. 17 4
      MediaBrowser.Providers/TV/TvExternalIds.cs
  86. 2 2
      README.md
  87. 0 1
      RSSDP/SsdpCommunicationsServer.cs
  88. 1 1
      deployment/build.windows.amd64
  89. 4 4
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  90. 2 0
      tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs
  91. 12 0
      tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs
  92. 3 3
      tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
  93. 7 0
      tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

+ 15 - 5
.ci/azure-pipelines-package.yml

@@ -35,7 +35,6 @@ jobs:
   steps:
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
     displayName: 'Build Dockerfile'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
 
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
     displayName: 'Run Dockerfile (unstable)'
@@ -47,14 +46,19 @@ jobs:
 
   - task: PublishPipelineArtifact@1
     displayName: 'Publish Release'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
     inputs:
       targetPath: '$(Build.SourcesDirectory)/deployment/dist'
       artifactName: 'jellyfin-server-$(BuildConfiguration)'
 
+  - task: SSH@0
+    displayName: 'Create target directory on repository server'
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
   - task: CopyFilesOverSSH@0
     displayName: 'Upload artifacts to repository server'
-    condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))
     inputs:
       sshEndpoint: repository
       sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
@@ -120,7 +124,10 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'inline'
-      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
+      inline: |
+        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
+        rm $0
+        exit
 
   - task: SSH@0
     displayName: 'Update Stable Repository'
@@ -128,4 +135,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'inline'
-      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'
+      inline: |
+        sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
+        rm $0
+        exit

+ 4 - 0
.ci/azure-pipelines.yml

@@ -15,11 +15,13 @@ trigger:
   batch: true
 
 jobs:
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-main.yml
     parameters:
       LinuxImage: 'ubuntu-latest'
       RestoreBuildProjects: $(RestoreBuildProjects)
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-test.yml
     parameters:
       ImageNames:
@@ -27,6 +29,7 @@ jobs:
         Windows: 'windows-latest'
         macOS: 'macos-latest'
 
+- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-abi.yml
     parameters:
       Packages:
@@ -44,4 +47,5 @@ jobs:
           AssemblyFileName: MediaBrowser.Common.dll
       LinuxImage: 'ubuntu-latest'
 
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml

+ 1 - 1
Emby.Dlna/Profiles/DefaultProfile.cs

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
 
         public void AddXmlRootAttribute(string name, string value)
         {
-            var atts = XmlRootAttributes ?? new XmlAttribute[] { };
+            var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>();
             var list = atts.ToList();
 
             list.Add(new XmlAttribute

+ 1 - 1
Emby.Dlna/Profiles/DenonAvrProfile.cs

@@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles
                 },
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 1 - 1
Emby.Dlna/Profiles/DirectTvProfile.cs

@@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 1 - 1
Emby.Dlna/Profiles/Foobar2000Profile.cs

@@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 1 - 1
Emby.Dlna/Profiles/MarantzProfile.cs

@@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles
                 },
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = System.Array.Empty<ResponseProfile>();
         }
     }
 }

+ 2 - 1
Emby.Dlna/Profiles/MediaMonkeyProfile.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 2 - 1
Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 2 - 1
Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 2 - 1
Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 2 - 1
Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs

@@ -1,5 +1,6 @@
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Dlna;
 
 namespace Emby.Dlna.Profiles
@@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles
                 }
             };
 
-            ResponseProfiles = new ResponseProfile[] { };
+            ResponseProfiles = Array.Empty<ResponseProfile>();
         }
     }
 }

+ 17 - 18
Emby.Server.Implementations/ApplicationHost.cs

@@ -43,9 +43,9 @@ using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
+using Emby.Server.Implementations.SyncPlay;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
-using Emby.Server.Implementations.SyncPlay;
 using MediaBrowser.Api;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
@@ -78,8 +78,8 @@ using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Controller.Subtitles;
-using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.Model.Configuration;
@@ -484,12 +484,10 @@ namespace Emby.Server.Implementations
 
                 foreach (var plugin in Plugins)
                 {
-                    pluginBuilder.AppendLine(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "{0} {1}",
-                            plugin.Name,
-                            plugin.Version));
+                    pluginBuilder.Append(plugin.Name)
+                        .Append(' ')
+                        .Append(plugin.Version)
+                        .AppendLine();
                 }
 
                 Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString());
@@ -566,10 +564,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
             serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
-            serviceCollection.AddSingleton<IMediaEncoder>(provider =>
-                ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
+            serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
             serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -873,6 +869,11 @@ namespace Emby.Server.Implementations
                     Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
                     continue;
                 }
+                catch (TypeLoadException ex)
+                {
+                    Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
+                    continue;
+                }
 
                 foreach (Type type in exportedTypes)
                 {
@@ -1155,7 +1156,7 @@ namespace Emby.Server.Implementations
                     return null;
                 }
 
-                return GetLocalApiUrl(addresses.First());
+                return GetLocalApiUrl(addresses[0]);
             }
             catch (Exception ex)
             {
@@ -1228,7 +1229,7 @@ namespace Emby.Server.Implementations
             var addresses = ServerConfigurationManager
                 .Configuration
                 .LocalNetworkAddresses
-                .Select(NormalizeConfiguredLocalAddress)
+                .Select(x => NormalizeConfiguredLocalAddress(x))
                 .Where(i => i != null)
                 .ToList();
 
@@ -1249,8 +1250,7 @@ namespace Emby.Server.Implementations
                     }
                 }
 
-                var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
-                if (valid)
+                if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false))
                 {
                     resultList.Add(address);
 
@@ -1264,13 +1264,12 @@ namespace Emby.Server.Implementations
             return resultList;
         }
 
-        public IPAddress NormalizeConfiguredLocalAddress(string address)
+        public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address)
         {
             var index = address.Trim('/').IndexOf('/');
-
             if (index != -1)
             {
-                address = address.Substring(index + 1);
+                address = address.Slice(index + 1);
             }
 
             if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))

+ 0 - 56
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -363,60 +363,4 @@ namespace Emby.Server.Implementations.Collections
             return results.Values;
         }
     }
-
-    /// <summary>
-    /// The collection manager entry point.
-    /// </summary>
-    public sealed class CollectionManagerEntryPoint : IServerEntryPoint
-    {
-        private readonly CollectionManager _collectionManager;
-        private readonly IServerConfigurationManager _config;
-        private readonly ILogger<CollectionManagerEntryPoint> _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.
-        /// </summary>
-        /// <param name="collectionManager">The collection manager.</param>
-        /// <param name="config">The server configuration manager.</param>
-        /// <param name="logger">The logger.</param>
-        public CollectionManagerEntryPoint(
-            ICollectionManager collectionManager,
-            IServerConfigurationManager config,
-            ILogger<CollectionManagerEntryPoint> logger)
-        {
-            _collectionManager = (CollectionManager)collectionManager;
-            _config = config;
-            _logger = logger;
-        }
-
-        /// <inheritdoc />
-        public async Task RunAsync()
-        {
-            if (!_config.Configuration.CollectionsUpgraded && _config.Configuration.IsStartupWizardCompleted)
-            {
-                var path = _collectionManager.GetCollectionsFolderPath();
-
-                if (Directory.Exists(path))
-                {
-                    try
-                    {
-                        await _collectionManager.EnsureLibraryFolder(path, true).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "Error creating camera uploads library");
-                    }
-
-                    _config.Configuration.CollectionsUpgraded = true;
-                    _config.SaveConfiguration();
-                }
-            }
-        }
-
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            // Nothing to dispose
-        }
-    }
 }

+ 0 - 57
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -109,7 +109,6 @@ namespace Emby.Server.Implementations.Configuration
             if (!string.IsNullOrWhiteSpace(newPath)
                 && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
             {
-                // Validate
                 if (!File.Exists(newPath))
                 {
                     throw new FileNotFoundException(
@@ -133,7 +132,6 @@ namespace Emby.Server.Implementations.Configuration
             if (!string.IsNullOrWhiteSpace(newPath)
                 && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
             {
-                // Validate
                 if (!Directory.Exists(newPath))
                 {
                     throw new DirectoryNotFoundException(
@@ -146,60 +144,5 @@ namespace Emby.Server.Implementations.Configuration
                 EnsureWriteAccess(newPath);
             }
         }
-
-        /// <summary>
-        /// Sets all configuration values to their optimal values.
-        /// </summary>
-        /// <returns>If the configuration changed.</returns>
-        public bool SetOptimalValues()
-        {
-            var config = Configuration;
-
-            var changed = false;
-
-            if (!config.EnableCaseSensitiveItemIds)
-            {
-                config.EnableCaseSensitiveItemIds = true;
-                changed = true;
-            }
-
-            if (!config.SkipDeserializationForBasicTypes)
-            {
-                config.SkipDeserializationForBasicTypes = true;
-                changed = true;
-            }
-
-            if (!config.EnableSimpleArtistDetection)
-            {
-                config.EnableSimpleArtistDetection = true;
-                changed = true;
-            }
-
-            if (!config.EnableNormalizedItemByNameIds)
-            {
-                config.EnableNormalizedItemByNameIds = true;
-                changed = true;
-            }
-
-            if (!config.DisableLiveTvChannelUserDataName)
-            {
-                config.DisableLiveTvChannelUserDataName = true;
-                changed = true;
-            }
-
-            if (!config.EnableNewOmdbSupport)
-            {
-                config.EnableNewOmdbSupport = true;
-                changed = true;
-            }
-
-            if (!config.CollectionsUpgraded)
-            {
-                config.CollectionsUpgraded = true;
-                changed = true;
-            }
-
-            return changed;
-        }
     }
 }

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

@@ -25,7 +25,7 @@
 
   <ItemGroup>
     <PackageReference Include="IPNetwork2" Version="2.5.211" />
-    <PackageReference Include="Jellyfin.XmlTv" Version="10.4.3" />
+    <PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />

+ 11 - 2
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,3 +1,4 @@
+using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
@@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <inheritdoc />
         public Task RunAsync()
         {
-            _udpServer = new UdpServer(_logger, _appHost, _config);
-            _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            try
+            {
+                _udpServer = new UdpServer(_logger, _appHost, _config);
+                _udpServer.Start(PortNumber, _cancellationTokenSource.Token);
+            }
+            catch (SocketException ex)
+            {
+                _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
+            }
+
             return Task.CompletedTask;
         }
 

+ 11 - 1
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -1,5 +1,6 @@
 using System;
 using System.IO;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
@@ -13,19 +14,28 @@ namespace Emby.Server.Implementations.Library
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     {
         private readonly ILibraryManager _libraryManager;
+        private readonly IServerApplicationPaths _serverApplicationPaths;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
-        public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
+        /// <param name="serverApplicationPaths">The server application paths.</param>
+        public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
         {
             _libraryManager = libraryManager;
+            _serverApplicationPaths = serverApplicationPaths;
         }
 
         /// <inheritdoc />
         public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
         {
+            // Don't ignore application folders
+            if (fileInfo.FullName.Contains(_serverApplicationPaths.RootFolderPath, StringComparison.InvariantCulture))
+            {
+                return false;
+            }
+
             // Don't ignore top level folders
             if (fileInfo.IsDirectory && parent is AggregateFolder)
             {

+ 39 - 5
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -1,3 +1,6 @@
+#nullable enable
+
+using System;
 using System.Linq;
 using DotNet.Globbing;
 
@@ -11,7 +14,7 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// Files matching these glob patterns will be ignored.
         /// </summary>
-        public static readonly string[] Patterns = new string[]
+        private static readonly string[] _patterns =
         {
             "**/small.jpg",
             "**/albumart.jpg",
@@ -19,32 +22,51 @@ namespace Emby.Server.Implementations.Library
 
             // Directories
             "**/metadata/**",
+            "**/metadata",
             "**/ps3_update/**",
+            "**/ps3_update",
             "**/ps3_vprm/**",
+            "**/ps3_vprm",
             "**/extrafanart/**",
+            "**/extrafanart",
             "**/extrathumbs/**",
+            "**/extrathumbs",
             "**/.actors/**",
+            "**/.actors",
             "**/.wd_tv/**",
+            "**/.wd_tv",
             "**/lost+found/**",
+            "**/lost+found",
 
             // WMC temp recording directories that will constantly be written to
             "**/TempRec/**",
+            "**/TempRec",
             "**/TempSBE/**",
+            "**/TempSBE",
 
             // Synology
             "**/eaDir/**",
+            "**/eaDir",
             "**/@eaDir/**",
+            "**/@eaDir",
             "**/#recycle/**",
+            "**/#recycle",
 
             // Qnap
             "**/@Recycle/**",
+            "**/@Recycle",
             "**/.@__thumb/**",
+            "**/.@__thumb",
             "**/$RECYCLE.BIN/**",
+            "**/$RECYCLE.BIN",
             "**/System Volume Information/**",
+            "**/System Volume Information",
             "**/.grab/**",
+            "**/.grab",
 
             // Unix hidden files and directories
             "**/.*/**",
+            "**/.*",
 
             // thumbs.db
             "**/thumbs.db",
@@ -56,19 +78,31 @@ namespace Emby.Server.Implementations.Library
 
         private static readonly GlobOptions _globOptions = new GlobOptions
         {
-            Evaluation = {
+            Evaluation =
+            {
                 CaseInsensitive = true
             }
         };
 
-        private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
+        private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
 
         /// <summary>
         /// Returns true if the supplied path should be ignored.
         /// </summary>
-        public static bool ShouldIgnore(string path)
+        /// <param name="path">The path to test.</param>
+        /// <returns>Whether to ignore the path.</returns>
+        public static bool ShouldIgnore(ReadOnlySpan<char> path)
         {
-            return _globs.Any(g => g.IsMatch(path));
+            int len = _globs.Length;
+            for (int i = 0; i < len; i++)
+            {
+                if (_globs[i].IsMatch(path))
+                {
+                    return true;
+                }
+            }
+
+            return false;
         }
     }
 }

+ 19 - 10
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -514,8 +514,8 @@ namespace Emby.Server.Implementations.Library
             return key.GetMD5();
         }
 
-        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, bool allowIgnorePath = true)
-            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent, allowIgnorePath: allowIgnorePath);
+        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
+            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
 
         private BaseItem ResolvePath(
             FileSystemMetadata fileInfo,
@@ -523,8 +523,7 @@ namespace Emby.Server.Implementations.Library
             IItemResolver[] resolvers,
             Folder parent = null,
             string collectionType = null,
-            LibraryOptions libraryOptions = null,
-            bool allowIgnorePath = true)
+            LibraryOptions libraryOptions = null)
         {
             if (fileInfo == null)
             {
@@ -548,7 +547,7 @@ namespace Emby.Server.Implementations.Library
             };
 
             // Return null if ignore rules deem that we should do so
-            if (allowIgnorePath && IgnoreFile(args.FileInfo, args.Parent))
+            if (IgnoreFile(args.FileInfo, args.Parent))
             {
                 return null;
             }
@@ -713,7 +712,7 @@ namespace Emby.Server.Implementations.Library
             Directory.CreateDirectory(rootFolderPath);
 
             var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
-                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false))
+                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
                              .DeepCopy<Folder, AggregateFolder>();
 
             // In case program data folder was moved
@@ -795,7 +794,7 @@ namespace Emby.Server.Implementations.Library
                         if (tmpItem == null)
                         {
                             _logger.LogDebug("Creating new userRootFolder with DeepCopy");
-                            tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath), allowIgnorePath: false)).DeepCopy<Folder, UserRootFolder>();
+                            tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>();
                         }
 
                         // In case program data folder was moved
@@ -1894,9 +1893,19 @@ namespace Emby.Server.Implementations.Library
                     }
                 }
 
-                ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
-                image.Width = size.Width;
-                image.Height = size.Height;
+                try
+                {
+                    ImageDimensions size = _imageProcessor.GetImageDimensions(item, image);
+                    image.Width = size.Width;
+                    image.Height = size.Height;
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path);
+                    image.Width = 0;
+                    image.Height = 0;
+                    continue;
+                }
 
                 try
                 {

+ 7 - 3
Emby.Server.Implementations/Localization/Core/af.json

@@ -19,8 +19,8 @@
     "Sync": "Sinkroniseer",
     "HeaderFavoriteSongs": "Gunsteling Liedjies",
     "Songs": "Liedjies",
-    "DeviceOnlineWithName": "{0} is verbind",
-    "DeviceOfflineWithName": "{0} het afgesluit",
+    "DeviceOnlineWithName": "{0} gekoppel is",
+    "DeviceOfflineWithName": "{0} is ontkoppel",
     "Collections": "Versamelings",
     "Inherit": "Ontvang",
     "HeaderLiveTV": "Live TV",
@@ -91,5 +91,9 @@
     "ChapterNameValue": "Hoofstuk",
     "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
     "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
-    "Albums": "Albums"
+    "Albums": "Albums",
+    "TasksChannelsCategory": "Internet kanale",
+    "TasksApplicationCategory": "aansoek",
+    "TasksLibraryCategory": "biblioteek",
+    "TasksMaintenanceCategory": "onderhoud"
 }

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

@@ -1,5 +1,5 @@
 {
-    "Albums": "ألبومات",
+    "Albums": "البومات",
     "AppDeviceValues": "تطبيق: {0}, جهاز: {1}",
     "Application": "تطبيق",
     "Artists": "الفنانين",
@@ -14,7 +14,7 @@
     "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
     "Favorites": "المفضلة",
     "Folders": "المجلدات",
-    "Genres": "الأنواع",
+    "Genres": "التضنيفات",
     "HeaderAlbumArtists": "فناني الألبومات",
     "HeaderCameraUploads": "تحميلات الكاميرا",
     "HeaderContinueWatching": "استئناف",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي",
     "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي",
     "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا",
-    "NotificationOptionInstallationFailed": "فشل في التثبيت",
+    "NotificationOptionInstallationFailed": "فشل التثبيت",
     "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد",
     "NotificationOptionPluginError": "فشل في البرنامج المضاف",
     "NotificationOptionPluginInstalled": "تم تثبيت الملحق",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -31,7 +31,7 @@
     "ItemAddedWithName": "{0} fue agregado a la biblioteca",
     "ItemRemovedWithName": "{0} fue removido de la biblioteca",
     "LabelIpAddressValue": "Dirección IP: {0}",
-    "LabelRunningTimeValue": "Duración: {0}",
+    "LabelRunningTimeValue": "Tiempo de reproducción: {0}",
     "Latest": "Recientes",
     "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado",
     "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}",

+ 3 - 1
Emby.Server.Implementations/Localization/Core/mr.json

@@ -57,5 +57,7 @@
     "HeaderCameraUploads": "कॅमेरा अपलोड",
     "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
     "Application": "अ‍ॅप्लिकेशन",
-    "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}"
+    "AppDeviceValues": "अ‍ॅप: {0}, यंत्र: {1}",
+    "Collections": "संग्रह",
+    "ChapterNameValue": "धडा {0}"
 }

+ 10 - 1
Emby.Server.Implementations/Localization/Core/pt.json

@@ -104,5 +104,14 @@
     "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.",
     "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.",
     "TasksChannelsCategory": "Canais de Internet",
-    "TaskRefreshChapterImages": "Extrair Imagens do Capítulo"
+    "TaskRefreshChapterImages": "Extrair Imagens do Capítulo",
+    "TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.",
+    "TaskDownloadMissingSubtitles": "Download das legendas em falta",
+    "TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.",
+    "TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.",
+    "TaskCleanTranscode": "Limpar o diretório de Transcode",
+    "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.",
+    "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.",
+    "TaskRefreshPeople": "Atualizar pessoas",
+    "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados."
 }

+ 3 - 0
Emby.Server.Implementations/Net/SocketFactory.cs

@@ -19,6 +19,7 @@ namespace Emby.Server.Implementations.Net
             var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
             try
             {
+                retVal.EnableBroadcast = true;
                 retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
 
@@ -46,6 +47,7 @@ namespace Emby.Server.Implementations.Net
             var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp);
             try
             {
+                retVal.EnableBroadcast = true;
                 retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
 
@@ -112,6 +114,7 @@ namespace Emby.Server.Implementations.Net
 
             try
             {
+                retVal.EnableBroadcast = true;
                 // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
                 retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
 

+ 3 - 2
Emby.Server.Implementations/Session/SessionManager.cs

@@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session
                     }
                     catch (DbUpdateConcurrencyException e)
                     {
-                        _logger.LogWarning(e, "Error updating user's last activity date.");
+                        _logger.LogDebug(e, "Error updating user's last activity date.");
                     }
                 }
             }
@@ -502,7 +502,8 @@ namespace Emby.Server.Implementations.Session
                 Client = appName,
                 DeviceId = deviceId,
                 ApplicationVersion = appVersion,
-                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
+                ServerId = _appHost.SystemId
             };
 
             var username = user?.Username;

+ 15 - 17
Emby.Server.Implementations/SyncPlay/SyncPlayController.cs

@@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay
         }
 
         /// <inheritdoc />
-        public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
+        public void CreateGroup(SessionInfo session, CancellationToken cancellationToken)
         {
             _group.AddSession(session);
             _syncPlayManager.AddSessionToGroup(session, this);
 
             _group.PlayingItem = session.FullNowPlayingItem;
-            _group.IsPaused = true;
+            _group.IsPaused = session.PlayState.IsPaused;
             _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
             _group.LastActivity = DateTime.UtcNow;
 
             var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
             SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
-            var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
-            SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
         }
 
         /// <inheritdoc />
         public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
         {
-            if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
+            if (session.NowPlayingItem?.Id == _group.PlayingItem.Id)
             {
                 _group.AddSession(session);
                 _syncPlayManager.AddSessionToGroup(session, this);
@@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
                 SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
 
-                // Client join and play, syncing will happen client side
+                // Syncing will happen client-side
                 if (!_group.IsPaused)
                 {
                     var playCommand = NewSyncPlayCommand(SendCommandType.Play);
@@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay
         /// <inheritdoc />
         public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
         {
-            // The server's job is to mantain a consistent state to which clients refer to,
-            // as also to notify clients of state changes.
-            // The actual syncing of media playback happens client side.
-            // Clients are aware of the server's time and use it to sync.
+            // The server's job is to maintain a consistent state for clients to reference
+            // and notify clients of state changes. The actual syncing of media playback
+            // happens client side. Clients are aware of the server's time and use it to sync.
             switch (request.Type)
             {
                 case PlaybackRequestType.Play:
@@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay
                 case PlaybackRequestType.Seek:
                     HandleSeekRequest(session, request, cancellationToken);
                     break;
-                case PlaybackRequestType.Buffering:
+                case PlaybackRequestType.Buffer:
                     HandleBufferingRequest(session, request, cancellationToken);
                     break;
-                case PlaybackRequestType.BufferingDone:
+                case PlaybackRequestType.Ready:
                     HandleBufferingDoneRequest(session, request, cancellationToken);
                     break;
-                case PlaybackRequestType.UpdatePing:
+                case PlaybackRequestType.Ping:
                     HandlePingUpdateRequest(session, request);
                     break;
             }
@@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay
             {
                 // Pick a suitable time that accounts for latency
                 var delay = _group.GetHighestPing() * 2;
-                delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+                delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
 
                 // Unpause group and set starting point in future
                 // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@@ -337,8 +334,9 @@ namespace Emby.Server.Implementations.SyncPlay
                 var currentTime = DateTime.UtcNow;
                 var elapsedTime = currentTime - _group.LastActivity;
                 _group.LastActivity = currentTime;
+
                 // Seek only if playback actually started
-                // (a pause request may be issued during the delay added to account for latency)
+                // Pause request may be issued during the delay added to account for latency
                 _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
 
                 var command = NewSyncPlayCommand(SendCommandType.Pause);
@@ -451,7 +449,7 @@ namespace Emby.Server.Implementations.SyncPlay
                     {
                         // Client, that was buffering, resumed playback but did not update others in time
                         delay = _group.GetHighestPing() * 2;
-                        delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+                        delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
 
                         _group.LastActivity = currentTime.AddMilliseconds(
                             delay);
@@ -495,7 +493,7 @@ namespace Emby.Server.Implementations.SyncPlay
         private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
         {
             // Collected pings are used to account for network latency when unpausing playback
-            _group.UpdatePing(session, request.Ping ?? _group.DefaulPing);
+            _group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
         }
 
         /// <inheritdoc />

+ 6 - 4
Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs

@@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay
             {
                 _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
 
-                var error = new GroupUpdate<string>()
+                var error = new GroupUpdate<string>
                 {
                     Type = GroupUpdateType.CreateGroupDenied
                 };
+
                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
@@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 var group = new SyncPlayController(_sessionManager, this);
                 _groups[group.GetGroupId()] = group;
 
-                group.InitGroup(session, cancellationToken);
+                group.CreateGroup(session, cancellationToken);
             }
         }
 
@@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 {
                     Type = GroupUpdateType.JoinGroupDenied
                 };
+
                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
@@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay
                     group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select(
                     group => group.GetInfo()).ToList();
             }
-            // Otherwise show all available groups
             else
             {
+                // Otherwise show all available groups
                 return _groups.Values.Where(
                     group => HasAccessToItem(user, group.GetPlayingItemId())).Select(
                     group => group.GetInfo()).ToList();
@@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay
                 {
                     Type = GroupUpdateType.JoinGroupDenied
                 };
+
                 _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
                 return;
             }
@@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay
             }
 
             _sessionToGroupMap.Remove(session.Id, out var tempGroup);
-
             if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
             {
                 throw new InvalidOperationException("Session was in wrong group!");

+ 0 - 1
Jellyfin.Api/Controllers/StartupController.cs

@@ -36,7 +36,6 @@ namespace Jellyfin.Api.Controllers
         public void CompleteWizard()
         {
             _config.Configuration.IsStartupWizardCompleted = true;
-            _config.SetOptimalValues();
             _config.SaveConfiguration();
         }
 

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -16,7 +16,7 @@
     <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
   </ItemGroup>
 
   <ItemGroup>

+ 0 - 1
Jellyfin.Data/Jellyfin.Data.csproj

@@ -21,7 +21,6 @@
   <ItemGroup>
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
     <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" />
   </ItemGroup>
 
 </Project>

+ 0 - 25
Jellyfin.Server.Implementations/Users/DefaultAuthenticationProvider.cs

@@ -5,7 +5,6 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Model.Cryptography;
@@ -117,29 +116,5 @@ namespace Jellyfin.Server.Implementations.Users
 
             return Task.CompletedTask;
         }
-
-        /// <inheritdoc />
-        public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
-        {
-            if (newPassword != null)
-            {
-                newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString();
-            }
-
-            if (string.IsNullOrWhiteSpace(newPasswordHash))
-            {
-                throw new ArgumentNullException(nameof(newPasswordHash));
-            }
-
-            user.EasyPassword = newPasswordHash;
-        }
-
-        /// <inheritdoc />
-        public string? GetEasyPasswordHash(User user)
-        {
-            return string.IsNullOrEmpty(user.EasyPassword)
-                ? null
-                : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash);
-        }
     }
 }

+ 0 - 12
Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs

@@ -34,17 +34,5 @@ namespace Jellyfin.Server.Implementations.Users
         {
             return Task.CompletedTask;
         }
-
-        /// <inheritdoc />
-        public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
-        {
-            // Nothing here
-        }
-
-        /// <inheritdoc />
-        public string GetEasyPasswordHash(User user)
-        {
-            return string.Empty;
-        }
     }
 }

+ 76 - 21
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -12,6 +12,7 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Drawing;
@@ -22,6 +23,7 @@ using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Users;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Server.Implementations.Users
@@ -85,7 +87,19 @@ namespace Jellyfin.Server.Implementations.Users
         public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
 
         /// <inheritdoc/>
-        public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
+        public IEnumerable<User> Users
+        {
+            get
+            {
+                using var dbContext = _dbProvider.CreateContext();
+                return dbContext.Users
+                    .Include(user => user.Permissions)
+                    .Include(user => user.Preferences)
+                    .Include(user => user.AccessSchedules)
+                    .Include(user => user.ProfileImage)
+                    .ToList();
+            }
+        }
 
         /// <inheritdoc/>
         public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
@@ -98,7 +112,13 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
 
-            return _dbProvider.CreateContext().Users.Find(id);
+            using var dbContext = _dbProvider.CreateContext();
+            return dbContext.Users
+                .Include(user => user.Permissions)
+                .Include(user => user.Preferences)
+                .Include(user => user.AccessSchedules)
+                .Include(user => user.ProfileImage)
+                .FirstOrDefault(user => user.Id == id);
         }
 
         /// <inheritdoc/>
@@ -109,9 +129,14 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Invalid username", nameof(name));
             }
 
-            // This can't use an overload with StringComparer because that would cause the query to
-            // have to be evaluated client-side.
-            return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name));
+            using var dbContext = _dbProvider.CreateContext();
+            return dbContext.Users
+                .Include(user => user.Permissions)
+                .Include(user => user.Preferences)
+                .Include(user => user.AccessSchedules)
+                .Include(user => user.ProfileImage)
+                .AsEnumerable()
+                .FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase));
         }
 
         /// <inheritdoc/>
@@ -127,7 +152,7 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Invalid username", nameof(newName));
             }
 
-            if (user.Username.Equals(newName, StringComparison.Ordinal))
+            if (user.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))
             {
                 throw new ArgumentException("The new and old names must be different.");
             }
@@ -149,7 +174,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public void UpdateUser(User user)
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             dbContext.Users.Update(user);
             dbContext.SaveChanges();
         }
@@ -157,7 +182,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public async Task UpdateUserAsync(User user)
         {
-            var dbContext = _dbProvider.CreateContext();
+            await using var dbContext = _dbProvider.CreateContext();
             dbContext.Users.Update(user);
 
             await dbContext.SaveChangesAsync().ConfigureAwait(false);
@@ -171,7 +196,7 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
             }
 
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
 
             // TODO: Remove after user item data is migrated.
             var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
@@ -192,15 +217,20 @@ namespace Jellyfin.Server.Implementations.Users
         }
 
         /// <inheritdoc/>
-        public void DeleteUser(User user)
+        public void DeleteUser(Guid userId)
         {
+            using var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users
+                .Include(u => u.Permissions)
+                .Include(u => u.Preferences)
+                .Include(u => u.AccessSchedules)
+                .Include(u => u.ProfileImage)
+                .FirstOrDefault(u => u.Id == userId);
             if (user == null)
             {
-                throw new ArgumentNullException(nameof(user));
+                throw new ResourceNotFoundException(nameof(userId));
             }
 
-            var dbContext = _dbProvider.CreateContext();
-
             if (dbContext.Users.Find(user.Id) == null)
             {
                 throw new ArgumentException(string.Format(
@@ -226,9 +256,18 @@ namespace Jellyfin.Server.Implementations.Users
                         CultureInfo.InvariantCulture,
                         "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
                         user.Username),
-                    nameof(user));
+                    nameof(userId));
             }
 
+            // Clear all entities related to the user from the database.
+            if (user.ProfileImage != null)
+            {
+                dbContext.Remove(user.ProfileImage);
+            }
+
+            dbContext.RemoveRange(user.Permissions);
+            dbContext.RemoveRange(user.Preferences);
+            dbContext.RemoveRange(user.AccessSchedules);
             dbContext.Users.Remove(user);
             dbContext.SaveChanges();
             OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
@@ -263,7 +302,17 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
         {
-            GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
+            if (newPassword != null)
+            {
+                newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString();
+            }
+
+            if (string.IsNullOrWhiteSpace(newPasswordSha1))
+            {
+                throw new ArgumentNullException(nameof(newPasswordSha1));
+            }
+
+            user.EasyPassword = newPasswordSha1;
             UpdateUser(user);
 
             OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
@@ -361,7 +410,7 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentNullException(nameof(username));
             }
 
-            var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+            var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
             bool success;
             IAuthenticationProvider? authenticationProvider;
 
@@ -389,8 +438,7 @@ namespace Jellyfin.Server.Implementations.Users
 
                     // Search the database for the user again
                     // the authentication provider might have created it
-                    user = Users
-                        .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
+                    user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
 
                     if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
                     {
@@ -527,7 +575,7 @@ namespace Jellyfin.Server.Implementations.Users
         public void Initialize()
         {
             // TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
 
             if (dbContext.Users.Any())
             {
@@ -590,7 +638,14 @@ namespace Jellyfin.Server.Implementations.Users
         public void UpdateConfiguration(Guid userId, UserConfiguration config)
         {
             var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
+            var user = dbContext.Users
+                           .Include(u => u.Permissions)
+                           .Include(u => u.Preferences)
+                           .Include(u => u.AccessSchedules)
+                           .Include(u => u.ProfileImage)
+                           .FirstOrDefault(u => u.Id == userId)
+                       ?? throw new ArgumentException("No user exists with given Id!");
+
             user.SubtitleMode = config.SubtitleMode;
             user.HidePlayedInLatest = config.HidePlayedInLatest;
             user.EnableLocalPassword = config.EnableLocalPassword;
@@ -679,7 +734,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public void ClearProfileImage(User user)
         {
-            var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateContext();
             dbContext.Remove(user.ProfileImage);
             dbContext.SaveChanges();
         }

+ 1 - 2
Jellyfin.Server/CoreAppHost.cs

@@ -66,8 +66,7 @@ namespace Jellyfin.Server
             // TODO: Set up scoping and use AddDbContextPool
             serviceCollection.AddDbContext<JellyfinDb>(
                 options => options
-                    .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
-                    .UseLazyLoadingProxies(),
+                    .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
                 ServiceLifetime.Transient);
 
             serviceCollection.AddSingleton<JellyfinDbProvider>();

+ 2 - 2
Jellyfin.Server/Program.cs

@@ -274,10 +274,10 @@ namespace Jellyfin.Server
                     var addresses = appHost.ServerConfigurationManager
                         .Configuration
                         .LocalNetworkAddresses
-                        .Select(appHost.NormalizeConfiguredLocalAddress)
+                        .Select(x => appHost.NormalizeConfiguredLocalAddress(x))
                         .Where(i => i != null)
                         .ToHashSet();
-                    if (addresses.Any() && !addresses.Contains(IPAddress.Any))
+                    if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any))
                     {
                         if (!addresses.Contains(IPAddress.Loopback))
                         {

+ 5 - 0
Jellyfin.Server/StartupOptions.cs

@@ -101,6 +101,11 @@ namespace Jellyfin.Server
                 config.Add(UdpServer.AddressOverrideConfigKey, PublishedServerUrl.ToString());
             }
 
+            if (FFmpegPath != null)
+            {
+                config.Add(ConfigurationExtensions.FfmpegPathKey, FFmpegPath);
+            }
+
             return config;
         }
     }

+ 1 - 1
MediaBrowser.Api/FilterService.cs

@@ -125,7 +125,7 @@ namespace MediaBrowser.Api
                 IncludeItemTypes = request.GetIncludeItemTypes(),
                 DtoOptions = new Controller.Dto.DtoOptions
                 {
-                    Fields = new ItemFields[] { },
+                    Fields = Array.Empty<ItemFields>(),
                     EnableImages = false,
                     EnableUserData = false
                 },

+ 32 - 72
MediaBrowser.Api/SyncPlay/SyncPlayService.cs

@@ -11,93 +11,66 @@ using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Api.SyncPlay
 {
-    [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")]
+    [Route("/SyncPlay/New", "POST", Summary = "Create a new SyncPlay group")]
     [Authenticated]
-    public class SyncPlayNewGroup : IReturnVoid
+    public class SyncPlayNew : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")]
+    [Route("/SyncPlay/Join", "POST", Summary = "Join an existing SyncPlay group")]
     [Authenticated]
-    public class SyncPlayJoinGroup : IReturnVoid
+    public class SyncPlayJoin : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// Gets or sets the Group id.
         /// </summary>
         /// <value>The Group id to join.</value>
         [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
         public string GroupId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the playing item id.
-        /// </summary>
-        /// <value>The client's currently playing item id.</value>
-        [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
-        public string PlayingItemId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")]
+    [Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")]
     [Authenticated]
-    public class SyncPlayLeaveGroup : IReturnVoid
+    public class SyncPlayLeave : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/ListGroups", "POST", Summary = "List SyncPlay groups")]
+    [Route("/SyncPlay/List", "GET", Summary = "List SyncPlay groups")]
     [Authenticated]
-    public class SyncPlayListGroups : IReturnVoid
+    public class SyncPlayList : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// Gets or sets the filter item id.
         /// </summary>
         /// <value>The filter item id.</value>
-        [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
+        [ApiMember(Name = "FilterItemId", Description = "Filter by item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
         public string FilterItemId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")]
+    [Route("/SyncPlay/Play", "POST", Summary = "Request play in SyncPlay group")]
     [Authenticated]
-    public class SyncPlayPlayRequest : IReturnVoid
+    public class SyncPlayPlay : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")]
+    [Route("/SyncPlay/Pause", "POST", Summary = "Request pause in SyncPlay group")]
     [Authenticated]
-    public class SyncPlayPauseRequest : IReturnVoid
+    public class SyncPlayPause : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")]
+    [Route("/SyncPlay/Seek", "POST", Summary = "Request seek in SyncPlay group")]
     [Authenticated]
-    public class SyncPlaySeekRequest : IReturnVoid
+    public class SyncPlaySeek : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
         public long PositionTicks { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
+    [Route("/SyncPlay/Buffering", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
     [Authenticated]
-    public class SyncPlayBufferingRequest : IReturnVoid
+    public class SyncPlayBuffering : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// Gets or sets the date used to pin PositionTicks in time.
         /// </summary>
@@ -109,20 +82,17 @@ namespace MediaBrowser.Api.SyncPlay
         public long PositionTicks { get; set; }
 
         /// <summary>
-        /// Gets or sets whether this is a buffering or a buffering-done request.
+        /// Gets or sets whether this is a buffering or a ready request.
         /// </summary>
         /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value>
         [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")]
         public bool BufferingDone { get; set; }
     }
 
-    [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")]
+    [Route("/SyncPlay/Ping", "POST", Summary = "Update session ping")]
     [Authenticated]
-    public class SyncPlayUpdatePing : IReturnVoid
+    public class SyncPlayPing : IReturnVoid
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
         public double Ping { get; set; }
     }
@@ -158,7 +128,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayNewGroup request)
+        public void Post(SyncPlayNew request)
         {
             var currentSession = GetSession(_sessionContext);
             _syncPlayManager.NewGroup(currentSession, CancellationToken.None);
@@ -168,30 +138,20 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayJoinGroup request)
+        public void Post(SyncPlayJoin request)
         {
             var currentSession = GetSession(_sessionContext);
 
             Guid groupId;
-            Guid playingItemId = Guid.Empty;
-
             if (!Guid.TryParse(request.GroupId, out groupId))
             {
                 Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId);
                 return;
             }
 
-            // Both null and empty strings mean that client isn't playing anything
-            if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId))
-            {
-                Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId);
-                return;
-            }
-
             var joinRequest = new JoinGroupRequest()
             {
-                GroupId = groupId,
-                PlayingItemId = playingItemId
+                GroupId = groupId
             };
 
             _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None);
@@ -201,7 +161,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayLeaveGroup request)
+        public void Post(SyncPlayLeave request)
         {
             var currentSession = GetSession(_sessionContext);
             _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None);
@@ -212,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// </summary>
         /// <param name="request">The request.</param>
         /// <value>The requested list of groups.</value>
-        public List<GroupInfoView> Post(SyncPlayListGroups request)
+        public List<GroupInfoView> Get(SyncPlayList request)
         {
             var currentSession = GetSession(_sessionContext);
             var filterItemId = Guid.Empty;
@@ -229,7 +189,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayPlayRequest request)
+        public void Post(SyncPlayPlay request)
         {
             var currentSession = GetSession(_sessionContext);
             var syncPlayRequest = new PlaybackRequest()
@@ -243,7 +203,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayPauseRequest request)
+        public void Post(SyncPlayPause request)
         {
             var currentSession = GetSession(_sessionContext);
             var syncPlayRequest = new PlaybackRequest()
@@ -257,7 +217,7 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlaySeekRequest request)
+        public void Post(SyncPlaySeek request)
         {
             var currentSession = GetSession(_sessionContext);
             var syncPlayRequest = new PlaybackRequest()
@@ -272,12 +232,12 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayBufferingRequest request)
+        public void Post(SyncPlayBuffering request)
         {
             var currentSession = GetSession(_sessionContext);
             var syncPlayRequest = new PlaybackRequest()
             {
-                Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering,
+                Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer,
                 When = DateTime.Parse(request.When),
                 PositionTicks = request.PositionTicks
             };
@@ -288,12 +248,12 @@ namespace MediaBrowser.Api.SyncPlay
         /// Handles the specified request.
         /// </summary>
         /// <param name="request">The request.</param>
-        public void Post(SyncPlayUpdatePing request)
+        public void Post(SyncPlayPing request)
         {
             var currentSession = GetSession(_sessionContext);
             var syncPlayRequest = new PlaybackRequest()
             {
-                Type = PlaybackRequestType.UpdatePing,
+                Type = PlaybackRequestType.Ping,
                 Ping = Convert.ToInt64(request.Ping)
             };
             _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None);

+ 2 - 9
MediaBrowser.Api/UserService.cs

@@ -365,15 +365,8 @@ namespace MediaBrowser.Api
 
         public Task DeleteAsync(DeleteUser request)
         {
-            var user = _userManager.GetUserById(request.Id);
-
-            if (user == null)
-            {
-                throw new ResourceNotFoundException("User not found");
-            }
-
-            _sessionMananger.RevokeUserTokens(user.Id, null);
-            _userManager.DeleteUser(user);
+            _userManager.DeleteUser(request.Id);
+            _sessionMananger.RevokeUserTokens(request.Id, null);
             return Task.CompletedTask;
         }
 

+ 0 - 2
MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs

@@ -13,8 +13,6 @@ namespace MediaBrowser.Controller.Authentication
         Task<ProviderAuthenticationResult> Authenticate(string username, string password);
         bool HasPassword(User user);
         Task ChangePassword(User user, string newPassword);
-        void ChangeEasyPassword(User user, string newPassword, string newPasswordHash);
-        string GetEasyPasswordHash(User user);
     }
 
     public interface IRequiresResolvedUser

+ 0 - 2
MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs

@@ -19,7 +19,5 @@ namespace MediaBrowser.Controller.Configuration
         /// </summary>
         /// <value>The configuration.</value>
         ServerConfiguration Configuration { get; }
-
-        bool SetOptimalValues();
     }
 }

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

@@ -613,7 +613,7 @@ namespace MediaBrowser.Controller.Entities
             {
                 if (!IsFileProtocol)
                 {
-                    return new string[] { };
+                    return Array.Empty<string>();
                 }
 
                 return new[] { Path };

+ 1 - 1
MediaBrowser.Controller/Entities/Movies/BoxSet.cs

@@ -198,7 +198,7 @@ namespace MediaBrowser.Controller.Entities.Movies
 
         public Guid[] GetLibraryFolderIds()
         {
-            var expandedFolders = new List<Guid>() { };
+            var expandedFolders = new List<Guid>();
 
             return FlattenItems(this, expandedFolders)
                 .SelectMany(i => LibraryManager.GetCollectionFolders(i))

+ 5 - 0
MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs

@@ -23,6 +23,11 @@ namespace MediaBrowser.Controller.Extensions
         /// </summary>
         public const string FfmpegAnalyzeDurationKey = "FFmpeg:analyzeduration";
 
+        /// <summary>
+        /// The key for the FFmpeg path option.
+        /// </summary>
+        public const string FfmpegPathKey = "ffmpeg";
+
         /// <summary>
         /// The key for a setting that indicates whether playlists should allow duplicate entries.
         /// </summary>

+ 1 - 3
MediaBrowser.Controller/Library/ILibraryManager.cs

@@ -30,12 +30,10 @@ namespace MediaBrowser.Controller.Library
         /// </summary>
         /// <param name="fileInfo">The file information.</param>
         /// <param name="parent">The parent.</param>
-        /// <param name="allowIgnorePath">Allow the path to be ignored.</param>
         /// <returns>BaseItem.</returns>
         BaseItem ResolvePath(
             FileSystemMetadata fileInfo,
-            Folder parent = null,
-            bool allowIgnorePath = true);
+            Folder parent = null);
 
         /// <summary>
         /// Resolves a set of files into a list of BaseItem.

+ 2 - 2
MediaBrowser.Controller/Library/IUserManager.cs

@@ -111,8 +111,8 @@ namespace MediaBrowser.Controller.Library
         /// <summary>
         /// Deletes the specified user.
         /// </summary>
-        /// <param name="user">The user to be deleted.</param>
-        void DeleteUser(User user);
+        /// <param name="userId">The id of the user to be deleted.</param>
+        void DeleteUser(Guid userId);
 
         /// <summary>
         /// Resets the password.

+ 1 - 1
MediaBrowser.Controller/Library/TVUtils.cs

@@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Library
                     };
                 }
 
-                return new DayOfWeek[] { };
+                return Array.Empty<DayOfWeek>();
             }
 
             return null;

+ 26 - 18
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -483,7 +483,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                     {
                         if (isQsvDecoder)
                         {
-                            arg.Append("-hwaccel qsv ");
+                            arg.Append("-hwaccel qsv -init_hw_device qsv=hw ");
                         }
                         // While using SW decoder
                         else
@@ -1351,7 +1351,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 transcoderChannelLimit = 6;
             }
 
-            var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec);
+            var isTranscodingAudio = !IsCopyCodec(codec);
 
             int? resultChannels = state.GetRequestedAudioChannels(codec);
             if (isTranscodingAudio)
@@ -1757,7 +1757,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // output dimensions. Output dimensions are guaranteed to be even.
                 var outputWidth = width.Value;
                 var outputHeight = height.Value;
-                var vaapi_or_qsv = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ? "qsv" : "vaapi";
+                var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
 
                 if (!videoWidth.HasValue
                     || outputWidth != videoWidth.Value
@@ -1765,17 +1765,19 @@ namespace MediaBrowser.Controller.MediaEncoding
                     || outputHeight != videoHeight.Value)
                 {
                     // Force nv12 pixel format to enable 10-bit to 8-bit colour conversion.
+                    // use vpp_qsv filter to avoid green bar when the fixed output size is requested.
                     filters.Add(
                         string.Format(
                             CultureInfo.InvariantCulture,
-                            "scale_{0}=w={1}:h={2}:format=nv12",
-                            vaapi_or_qsv,
+                            "{0}=w={1}:h={2}:format=nv12",
+                            qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi",
                             outputWidth,
                             outputHeight));
                 }
                 else
                 {
-                    filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv));
+                    // set w=0:h=0 for vpp_qsv to keep the original dimensions, otherwise it will fail.
+                    filters.Add(string.Format(CultureInfo.InvariantCulture, "{0}format=nv12", qsv_or_vaapi ? "vpp_qsv=w=0:h=0:" : "scale_vaapi="));
                 }
             }
             else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1
@@ -2262,7 +2264,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 flags.Add("+ignidx");
             }
 
-            if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec))
             {
                 flags.Add("+genpts");
             }
@@ -2521,21 +2523,21 @@ namespace MediaBrowser.Controller.MediaEncoding
         }
 
         /// <summary>
-        /// Gets the name of the output video codec.
+        /// Gets the ffmpeg option string for the hardware accelerated video decoder.
         /// </summary>
+        /// <param name="state">The encoding job info.</param>
+        /// <param name="encodingOptions">The encoding options.</param>
+        /// <returns>The option string or null if none available.</returns>
         protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions)
         {
-            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             var videoStream = state.VideoStream;
-            var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
-                || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
-
 
-            if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
+            if (videoStream == null)
             {
                 return null;
             }
 
+            var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
             // Only use alternative encoders for video files.
             // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
             // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this.
@@ -2544,10 +2546,16 @@ namespace MediaBrowser.Controller.MediaEncoding
                 return null;
             }
 
-            if (videoStream != null
-                && !string.IsNullOrEmpty(videoStream.Codec)
-                && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+            if (IsCopyCodec(state.OutputVideoCodec))
             {
+                return null;
+            }
+
+            if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType))
+            {
+                var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile)
+                    && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
+
                 // Only hevc and vp9 formats have 10-bit hardware decoder support now.
                 if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase)
@@ -3000,7 +3008,7 @@ namespace MediaBrowser.Controller.MediaEncoding
                 args += " -mpegts_m2ts_mode 1";
             }
 
-            if (EncodingHelper.IsCopyCodec(videoCodec))
+            if (IsCopyCodec(videoCodec))
             {
                 if (state.VideoStream != null
                     && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase)
@@ -3102,7 +3110,7 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             var args = "-codec:a:0 " + codec;
 
-            if (EncodingHelper.IsCopyCodec(codec))
+            if (IsCopyCodec(codec))
             {
                 return args;
             }

+ 31 - 1
MediaBrowser.Controller/Providers/IExternalId.cs

@@ -1,15 +1,45 @@
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Controller.Providers
 {
+    /// <summary>
+    /// Represents an identifier for an external provider.
+    /// </summary>
     public interface IExternalId
     {
-        string Name { get; }
+        /// <summary>
+        /// Gets the display name of the provider associated with this ID type.
+        /// </summary>
+        string ProviderName { get; }
 
+        /// <summary>
+        /// Gets the unique key to distinguish this provider/type pair. This should be unique across providers.
+        /// </summary>
+        // TODO: This property is not actually unique across the concrete types at the moment. It should be updated to be unique.
         string Key { get; }
 
+        /// <summary>
+        /// Gets the specific media type for this id. This is used to distinguish between the different
+        /// external id types for providers with multiple ids.
+        /// A null value indicates there is no specific media type associated with the external id, or this is the
+        /// default id for the external provider so there is no need to specify a type.
+        /// </summary>
+        /// <remarks>
+        /// This can be used along with the <see cref="ProviderName"/> to localize the external id on the client.
+        /// </remarks>
+        ExternalIdMediaType? Type { get; }
+
+        /// <summary>
+        /// Gets the URL format string for this id.
+        /// </summary>
         string UrlFormatString { get; }
 
+        /// <summary>
+        /// Determines whether this id supports a given item type.
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <returns>True if this item is supported, otherwise false.</returns>
         bool Supports(IHasProviderIds item);
     }
 }

+ 15 - 0
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -108,6 +108,12 @@ namespace MediaBrowser.Controller.Session
         /// <value>The name of the device.</value>
         public string DeviceName { get; set; }
 
+        /// <summary>
+        /// Gets or sets the type of the device.
+        /// </summary>
+        /// <value>The type of the device.</value>
+        public string DeviceType { get; set; }
+
         /// <summary>
         /// Gets or sets the now playing item.
         /// </summary>
@@ -215,8 +221,17 @@ namespace MediaBrowser.Controller.Session
 
         public string PlaylistItemId { get; set; }
 
+        public string ServerId { get; set; }
+
         public string UserPrimaryImageTag { get; set; }
 
+        /// <summary>
+        /// Gets or sets the supported commands.
+        /// </summary>
+        /// <value>The supported commands.</value>
+        public string[] SupportedCommands
+            => Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
+
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {
             var controllers = SessionControllers.ToList();

+ 11 - 11
MediaBrowser.Controller/SyncPlay/GroupInfo.cs

@@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <summary>
         /// Gets the default ping value used for sessions.
         /// </summary>
-        public long DefaulPing { get; } = 500;
+        public long DefaultPing { get; } = 500;
 
         /// <summary>
         /// Gets or sets the group identifier.
@@ -70,16 +70,16 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="session">The session.</param>
         public void AddSession(SessionInfo session)
         {
-            if (ContainsSession(session.Id.ToString()))
+            if (ContainsSession(session.Id))
             {
                 return;
             }
 
             var member = new GroupMember();
             member.Session = session;
-            member.Ping = DefaulPing;
+            member.Ping = DefaultPing;
             member.IsBuffering = false;
-            Participants[session.Id.ToString()] = member;
+            Participants[session.Id] = member;
         }
 
         /// <summary>
@@ -88,12 +88,12 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="session">The session.</param>
         public void RemoveSession(SessionInfo session)
         {
-            if (!ContainsSession(session.Id.ToString()))
+            if (!ContainsSession(session.Id))
             {
                 return;
             }
 
-            Participants.Remove(session.Id.ToString(), out _);
+            Participants.Remove(session.Id, out _);
         }
 
         /// <summary>
@@ -103,12 +103,12 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="ping">The ping.</param>
         public void UpdatePing(SessionInfo session, long ping)
         {
-            if (!ContainsSession(session.Id.ToString()))
+            if (!ContainsSession(session.Id))
             {
                 return;
             }
 
-            Participants[session.Id.ToString()].Ping = ping;
+            Participants[session.Id].Ping = ping;
         }
 
         /// <summary>
@@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <value name="session">The highest ping in the group.</value>
         public long GetHighestPing()
         {
-            long max = Int64.MinValue;
+            long max = long.MinValue;
             foreach (var session in Participants.Values)
             {
                 max = Math.Max(max, session.Ping);
@@ -133,12 +133,12 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="isBuffering">The state.</param>
         public void SetBuffering(SessionInfo session, bool isBuffering)
         {
-            if (!ContainsSession(session.Id.ToString()))
+            if (!ContainsSession(session.Id))
             {
                 return;
             }
 
-            Participants[session.Id.ToString()].IsBuffering = isBuffering;
+            Participants[session.Id].IsBuffering = isBuffering;
         }
 
         /// <summary>

+ 1 - 1
MediaBrowser.Controller/SyncPlay/GroupMember.cs

@@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.SyncPlay
     public class GroupMember
     {
         /// <summary>
-        /// Gets or sets whether this member is buffering.
+        /// Gets or sets a value indicating whether this member is buffering.
         /// </summary>
         /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value>
         public bool IsBuffering { get; set; }

+ 1 - 1
MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs

@@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.SyncPlay
         /// </summary>
         /// <param name="session">The session.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
-        void InitGroup(SessionInfo session, CancellationToken cancellationToken);
+        void CreateGroup(SessionInfo session, CancellationToken cancellationToken);
 
         /// <summary>
         /// Adds the session to the group.

+ 1 - 1
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -81,7 +81,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 var id = info.Key + "Id";
                 if (!_validProviderIds.ContainsKey(id))
                 {
-                    _validProviderIds.Add(id, info.Key);
+                    _validProviderIds.Add(id, info.Key!);
                 }
             }
 

+ 1 - 1
MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs

@@ -41,7 +41,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo
 
             var outputStream = new BlurayDiscInfo
             {
-                MediaStreams = new MediaStream[] { }
+                MediaStreams = Array.Empty<MediaStream>()
             };
 
             if (playlist == null)

+ 1 - 0
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -90,6 +90,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         // $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/'
         private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version>
         {
+            { "libavutil=56.51,libavcodec=58.91,libavformat=58.45,libavdevice=58.10,libavfilter=7.85,libswscale=5.7,libswresample=3.7,libpostproc=55.7,", new Version(4, 3) },
             { "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) },
             { "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) },
             { "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) },

+ 4 - 3
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -21,6 +21,7 @@ using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.System;
 using Microsoft.Extensions.Logging;
 using System.Diagnostics;
+using Microsoft.Extensions.Configuration;
 
 namespace MediaBrowser.MediaEncoding.Encoder
 {
@@ -46,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
         private readonly object _runningProcessesLock = new object();
         private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
 
-        private string _ffmpegPath;
+        private string _ffmpegPath = string.Empty;
         private string _ffprobePath;
 
         public MediaEncoder(
@@ -55,14 +56,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
             IFileSystem fileSystem,
             ILocalizationManager localization,
             Lazy<EncodingHelper> encodingHelperFactory,
-            string startupOptionsFFmpegPath)
+            IConfiguration config)
         {
             _logger = logger;
             _configurationManager = configurationManager;
             _fileSystem = fileSystem;
             _localization = localization;
             _encodingHelperFactory = encodingHelperFactory;
-            _startupOptionFFmpegPath = startupOptionsFFmpegPath;
+            _startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
         }
 
         private EncodingHelper EncodingHelper => _encodingHelperFactory.Value;

+ 2 - 2
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -1008,7 +1008,7 @@ namespace MediaBrowser.MediaEncoding.Probing
                 var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
                 if (string.IsNullOrWhiteSpace(artist))
                 {
-                    audio.Artists = new string[] { };
+                    audio.Artists = Array.Empty<string>();
                 }
                 else
                 {
@@ -1031,7 +1031,7 @@ namespace MediaBrowser.MediaEncoding.Probing
 
             if (string.IsNullOrWhiteSpace(albumArtist))
             {
-                audio.AlbumArtists = new string[] { };
+                audio.AlbumArtists = Array.Empty<string>();
             }
             else
             {

+ 6 - 2
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -82,8 +82,6 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableRemoteAccess { get; set; }
 
-        public bool CollectionsUpgraded { get; set; }
-
         /// <summary>
         /// Gets or sets a value indicating whether [enable case sensitive item ids].
         /// </summary>
@@ -269,6 +267,9 @@ namespace MediaBrowser.Model.Configuration
             PathSubstitutions = Array.Empty<PathSubstitution>();
             IgnoreVirtualInterfaces = false;
             EnableSimpleArtistDetection = false;
+            SkipDeserializationForBasicTypes = true;
+
+            PluginRepositories = new List<RepositoryInfo>();
 
             DisplaySpecialsWithinSeasons = true;
             EnableExternalContentInSuggestions = true;
@@ -282,6 +283,9 @@ namespace MediaBrowser.Model.Configuration
             EnableHttps = false;
             EnableDashboardResponseCaching = true;
             EnableCaseSensitiveItemIds = true;
+            EnableNormalizedItemByNameIds = true;
+            DisableLiveTvChannelUserDataName = true;
+            EnableNewOmdbSupport = true;
 
             AutoRunWebApp = true;
             EnableRemoteAccess = true;

+ 1 - 1
MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs

@@ -206,7 +206,7 @@ namespace MediaBrowser.Model.Dlna
                 }
             }
 
-            return new MediaFormatProfile[] { };
+            return Array.Empty<MediaFormatProfile>();
         }
 
         private MediaFormatProfile ValueOf(string value)

+ 7 - 7
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -144,7 +144,7 @@ namespace MediaBrowser.Model.Dlna
 
         public Dictionary<string, string> StreamOptions { get; private set; }
 
-        public string MediaSourceId => MediaSource == null ? null : MediaSource.Id;
+        public string MediaSourceId => MediaSource?.Id;
 
         public bool IsDirectStream =>
             PlayMethod == PlayMethod.DirectStream ||
@@ -813,18 +813,18 @@ namespace MediaBrowser.Model.Dlna
             {
                 var stream = TargetAudioStream;
 
-                string inputCodec = stream == null ? null : stream.Codec;
+                string inputCodec = stream?.Codec;
 
                 if (IsDirectStream)
                 {
-                    return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
                 }
 
                 foreach (string codec in AudioCodecs)
                 {
                     if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                     {
-                        return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
                     }
                 }
 
@@ -838,18 +838,18 @@ namespace MediaBrowser.Model.Dlna
             {
                 var stream = TargetVideoStream;
 
-                string inputCodec = stream == null ? null : stream.Codec;
+                string inputCodec = stream?.Codec;
 
                 if (IsDirectStream)
                 {
-                    return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec };
+                    return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec };
                 }
 
                 foreach (string codec in VideoCodecs)
                 {
                     if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase))
                     {
-                        return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec };
+                        return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec };
                     }
                 }
 

+ 21 - 11
MediaBrowser.Model/Providers/ExternalIdInfo.cs

@@ -1,26 +1,36 @@
-#nullable disable
-#pragma warning disable CS1591
-
 namespace MediaBrowser.Model.Providers
 {
+    /// <summary>
+    /// Represents the external id information for serialization to the client.
+    /// </summary>
     public class ExternalIdInfo
     {
         /// <summary>
-        /// Gets or sets the name.
+        /// Gets or sets the display name of the external id provider (IE: IMDB, MusicBrainz, etc).
+        /// </summary>
+        // TODO: This should be renamed to ProviderName
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the unique key for this id. This key should be unique across all providers.
         /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
+        // TODO: This property is not actually unique across the concrete types at the moment. It should be updated to be unique.
+        public string? Key { get; set; }
 
         /// <summary>
-        /// Gets or sets the key.
+        /// Gets or sets the specific media type for this id. This is used to distinguish between the different
+        /// external id types for providers with multiple ids.
+        /// A null value indicates there is no specific media type associated with the external id, or this is the
+        /// default id for the external provider so there is no need to specify a type.
         /// </summary>
-        /// <value>The key.</value>
-        public string Key { get; set; }
+        /// <remarks>
+        /// This can be used along with the <see cref="Name"/> to localize the external id on the client.
+        /// </remarks>
+        public ExternalIdMediaType? Type { get; set; }
 
         /// <summary>
         /// Gets or sets the URL format string.
         /// </summary>
-        /// <value>The URL format string.</value>
-        public string UrlFormatString { get; set; }
+        public string? UrlFormatString { get; set; }
     }
 }

+ 71 - 0
MediaBrowser.Model/Providers/ExternalIdMediaType.cs

@@ -0,0 +1,71 @@
+namespace MediaBrowser.Model.Providers
+{
+    /// <summary>
+    /// The specific media type of an <see cref="ExternalIdInfo"/>.
+    /// </summary>
+    /// <remarks>
+    /// Client applications may use this as a translation key.
+    /// </remarks>
+    public enum ExternalIdMediaType
+    {
+        /// <summary>
+        /// A music album.
+        /// </summary>
+        Album = 1,
+
+        /// <summary>
+        /// The artist of a music album.
+        /// </summary>
+        AlbumArtist = 2,
+
+        /// <summary>
+        /// The artist of a media item.
+        /// </summary>
+        Artist = 3,
+
+        /// <summary>
+        /// A boxed set of media.
+        /// </summary>
+        BoxSet = 4,
+
+        /// <summary>
+        /// A series episode.
+        /// </summary>
+        Episode = 5,
+
+        /// <summary>
+        /// A movie.
+        /// </summary>
+        Movie = 6,
+
+        /// <summary>
+        /// An alternative artist apart from the main artist.
+        /// </summary>
+        OtherArtist = 7,
+
+        /// <summary>
+        /// A person.
+        /// </summary>
+        Person = 8,
+
+        /// <summary>
+        /// A release group.
+        /// </summary>
+        ReleaseGroup = 9,
+
+        /// <summary>
+        /// A single season of a series.
+        /// </summary>
+        Season = 10,
+
+        /// <summary>
+        /// A series.
+        /// </summary>
+        Series = 11,
+
+        /// <summary>
+        /// A music track.
+        /// </summary>
+        Track = 12
+    }
+}

+ 2 - 1
MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs

@@ -1,6 +1,7 @@
 #nullable disable
 #pragma warning disable CS1591
 
+using System;
 using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Model.Querying
@@ -54,7 +55,7 @@ namespace MediaBrowser.Model.Querying
 
         public UpcomingEpisodesQuery()
         {
-            EnableImageTypes = new ImageType[] { };
+            EnableImageTypes = Array.Empty<ImageType>();
         }
     }
 }

+ 0 - 6
MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs

@@ -12,11 +12,5 @@ namespace MediaBrowser.Model.SyncPlay
         /// </summary>
         /// <value>The Group id to join.</value>
         public Guid GroupId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the playing item id.
-        /// </summary>
-        /// <value>The client's currently playing item id.</value>
-        public Guid PlayingItemId { get; set; }
     }
 }

+ 3 - 3
MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs

@@ -23,16 +23,16 @@ namespace MediaBrowser.Model.SyncPlay
         /// <summary>
         /// A user is signaling that playback is buffering.
         /// </summary>
-        Buffering = 3,
+        Buffer = 3,
 
         /// <summary>
         /// A user is signaling that playback resumed.
         /// </summary>
-        BufferingDone = 4,
+        Ready = 4,
 
         /// <summary>
         /// A user is reporting its ping.
         /// </summary>
-        UpdatePing = 5
+        Ping = 5
     }
 }

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

@@ -723,7 +723,7 @@ namespace MediaBrowser.Providers.Manager
                             userDataList.AddRange(localItem.UserDataList);
                         }
 
-                        MergeData(localItem, temp, new MetadataField[] { }, !options.ReplaceAllMetadata, true);
+                        MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
                         refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport;
 
                         // Only one local provider allowed per item
@@ -849,7 +849,7 @@ namespace MediaBrowser.Providers.Manager
                     {
                         result.Provider = provider.Name;
 
-                        MergeData(result, temp, new MetadataField[] { }, false, false);
+                        MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
                         MergeNewData(temp.Item, id);
 
                         refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload;

+ 4 - 3
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -102,7 +102,7 @@ namespace MediaBrowser.Providers.Manager
 
             _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
             _metadataProviders = metadataProviders.ToArray();
-            _externalIds = externalIds.OrderBy(i => i.Name).ToArray();
+            _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
 
             _savers = metadataSavers.Where(i =>
             {
@@ -900,7 +900,7 @@ namespace MediaBrowser.Providers.Manager
 
                 return new ExternalUrl
                 {
-                    Name = i.Name,
+                    Name = i.ProviderName,
                     Url = string.Format(
                         CultureInfo.InvariantCulture,
                         i.UrlFormatString,
@@ -914,8 +914,9 @@ namespace MediaBrowser.Providers.Manager
             return GetExternalIds(item)
                 .Select(i => new ExternalIdInfo
                 {
-                    Name = i.Name,
+                    Name = i.ProviderName,
                     Key = i.Key,
+                    Type = i.Type,
                     UrlFormatString = i.UrlFormatString
                 });
         }

+ 9 - 2
MediaBrowser.Providers/Movies/MovieExternalIds.cs

@@ -6,17 +6,21 @@ using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Movies
 {
     public class ImdbExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "IMDb";
+        public string ProviderName => "IMDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Imdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/title/{0}";
 
@@ -36,11 +40,14 @@ namespace MediaBrowser.Providers.Movies
     public class ImdbPersonExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "IMDb";
+        public string ProviderName => "IMDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Imdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/name/{0}";
 

+ 5 - 1
MediaBrowser.Providers/Music/MusicExternalIds.cs

@@ -3,17 +3,21 @@
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Music
 {
     public class ImvdbId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "IMVDb";
+        public string ProviderName => "IMVDb";
 
         /// <inheritdoc />
         public string Key => "IMVDb";
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         public string UrlFormatString => null;
 

+ 17 - 4
MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs

@@ -3,17 +3,21 @@
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.AudioDb
 {
     public class AudioDbAlbumExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheAudioDb";
+        public string ProviderName => "TheAudioDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
@@ -24,11 +28,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbOtherAlbumExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheAudioDb Album";
+        public string ProviderName => "TheAudioDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
@@ -39,11 +46,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbArtistExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheAudioDb";
+        public string ProviderName => "TheAudioDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbArtist.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
 
@@ -54,11 +64,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbOtherArtistExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheAudioDb Artist";
+        public string ProviderName => "TheAudioDb";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbArtist.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
 

+ 25 - 6
MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs

@@ -3,6 +3,7 @@
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using MediaBrowser.Providers.Plugins.MusicBrainz;
 
 namespace MediaBrowser.Providers.Music
@@ -10,11 +11,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzReleaseGroupExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz Release Group";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
 
@@ -25,11 +29,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzAlbumArtistExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz Album Artist";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
@@ -40,11 +47,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzAlbumExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz Album";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
 
@@ -55,11 +65,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzArtistExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
@@ -70,12 +83,15 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzOtherArtistExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz Artist";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
 
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
@@ -86,11 +102,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzTrackId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "MusicBrainz Track";
+        public string ProviderName => "MusicBrainz";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzTrack.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
+
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
 

+ 39 - 0
MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs

@@ -229,6 +229,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
         }
 
+        public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, CancellationToken cancellationToken)
+        {
+            var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+            var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+            if (imagesSummary.Data.Fanart > 0)
+            {
+                yield return KeyType.Fanart;
+            }
+
+            if (imagesSummary.Data.Series > 0)
+            {
+                yield return KeyType.Series;
+            }
+
+            if (imagesSummary.Data.Poster > 0)
+            {
+                yield return KeyType.Poster;
+            }
+        }
+
+        public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, CancellationToken cancellationToken)
+        {
+            var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
+            var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
+
+            if (imagesSummary.Data.Season > 0)
+            {
+                yield return KeyType.Season;
+            }
+
+            if (imagesSummary.Data.Fanart > 0)
+            {
+                yield return KeyType.Fanart;
+            }
+
+            // TODO seasonwide is not supported in TvDbSharper
+        }
+
         private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
         {
             if (_cache.TryGetValue(key, out T cachedValue))

+ 2 - 2
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs

@@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             var language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
 
-            var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart };
-            foreach (var keyType in keyTypes)
+            var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
+            await foreach (var keyType in keyTypes)
             {
                 var imageQuery = new ImagesQuery
                 {

+ 3 - 2
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs

@@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
 
             var language = item.GetPreferredMetadataLanguage();
             var remoteImages = new List<RemoteImageInfo>();
-            var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart };
             var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
-            foreach (KeyType keyType in keyTypes)
+            var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
+                .ConfigureAwait(false);
+            await foreach (KeyType keyType in allowedKeyTypes)
             {
                 var imageQuery = new ImagesQuery
                 {

+ 12 - 3
MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs

@@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                 {
                     Name = tvdbTitles.FirstOrDefault(),
                     ProductionYear = firstAired.Year,
-                    SearchProviderName = Name,
-                    ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner
+                    SearchProviderName = Name
                 };
 
+                if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
+                {
+                    // Results from their Search endpoints already include the /banners/ part in the url, because reasons...
+                    remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
+                }
+
                 try
                 {
                     var seriesSesult =
@@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
                     Type = PersonType.Actor,
                     Name = (actor.Name ?? string.Empty).Trim(),
                     Role = actor.Role,
-                    ImageUrl = TvdbUtils.BannerUrl + actor.Image,
                     SortOrder = actor.SortOrder
                 };
 
+                if (!string.IsNullOrEmpty(actor.Image))
+                {
+                    personInfo.ImageUrl = TvdbUtils.TvdbImageBaseUrl + actor.Image;
+                }
+
                 if (!string.IsNullOrWhiteSpace(personInfo.Name))
                 {
                     result.AddPerson(personInfo);

+ 2 - 1
MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs

@@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
     {
         public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K";
         public const string TvdbBaseUrl = "https://www.thetvdb.com/";
-        public const string BannerUrl = TvdbBaseUrl + "banners/";
+        public const string TvdbImageBaseUrl = "https://www.thetvdb.com";
+        public const string BannerUrl = TvdbImageBaseUrl + "/banners/";
 
         public static ImageType GetImageTypeFromKeyType(string keyType)
         {

+ 8 - 3
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs

@@ -1,20 +1,25 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 {
+    /// <summary>
+    /// External ID for a TMDB box set.
+    /// </summary>
     public class TmdbBoxSetExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
         /// <inheritdoc />
         public string Key => MetadataProvider.TmdbCollection.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
+
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
 

+ 8 - 3
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs

@@ -1,21 +1,26 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 {
+    /// <summary>
+    /// External ID for a TMBD movie.
+    /// </summary>
     public class TmdbMovieExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
+
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
 

+ 6 - 6
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
             using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions
             {
-                Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey),
+                Url = string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey),
                 CancellationToken = cancellationToken,
                 AcceptHeader = TmdbUtils.AcceptHeader
             }).ConfigureAwait(false))
@@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 preferredLanguage = "alllang";
             }
 
-            var filename = string.Format("all-{0}.json", preferredLanguage);
+            var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage);
 
             return Path.Combine(path, filename);
         }
@@ -276,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                 languages.Add("en");
             }
 
-            return string.Join(",", languages.ToArray());
+            return string.Join(",", languages);
         }
 
         public static string NormalizeLanguage(string language)
@@ -321,11 +321,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
         /// <returns>Task{CompleteMovieData}.</returns>
         internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
         {
-            var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey);
+            var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey);
 
             if (!string.IsNullOrEmpty(language))
             {
-                url += string.Format("&language={0}", NormalizeLanguage(language));
+                url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language));
 
                 // Get images in english and with no language
                 url += "&include_image_language=" + GetImageLanguagesParam(language);
@@ -377,7 +377,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
             {
                 _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
 
-                url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+                url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
 
                 if (!string.IsNullOrEmpty(language))
                 {

+ 8 - 3
MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs

@@ -1,19 +1,24 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.People
 {
+    /// <summary>
+    /// External ID for a TMDB person.
+    /// </summary>
     public class TmdbPersonExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
 

+ 8 - 3
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs

@@ -1,19 +1,24 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 {
+    /// <summary>
+    /// External ID for a TMDB series.
+    /// </summary>
     public class TmdbSeriesExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
+
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
 

+ 17 - 4
MediaBrowser.Providers/TV/TvExternalIds.cs

@@ -3,6 +3,7 @@
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 
 namespace MediaBrowser.Providers.TV
@@ -10,11 +11,14 @@ namespace MediaBrowser.Providers.TV
     public class Zap2ItExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "Zap2It";
+        public string ProviderName => "Zap2It";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Zap2It.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
 
@@ -25,11 +29,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
 
@@ -40,11 +47,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbSeasonExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
+
         /// <inheritdoc />
         public string UrlFormatString => null;
 
@@ -55,11 +65,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbEpisodeExternalId : IExternalId
     {
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
+
         /// <inheritdoc />
         public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
 

+ 2 - 2
README.md

@@ -16,8 +16,8 @@
 <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/?utm_source=widget">
 <img alt="Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-core/svg-badge.svg"/>
 </a>
-<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1">
-<img alt="Azure Builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"/>
+<a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=29">
+<img alt="Azure Builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20Server"/>
 </a>
 <a href="https://hub.docker.com/r/jellyfin/jellyfin">
 <img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/>

+ 0 - 1
RSSDP/SsdpCommunicationsServer.cs

@@ -339,7 +339,6 @@ namespace Rssdp.Infrastructure
         private ISocket ListenForBroadcastsAsync()
         {
             var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort);
-
             _ = ListenToSocketInternal(socket);
 
             return socket;

+ 1 - 1
deployment/build.windows.amd64

@@ -8,7 +8,7 @@ set -o xtrace
 # Version variables
 NSSM_VERSION="nssm-2.24-101-g897c7ad"
 NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip"
-FFMPEG_VERSION="ffmpeg-4.2.1-win64-static"
+FFMPEG_VERSION="ffmpeg-4.3-win64-static"
 FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip"
 
 # Move to source directory

+ 4 - 4
tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj

@@ -13,15 +13,15 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="AutoFixture" Version="4.11.0" />
-    <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
-    <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" />
+    <PackageReference Include="AutoFixture" Version="4.13.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
+    <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" />
     <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
-    <PackageReference Include="Moq" Version="4.14.3" />
+    <PackageReference Include="Moq" Version="4.14.5" />
   </ItemGroup>
 
   <!-- Code Analyzers -->

+ 2 - 0
tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs

@@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests
         }
 
         [Theory]
+        [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)]
         [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)]
@@ -32,6 +33,7 @@ namespace Jellyfin.MediaEncoding.Tests
         {
             public IEnumerator<object?[]> GetEnumerator()
             {
+                yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) };
                 yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) };

+ 12 - 0
tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs

@@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests
 {
     internal static class EncoderValidatorTestsData
     {
+        public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers
+built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
+configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl
+libavutil      56. 51.100 / 56. 51.100
+libavcodec     58. 91.100 / 58. 91.100
+libavformat    58. 45.100 / 58. 45.100
+libavdevice    58. 10.100 / 58. 10.100
+libavfilter     7. 85.100 /  7. 85.100
+libswscale      5.  7.100 /  5.  7.100
+libswresample   3.  7.100 /  3.  7.100
+libpostproc    55.  7.100 / 55.  7.100";
+
         public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers
 built with gcc 9.1.1 (GCC) 20190807
 configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt

+ 3 - 3
tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj

@@ -14,9 +14,9 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="AutoFixture" Version="4.11.0" />
-    <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
-    <PackageReference Include="Moq" Version="4.14.3" />
+    <PackageReference Include="AutoFixture" Version="4.13.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
+    <PackageReference Include="Moq" Version="4.14.5" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />

+ 7 - 0
tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

@@ -7,12 +7,19 @@ namespace Jellyfin.Server.Implementations.Tests.Library
     {
         [Theory]
         [InlineData("/media/small.jpg", true)]
+        [InlineData("/media/albumart.jpg", true)]
+        [InlineData("/media/movie.sample.mp4", true)]
         [InlineData("/media/movies/#Recycle/test.txt", true)]
         [InlineData("/media/movies/#recycle/", true)]
+        [InlineData("/media/movies/#recycle", true)]
         [InlineData("thumbs.db", true)]
         [InlineData(@"C:\media\movies\movie.avi", false)]
         [InlineData("/media/.hiddendir/file.mp4", true)]
         [InlineData("/media/dir/.hiddenfile.mp4", true)]
+        [InlineData("/volume1/video/Series/@eaDir", true)]
+        [InlineData("/volume1/video/Series/@eaDir/file.txt", true)]
+        [InlineData("/directory/@Recycle", true)]
+        [InlineData("/directory/@Recycle/file.mp3", true)]
         public void PathIgnored(string path, bool expected)
         {
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));