Browse Source

Merge branch 'master' into feature/ffmpeg-version-check

Max Git 5 years ago
parent
commit
3588ee5229
71 changed files with 883 additions and 401 deletions
  1. 131 0
      .ci/azure-pipelines-package.yml
  2. 2 0
      .ci/azure-pipelines.yml
  3. 1 2
      .gitignore
  4. 6 6
      .vscode/launch.json
  5. 6 1
      .vscode/tasks.json
  6. 1 3
      Emby.Server.Implementations/ApplicationHost.cs
  7. 0 56
      Emby.Server.Implementations/Collections/CollectionManager.cs
  8. 0 57
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  9. 6 3
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  10. 83 103
      Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs
  11. 11 1
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  12. 39 5
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  13. 19 10
      Emby.Server.Implementations/Library/LibraryManager.cs
  14. 3 3
      Emby.Server.Implementations/Localization/Core/ar.json
  15. 1 1
      Emby.Server.Implementations/Localization/Core/es-AR.json
  16. 1 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  17. 33 19
      Emby.Server.Implementations/Localization/Core/hr.json
  18. 25 1
      Emby.Server.Implementations/Localization/Core/ne.json
  19. 4 3
      Emby.Server.Implementations/Localization/Core/pt.json
  20. 3 0
      Emby.Server.Implementations/Net/SocketFactory.cs
  21. 2 1
      Emby.Server.Implementations/Session/SessionManager.cs
  22. 0 1
      Jellyfin.Api/Controllers/StartupController.cs
  23. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  24. 15 5
      Jellyfin.Server.Implementations/Users/UserManager.cs
  25. 5 0
      Jellyfin.Server/StartupOptions.cs
  26. 10 33
      MediaBrowser.Api/SyncPlay/SyncPlayService.cs
  27. 2 9
      MediaBrowser.Api/UserService.cs
  28. 0 2
      MediaBrowser.Controller/Configuration/IServerConfigurationManager.cs
  29. 0 2
      MediaBrowser.Controller/Entities/BaseItem.cs
  30. 1 1
      MediaBrowser.Controller/Entities/UserView.cs
  31. 3 2
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  32. 5 0
      MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
  33. 1 3
      MediaBrowser.Controller/Library/ILibraryManager.cs
  34. 2 2
      MediaBrowser.Controller/Library/IUserManager.cs
  35. 31 1
      MediaBrowser.Controller/Providers/IExternalId.cs
  36. 15 0
      MediaBrowser.Controller/Session/SessionInfo.cs
  37. 1 1
      MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
  38. 4 3
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  39. 6 2
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  40. 21 11
      MediaBrowser.Model/Providers/ExternalIdInfo.cs
  41. 71 0
      MediaBrowser.Model/Providers/ExternalIdMediaType.cs
  42. 4 3
      MediaBrowser.Providers/Manager/ProviderManager.cs
  43. 9 2
      MediaBrowser.Providers/Movies/MovieExternalIds.cs
  44. 5 1
      MediaBrowser.Providers/Music/MusicExternalIds.cs
  45. 17 4
      MediaBrowser.Providers/Plugins/AudioDb/ExternalIds.cs
  46. 25 6
      MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
  47. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
  48. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
  49. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
  50. 8 3
      MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
  51. 17 4
      MediaBrowser.Providers/TV/TvExternalIds.cs
  52. 2 2
      README.md
  53. 0 1
      RSSDP/SsdpCommunicationsServer.cs
  54. 15 0
      deployment/Dockerfile.docker.amd64
  55. 15 0
      deployment/Dockerfile.docker.arm64
  56. 15 0
      deployment/Dockerfile.docker.armhf
  57. 16 0
      deployment/build.centos.amd64
  58. 15 0
      deployment/build.debian.amd64
  59. 15 0
      deployment/build.debian.arm64
  60. 15 0
      deployment/build.debian.armhf
  61. 16 0
      deployment/build.fedora.amd64
  62. 5 1
      deployment/build.linux.amd64
  63. 5 1
      deployment/build.macos
  64. 5 1
      deployment/build.portable
  65. 15 0
      deployment/build.ubuntu.amd64
  66. 15 0
      deployment/build.ubuntu.arm64
  67. 15 0
      deployment/build.ubuntu.armhf
  68. 5 1
      deployment/build.windows.amd64
  69. 4 4
      tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
  70. 3 3
      tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
  71. 7 0
      tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs

+ 131 - 0
.ci/azure-pipelines-package.yml

@@ -0,0 +1,131 @@
+jobs:
+- job: BuildPackage
+  displayName: 'Build Packages'
+
+  strategy:
+    matrix:
+      CentOS.amd64:
+        BuildConfiguration: centos.amd64
+      Fedora.amd64:
+        BuildConfiguration: fedora.amd64
+      Debian.amd64:
+        BuildConfiguration: debian.amd64
+      Debian.arm64:
+        BuildConfiguration: debian.arm64
+      Debian.armhf:
+        BuildConfiguration: debian.armhf
+      Ubuntu.amd64:
+        BuildConfiguration: ubuntu.amd64
+      Ubuntu.arm64:
+        BuildConfiguration: ubuntu.arm64
+      Ubuntu.armhf:
+        BuildConfiguration: ubuntu.armhf
+      Linux.amd64:
+        BuildConfiguration: linux.amd64
+      Windows.amd64:
+        BuildConfiguration: windows.amd64
+      MacOS:
+        BuildConfiguration: macos
+      Portable:
+        BuildConfiguration: portable
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  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)'
+    condition: 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="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
+    displayName: 'Run Dockerfile (stable)'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+
+  - 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: 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'
+      contents: '**'
+      targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
+- job: BuildDocker
+  displayName: 'Build Docker'
+
+  strategy:
+    matrix:
+      amd64:
+        BuildConfiguration: amd64
+      arm64:
+        BuildConfiguration: arm64
+      armhf:
+        BuildConfiguration: armhf
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: Docker@2
+    displayName: 'Push Unstable Image'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      repository: 'jellyfin/jellyfin-server'
+      command: buildAndPush
+      buildContext: '.'
+      Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
+      containerRegistry: Docker Hub
+      tags: |
+        unstable-$(Build.BuildNumber)-$(BuildConfiguration)
+        unstable-$(BuildConfiguration)
+
+  - task: Docker@2
+    displayName: 'Push Stable Image'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    inputs:
+      repository: 'jellyfin/jellyfin-server'
+      command: buildAndPush
+      buildContext: '.'
+      Dockerfile: 'deployment/Dockerfile.docker.$(BuildConfiguration)'
+      containerRegistry: Docker Hub
+      tags: |
+        stable-$(Build.BuildNumber)-$(BuildConfiguration)
+        stable-$(BuildConfiguration)
+
+- job: CollectArtifacts
+  displayName: 'Collect Artifacts'
+  dependsOn:
+  - BuildPackage
+  - BuildDocker
+  condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: SSH@0
+    displayName: 'Update Unstable Repository'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable'
+
+  - task: SSH@0
+    displayName: 'Update Stable Repository'
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)'

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

@@ -43,3 +43,5 @@ jobs:
           NugetPackageName: Jellyfin.Common
           NugetPackageName: Jellyfin.Common
           AssemblyFileName: MediaBrowser.Common.dll
           AssemblyFileName: MediaBrowser.Common.dll
       LinuxImage: 'ubuntu-latest'
       LinuxImage: 'ubuntu-latest'
+
+  - template: azure-pipelines-package.yml

+ 1 - 2
.gitignore

@@ -39,7 +39,6 @@ ProgramData*/
 CorePlugins*/
 CorePlugins*/
 ProgramData-Server*/
 ProgramData-Server*/
 ProgramData-UI*/
 ProgramData-UI*/
-MediaBrowser.WebDashboard/jellyfin-web/**
 
 
 #################
 #################
 ## Visual Studio
 ## Visual Studio
@@ -276,4 +275,4 @@ BenchmarkDotNet.Artifacts
 # Ignore web artifacts from native builds
 # Ignore web artifacts from native builds
 web/
 web/
 web-src.*
 web-src.*
-MediaBrowser.WebDashboard/jellyfin-web/
+MediaBrowser.WebDashboard/jellyfin-web

+ 6 - 6
.vscode/launch.json

@@ -1,9 +1,6 @@
 {
 {
-   // Use IntelliSense to find out which attributes exist for C# debugging
-   // Use hover for the description of the existing attributes
-   // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
-   "version": "0.2.0",
-   "configurations": [
+    "version": "0.2.0",
+    "configurations": [
         {
         {
             "name": ".NET Core Launch (console)",
             "name": ".NET Core Launch (console)",
             "type": "coreclr",
             "type": "coreclr",
@@ -24,5 +21,8 @@
             "request": "attach",
             "request": "attach",
             "processId": "${command:pickProcess}"
             "processId": "${command:pickProcess}"
         }
         }
-    ,]
+    ],
+    "env": {
+        "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
+    }
 }
 }

+ 6 - 1
.vscode/tasks.json

@@ -21,5 +21,10 @@
             ],
             ],
             "problemMatcher": "$msCompile"
             "problemMatcher": "$msCompile"
         }
         }
-    ]
+    ],
+    "options": {
+        "env": {
+            "DOTNET_CLI_TELEMETRY_OPTOUT": "1"
+        }
+    }
 }
 }

+ 1 - 3
Emby.Server.Implementations/ApplicationHost.cs

@@ -566,10 +566,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
             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: 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.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
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
             serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
             serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));

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

@@ -363,60 +363,4 @@ namespace Emby.Server.Implementations.Collections
             return results.Values;
             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)
             if (!string.IsNullOrWhiteSpace(newPath)
                 && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
                 && !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
             {
             {
-                // Validate
                 if (!File.Exists(newPath))
                 if (!File.Exists(newPath))
                 {
                 {
                     throw new FileNotFoundException(
                     throw new FileNotFoundException(
@@ -133,7 +132,6 @@ namespace Emby.Server.Implementations.Configuration
             if (!string.IsNullOrWhiteSpace(newPath)
             if (!string.IsNullOrWhiteSpace(newPath)
                 && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
                 && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal))
             {
             {
-                // Validate
                 if (!Directory.Exists(newPath))
                 if (!Directory.Exists(newPath))
                 {
                 {
                     throw new DirectoryNotFoundException(
                     throw new DirectoryNotFoundException(
@@ -146,60 +144,5 @@ namespace Emby.Server.Implementations.Configuration
                 EnsureWriteAccess(newPath);
                 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;
-        }
     }
     }
 }
 }

+ 6 - 3
Emby.Server.Implementations/HttpServer/HttpResultFactory.cs

@@ -585,7 +585,7 @@ namespace Emby.Server.Implementations.HttpServer
 
 
             if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
             if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue)
             {
             {
-                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest, _logger)
+                var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest)
                 {
                 {
                     OnComplete = options.OnComplete
                     OnComplete = options.OnComplete
                 };
                 };
@@ -622,8 +622,11 @@ namespace Emby.Server.Implementations.HttpServer
         /// <summary>
         /// <summary>
         /// Adds the caching responseHeaders.
         /// Adds the caching responseHeaders.
         /// </summary>
         /// </summary>
-        private void AddCachingHeaders(IDictionary<string, string> responseHeaders, TimeSpan? cacheDuration,
-            bool noCache, DateTime? lastModifiedDate)
+        private void AddCachingHeaders(
+            IDictionary<string, string> responseHeaders,
+            TimeSpan? cacheDuration,
+            bool noCache,
+            DateTime? lastModifiedDate)
         {
         {
             if (noCache)
             if (noCache)
             {
             {

+ 83 - 103
Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
@@ -8,52 +9,17 @@ using System.Net;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.Services;
-using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 using Microsoft.Net.Http.Headers;
 
 
 namespace Emby.Server.Implementations.HttpServer
 namespace Emby.Server.Implementations.HttpServer
 {
 {
     public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
     public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult
     {
     {
-        /// <summary>
-        /// Gets or sets the source stream.
-        /// </summary>
-        /// <value>The source stream.</value>
-        private Stream SourceStream { get; set; }
-
-        private string RangeHeader { get; set; }
-
-        private bool IsHeadRequest { get; set; }
-
-        private long RangeStart { get; set; }
-
-        private long RangeEnd { get; set; }
-
-        private long RangeLength { get; set; }
-
-        private long TotalContentLength { get; set; }
-
-        public Action OnComplete { get; set; }
-
-        private readonly ILogger _logger;
-
         private const int BufferSize = 81920;
         private const int BufferSize = 81920;
 
 
-        /// <summary>
-        /// The _options.
-        /// </summary>
         private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
         private readonly Dictionary<string, string> _options = new Dictionary<string, string>();
 
 
-        /// <summary>
-        /// The us culture.
-        /// </summary>
-        private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
-        /// <summary>
-        /// Additional HTTP Headers.
-        /// </summary>
-        /// <value>The headers.</value>
-        public IDictionary<string, string> Headers => _options;
+        private List<KeyValuePair<long, long?>> _requestedRanges;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
         /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class.
@@ -63,8 +29,7 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="source">The source.</param>
         /// <param name="source">The source.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="contentType">Type of the content.</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
         /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param>
-        /// <param name="logger">The logger instance.</param>
-        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest, ILogger logger)
+        public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest)
         {
         {
             if (string.IsNullOrEmpty(contentType))
             if (string.IsNullOrEmpty(contentType))
             {
             {
@@ -74,7 +39,6 @@ namespace Emby.Server.Implementations.HttpServer
             RangeHeader = rangeHeader;
             RangeHeader = rangeHeader;
             SourceStream = source;
             SourceStream = source;
             IsHeadRequest = isHeadRequest;
             IsHeadRequest = isHeadRequest;
-            this._logger = logger;
 
 
             ContentType = contentType;
             ContentType = contentType;
             Headers[HeaderNames.ContentType] = contentType;
             Headers[HeaderNames.ContentType] = contentType;
@@ -85,40 +49,26 @@ namespace Emby.Server.Implementations.HttpServer
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Sets the range values.
+        /// Gets or sets the source stream.
         /// </summary>
         /// </summary>
-        private void SetRangeValues(long contentLength)
-        {
-            var requestedRange = RequestedRanges[0];
-
-            TotalContentLength = contentLength;
-
-            // If the requested range is "0-", we can optimize by just doing a stream copy
-            if (!requestedRange.Value.HasValue)
-            {
-                RangeEnd = TotalContentLength - 1;
-            }
-            else
-            {
-                RangeEnd = requestedRange.Value.Value;
-            }
-
-            RangeStart = requestedRange.Key;
-            RangeLength = 1 + RangeEnd - RangeStart;
+        /// <value>The source stream.</value>
+        private Stream SourceStream { get; set; }
+        private string RangeHeader { get; set; }
+        private bool IsHeadRequest { get; set; }
 
 
-            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
-            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
+        private long RangeStart { get; set; }
+        private long RangeEnd { get; set; }
+        private long RangeLength { get; set; }
+        private long TotalContentLength { get; set; }
 
 
-            if (RangeStart > 0 && SourceStream.CanSeek)
-            {
-                SourceStream.Position = RangeStart;
-            }
-        }
+        public Action OnComplete { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// The _requested ranges.
+        /// Additional HTTP Headers
         /// </summary>
         /// </summary>
-        private List<KeyValuePair<long, long?>> _requestedRanges;
+        /// <value>The headers.</value>
+        public IDictionary<string, string> Headers => _options;
+
         /// <summary>
         /// <summary>
         /// Gets the requested ranges.
         /// Gets the requested ranges.
         /// </summary>
         /// </summary>
@@ -143,12 +93,12 @@ namespace Emby.Server.Implementations.HttpServer
 
 
                         if (!string.IsNullOrEmpty(vals[0]))
                         if (!string.IsNullOrEmpty(vals[0]))
                         {
                         {
-                            start = long.Parse(vals[0], UsCulture);
+                            start = long.Parse(vals[0], CultureInfo.InvariantCulture);
                         }
                         }
 
 
                         if (!string.IsNullOrEmpty(vals[1]))
                         if (!string.IsNullOrEmpty(vals[1]))
                         {
                         {
-                            end = long.Parse(vals[1], UsCulture);
+                            end = long.Parse(vals[1], CultureInfo.InvariantCulture);
                         }
                         }
 
 
                         _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
                         _requestedRanges.Add(new KeyValuePair<long, long?>(start, end));
@@ -159,6 +109,51 @@ namespace Emby.Server.Implementations.HttpServer
             }
             }
         }
         }
 
 
+        public string ContentType { get; set; }
+
+        public IRequest RequestContext { get; set; }
+
+        public object Response { get; set; }
+
+        public int Status { get; set; }
+
+        public HttpStatusCode StatusCode
+        {
+            get => (HttpStatusCode)Status;
+            set => Status = (int)value;
+        }
+
+        /// <summary>
+        /// Sets the range values.
+        /// </summary>
+        private void SetRangeValues(long contentLength)
+        {
+            var requestedRange = RequestedRanges[0];
+
+            TotalContentLength = contentLength;
+
+            // If the requested range is "0-", we can optimize by just doing a stream copy
+            if (!requestedRange.Value.HasValue)
+            {
+                RangeEnd = TotalContentLength - 1;
+            }
+            else
+            {
+                RangeEnd = requestedRange.Value.Value;
+            }
+
+            RangeStart = requestedRange.Key;
+            RangeLength = 1 + RangeEnd - RangeStart;
+
+            Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
+            Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
+
+            if (RangeStart > 0 && SourceStream.CanSeek)
+            {
+                SourceStream.Position = RangeStart;
+            }
+        }
+
         public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
         public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken)
         {
         {
             try
             try
@@ -174,59 +169,44 @@ namespace Emby.Server.Implementations.HttpServer
                     // If the requested range is "0-", we can optimize by just doing a stream copy
                     // If the requested range is "0-", we can optimize by just doing a stream copy
                     if (RangeEnd >= TotalContentLength - 1)
                     if (RangeEnd >= TotalContentLength - 1)
                     {
                     {
-                        await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+                        await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false);
                     }
                     }
                     else
                     else
                     {
                     {
-                        await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false);
+                        await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false);
                     }
                     }
                 }
                 }
             }
             }
             finally
             finally
             {
             {
-                if (OnComplete != null)
-                {
-                    OnComplete();
-                }
+                OnComplete?.Invoke();
             }
             }
         }
         }
 
 
-        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength)
+        private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
         {
         {
-            var array = new byte[BufferSize];
-            int bytesRead;
-            while ((bytesRead = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0)
+            var array = ArrayPool<byte>.Shared.Rent(BufferSize);
+            try
             {
             {
-                if (bytesRead == 0)
+                int bytesRead;
+                while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0)
                 {
                 {
-                    break;
-                }
+                    var bytesToCopy = Math.Min(bytesRead, copyLength);
 
 
-                var bytesToCopy = Math.Min(bytesRead, copyLength);
+                    await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false);
 
 
-                await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false);
+                    copyLength -= bytesToCopy;
 
 
-                copyLength -= bytesToCopy;
-
-                if (copyLength <= 0)
-                {
-                    break;
+                    if (copyLength <= 0)
+                    {
+                        break;
+                    }
                 }
                 }
             }
             }
-        }
-
-        public string ContentType { get; set; }
-
-        public IRequest RequestContext { get; set; }
-
-        public object Response { get; set; }
-
-        public int Status { get; set; }
-
-        public HttpStatusCode StatusCode
-        {
-            get => (HttpStatusCode)Status;
-            set => Status = (int)value;
+            finally
+            {
+                ArrayPool<byte>.Shared.Return(array);
+            }
         }
         }
     }
     }
 }
 }

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

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.IO;
 using System.IO;
+using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
@@ -13,19 +14,28 @@ namespace Emby.Server.Implementations.Library
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     {
     {
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IServerApplicationPaths _serverApplicationPaths;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
         /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
         /// </summary>
         /// </summary>
         /// <param name="libraryManager">The library manager.</param>
         /// <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;
             _libraryManager = libraryManager;
+            _serverApplicationPaths = serverApplicationPaths;
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
         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
             // Don't ignore top level folders
             if (fileInfo.IsDirectory && parent is AggregateFolder)
             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 System.Linq;
 using DotNet.Globbing;
 using DotNet.Globbing;
 
 
@@ -11,7 +14,7 @@ namespace Emby.Server.Implementations.Library
         /// <summary>
         /// <summary>
         /// Files matching these glob patterns will be ignored.
         /// Files matching these glob patterns will be ignored.
         /// </summary>
         /// </summary>
-        public static readonly string[] Patterns = new string[]
+        private static readonly string[] _patterns =
         {
         {
             "**/small.jpg",
             "**/small.jpg",
             "**/albumart.jpg",
             "**/albumart.jpg",
@@ -19,32 +22,51 @@ namespace Emby.Server.Implementations.Library
 
 
             // Directories
             // Directories
             "**/metadata/**",
             "**/metadata/**",
+            "**/metadata",
             "**/ps3_update/**",
             "**/ps3_update/**",
+            "**/ps3_update",
             "**/ps3_vprm/**",
             "**/ps3_vprm/**",
+            "**/ps3_vprm",
             "**/extrafanart/**",
             "**/extrafanart/**",
+            "**/extrafanart",
             "**/extrathumbs/**",
             "**/extrathumbs/**",
+            "**/extrathumbs",
             "**/.actors/**",
             "**/.actors/**",
+            "**/.actors",
             "**/.wd_tv/**",
             "**/.wd_tv/**",
+            "**/.wd_tv",
             "**/lost+found/**",
             "**/lost+found/**",
+            "**/lost+found",
 
 
             // WMC temp recording directories that will constantly be written to
             // WMC temp recording directories that will constantly be written to
             "**/TempRec/**",
             "**/TempRec/**",
+            "**/TempRec",
             "**/TempSBE/**",
             "**/TempSBE/**",
+            "**/TempSBE",
 
 
             // Synology
             // Synology
             "**/eaDir/**",
             "**/eaDir/**",
+            "**/eaDir",
             "**/@eaDir/**",
             "**/@eaDir/**",
+            "**/@eaDir",
             "**/#recycle/**",
             "**/#recycle/**",
+            "**/#recycle",
 
 
             // Qnap
             // Qnap
             "**/@Recycle/**",
             "**/@Recycle/**",
+            "**/@Recycle",
             "**/.@__thumb/**",
             "**/.@__thumb/**",
+            "**/.@__thumb",
             "**/$RECYCLE.BIN/**",
             "**/$RECYCLE.BIN/**",
+            "**/$RECYCLE.BIN",
             "**/System Volume Information/**",
             "**/System Volume Information/**",
+            "**/System Volume Information",
             "**/.grab/**",
             "**/.grab/**",
+            "**/.grab",
 
 
             // Unix hidden files and directories
             // Unix hidden files and directories
             "**/.*/**",
             "**/.*/**",
+            "**/.*",
 
 
             // thumbs.db
             // thumbs.db
             "**/thumbs.db",
             "**/thumbs.db",
@@ -56,19 +78,31 @@ namespace Emby.Server.Implementations.Library
 
 
         private static readonly GlobOptions _globOptions = new GlobOptions
         private static readonly GlobOptions _globOptions = new GlobOptions
         {
         {
-            Evaluation = {
+            Evaluation =
+            {
                 CaseInsensitive = true
                 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>
         /// <summary>
         /// Returns true if the supplied path should be ignored.
         /// Returns true if the supplied path should be ignored.
         /// </summary>
         /// </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();
             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(
         private BaseItem ResolvePath(
             FileSystemMetadata fileInfo,
             FileSystemMetadata fileInfo,
@@ -523,8 +523,7 @@ namespace Emby.Server.Implementations.Library
             IItemResolver[] resolvers,
             IItemResolver[] resolvers,
             Folder parent = null,
             Folder parent = null,
             string collectionType = null,
             string collectionType = null,
-            LibraryOptions libraryOptions = null,
-            bool allowIgnorePath = true)
+            LibraryOptions libraryOptions = null)
         {
         {
             if (fileInfo == null)
             if (fileInfo == null)
             {
             {
@@ -548,7 +547,7 @@ namespace Emby.Server.Implementations.Library
             };
             };
 
 
             // Return null if ignore rules deem that we should do so
             // 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;
                 return null;
             }
             }
@@ -713,7 +712,7 @@ namespace Emby.Server.Implementations.Library
             Directory.CreateDirectory(rootFolderPath);
             Directory.CreateDirectory(rootFolderPath);
 
 
             var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
             var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
-                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath), allowIgnorePath: false))
+                             ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)))
                              .DeepCopy<Folder, AggregateFolder>();
                              .DeepCopy<Folder, AggregateFolder>();
 
 
             // In case program data folder was moved
             // In case program data folder was moved
@@ -795,7 +794,7 @@ namespace Emby.Server.Implementations.Library
                         if (tmpItem == null)
                         if (tmpItem == null)
                         {
                         {
                             _logger.LogDebug("Creating new userRootFolder with DeepCopy");
                             _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
                         // 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
                 try
                 {
                 {

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

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

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

@@ -20,7 +20,7 @@
     "HeaderContinueWatching": "Seguir viendo",
     "HeaderContinueWatching": "Seguir viendo",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteAlbums": "Álbumes favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
-    "HeaderFavoriteEpisodes": "Episodios favoritos",
+    "HeaderFavoriteEpisodes": "Capítulos favoritos",
     "HeaderFavoriteShows": "Programas favoritos",
     "HeaderFavoriteShows": "Programas favoritos",
     "HeaderFavoriteSongs": "Canciones favoritas",
     "HeaderFavoriteSongs": "Canciones favoritas",
     "HeaderLiveTV": "TV en vivo",
     "HeaderLiveTV": "TV en vivo",

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

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

+ 33 - 19
Emby.Server.Implementations/Localization/Core/hr.json

@@ -5,23 +5,23 @@
     "Artists": "Izvođači",
     "Artists": "Izvođači",
     "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
     "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
     "Books": "Knjige",
     "Books": "Knjige",
-    "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
+    "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
     "Channels": "Kanali",
     "Channels": "Kanali",
     "ChapterNameValue": "Poglavlje {0}",
     "ChapterNameValue": "Poglavlje {0}",
     "Collections": "Kolekcije",
     "Collections": "Kolekcije",
     "DeviceOfflineWithName": "{0} se odspojilo",
     "DeviceOfflineWithName": "{0} se odspojilo",
     "DeviceOnlineWithName": "{0} je spojeno",
     "DeviceOnlineWithName": "{0} je spojeno",
     "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
     "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
-    "Favorites": "Omiljeni",
+    "Favorites": "Favoriti",
     "Folders": "Mape",
     "Folders": "Mape",
     "Genres": "Žanrovi",
     "Genres": "Žanrovi",
-    "HeaderAlbumArtists": "Izvođači albuma",
-    "HeaderCameraUploads": "Camera Uploads",
-    "HeaderContinueWatching": "Continue Watching",
+    "HeaderAlbumArtists": "Izvođači na albumu",
+    "HeaderCameraUploads": "Uvoz sa kamere",
+    "HeaderContinueWatching": "Nastavi gledati",
     "HeaderFavoriteAlbums": "Omiljeni albumi",
     "HeaderFavoriteAlbums": "Omiljeni albumi",
     "HeaderFavoriteArtists": "Omiljeni izvođači",
     "HeaderFavoriteArtists": "Omiljeni izvođači",
     "HeaderFavoriteEpisodes": "Omiljene epizode",
     "HeaderFavoriteEpisodes": "Omiljene epizode",
-    "HeaderFavoriteShows": "Omiljene emisije",
+    "HeaderFavoriteShows": "Omiljene serije",
     "HeaderFavoriteSongs": "Omiljene pjesme",
     "HeaderFavoriteSongs": "Omiljene pjesme",
     "HeaderLiveTV": "TV uživo",
     "HeaderLiveTV": "TV uživo",
     "HeaderNextUp": "Sljedeće je",
     "HeaderNextUp": "Sljedeće je",
@@ -34,23 +34,23 @@
     "LabelRunningTimeValue": "Vrijeme rada: {0}",
     "LabelRunningTimeValue": "Vrijeme rada: {0}",
     "Latest": "Najnovije",
     "Latest": "Najnovije",
     "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
     "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
-    "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
+    "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
     "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
     "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
     "MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
     "MixedContent": "Miješani sadržaj",
     "MixedContent": "Miješani sadržaj",
     "Movies": "Filmovi",
     "Movies": "Filmovi",
     "Music": "Glazba",
     "Music": "Glazba",
     "MusicVideos": "Glazbeni spotovi",
     "MusicVideos": "Glazbeni spotovi",
-    "NameInstallFailed": "{0} installation failed",
+    "NameInstallFailed": "{0} neuspješnih instalacija",
     "NameSeasonNumber": "Sezona {0}",
     "NameSeasonNumber": "Sezona {0}",
-    "NameSeasonUnknown": "Season Unknown",
-    "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
+    "NameSeasonUnknown": "Nepoznata sezona",
+    "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
     "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
     "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
     "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
     "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
     "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
     "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
     "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
     "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
     "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
     "NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
-    "NotificationOptionInstallationFailed": "Instalacija nije izvršena",
+    "NotificationOptionInstallationFailed": "Instalacija neuspješna",
     "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
     "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
     "NotificationOptionPluginError": "Dodatak otkazao",
     "NotificationOptionPluginError": "Dodatak otkazao",
     "NotificationOptionPluginInstalled": "Dodatak instaliran",
     "NotificationOptionPluginInstalled": "Dodatak instaliran",
@@ -62,7 +62,7 @@
     "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
     "NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
     "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
     "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
     "Photos": "Slike",
     "Photos": "Slike",
-    "Playlists": "Popisi",
+    "Playlists": "Popis za reprodukciju",
     "Plugin": "Dodatak",
     "Plugin": "Dodatak",
     "PluginInstalledWithName": "{0} je instalirano",
     "PluginInstalledWithName": "{0} je instalirano",
     "PluginUninstalledWithName": "{0} je deinstalirano",
     "PluginUninstalledWithName": "{0} je deinstalirano",
@@ -70,15 +70,15 @@
     "ProviderValue": "Pružitelj: {0}",
     "ProviderValue": "Pružitelj: {0}",
     "ScheduledTaskFailedWithName": "{0} neuspjelo",
     "ScheduledTaskFailedWithName": "{0} neuspjelo",
     "ScheduledTaskStartedWithName": "{0} pokrenuto",
     "ScheduledTaskStartedWithName": "{0} pokrenuto",
-    "ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
-    "Shows": "Shows",
+    "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
+    "Shows": "Serije",
     "Songs": "Pjesme",
     "Songs": "Pjesme",
     "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
     "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
     "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
     "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
-    "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
+    "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
     "Sync": "Sink.",
     "Sync": "Sink.",
     "System": "Sistem",
     "System": "Sistem",
-    "TvShows": "TV Shows",
+    "TvShows": "Serije",
     "User": "Korisnik",
     "User": "Korisnik",
     "UserCreatedWithName": "Korisnik {0} je stvoren",
     "UserCreatedWithName": "Korisnik {0} je stvoren",
     "UserDeletedWithName": "Korisnik {0} je obrisan",
     "UserDeletedWithName": "Korisnik {0} je obrisan",
@@ -87,10 +87,10 @@
     "UserOfflineFromDevice": "{0} se odspojilo od {1}",
     "UserOfflineFromDevice": "{0} se odspojilo od {1}",
     "UserOnlineFromDevice": "{0} je online od {1}",
     "UserOnlineFromDevice": "{0} je online od {1}",
     "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
     "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
-    "UserPolicyUpdatedWithName": "User policy has been updated for {0}",
+    "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
     "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
     "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
     "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
     "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
-    "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
+    "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
     "ValueSpecialEpisodeName": "Specijal - {0}",
     "ValueSpecialEpisodeName": "Specijal - {0}",
     "VersionNumber": "Verzija {0}",
     "VersionNumber": "Verzija {0}",
     "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
     "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
@@ -100,5 +100,19 @@
     "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
     "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
     "TaskCleanCache": "Očisti priručnu memoriju",
     "TaskCleanCache": "Očisti priručnu memoriju",
     "TasksApplicationCategory": "Aplikacija",
     "TasksApplicationCategory": "Aplikacija",
-    "TasksMaintenanceCategory": "Održavanje"
+    "TasksMaintenanceCategory": "Održavanje",
+    "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
+    "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
+    "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
+    "TaskRefreshChannels": "Osvježi kanale",
+    "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
+    "TaskCleanTranscode": "Očisti direktorij za transkodiranje",
+    "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
+    "TaskUpdatePlugins": "Ažuriraj dodatke",
+    "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
+    "TaskRefreshPeople": "Osvježi ljude",
+    "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
+    "TaskCleanLogs": "Očisti direktorij sa logovima",
+    "TasksChannelsCategory": "Internet kanali",
+    "TasksLibraryCategory": "Biblioteka"
 }
 }

+ 25 - 1
Emby.Server.Implementations/Localization/Core/ne.json

@@ -58,5 +58,29 @@
     "Books": "पुस्तकहरु",
     "Books": "पुस्तकहरु",
     "Artists": "कलाकारहरू",
     "Artists": "कलाकारहरू",
     "Application": "अनुप्रयोगहरू",
     "Application": "अनुप्रयोगहरू",
-    "Albums": "एल्बमहरू"
+    "Albums": "एल्बमहरू",
+    "TasksLibraryCategory": "पुस्तकालय",
+    "TasksApplicationCategory": "अनुप्रयोग",
+    "TasksMaintenanceCategory": "मर्मत",
+    "UserPolicyUpdatedWithName": "प्रयोगकर्ता नीति को लागी अद्यावधिक गरिएको छ {0}",
+    "UserPasswordChangedWithName": "पासवर्ड प्रयोगकर्ताका लागि परिवर्तन गरिएको छ {0}",
+    "UserOnlineFromDevice": "{0} बाट अनलाइन छ {1}",
+    "UserOfflineFromDevice": "{0} बाट विच्छेदन भएको छ {1}",
+    "UserLockedOutWithName": "प्रयोगकर्ता {0} लक गरिएको छ",
+    "UserDeletedWithName": "प्रयोगकर्ता {0} हटाइएको छ",
+    "UserCreatedWithName": "प्रयोगकर्ता {0} सिर्जना गरिएको छ",
+    "User": "प्रयोगकर्ता",
+    "PluginInstalledWithName": "",
+    "StartupEmbyServerIsLoading": "Jellyfin सर्भर लोड हुँदैछ। कृपया छिट्टै फेरि प्रयास गर्नुहोस्।",
+    "Songs": "गीतहरू",
+    "Shows": "शोहरू",
+    "ServerNameNeedsToBeRestarted": "{0} लाई पुन: सुरु गर्नु पर्छ",
+    "ScheduledTaskStartedWithName": "{0} सुरु भयो",
+    "ScheduledTaskFailedWithName": "{0} असफल",
+    "ProviderValue": "प्रदायक: {0}",
+    "Plugin": "प्लगइनहरू",
+    "Playlists": "प्लेलिस्टहरू",
+    "Photos": "तस्बिरहरु",
+    "NotificationOptionVideoPlaybackStopped": "भिडियो प्लेब्याक रोकियो",
+    "NotificationOptionVideoPlayback": "भिडियो प्लेब्याक सुरु भयो"
 }
 }

+ 4 - 3
Emby.Server.Implementations/Localization/Core/pt.json

@@ -101,7 +101,8 @@
     "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
     "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
     "TaskCleanLogs": "Limpar diretório de log",
     "TaskCleanLogs": "Limpar diretório de log",
     "TaskRefreshLibrary": "Escanear biblioteca de mídias",
     "TaskRefreshLibrary": "Escanear biblioteca de mídias",
-    "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
-    "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
-    "TasksChannelsCategory": "Canais de Internet"
+    "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"
 }
 }

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

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

@@ -502,7 +502,8 @@ namespace Emby.Server.Implementations.Session
                 Client = appName,
                 Client = appName,
                 DeviceId = deviceId,
                 DeviceId = deviceId,
                 ApplicationVersion = appVersion,
                 ApplicationVersion = appVersion,
-                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
+                ServerId = _appHost.SystemId
             };
             };
 
 
             var username = user?.Username;
             var username = user?.Username;

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

@@ -36,7 +36,6 @@ namespace Jellyfin.Api.Controllers
         public void CompleteWizard()
         public void CompleteWizard()
         {
         {
             _config.Configuration.IsStartupWizardCompleted = true;
             _config.Configuration.IsStartupWizardCompleted = true;
-            _config.SetOptimalValues();
             _config.SaveConfiguration();
             _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.Authentication" Version="2.2.0" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
     <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
     <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>
 
 
   <ItemGroup>
   <ItemGroup>

+ 15 - 5
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -12,6 +12,7 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Cryptography;
 using MediaBrowser.Common.Cryptography;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
@@ -192,15 +193,15 @@ namespace Jellyfin.Server.Implementations.Users
         }
         }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
-        public void DeleteUser(User user)
+        public void DeleteUser(Guid userId)
         {
         {
+            var dbContext = _dbProvider.CreateContext();
+            var user = dbContext.Users.Find(userId);
             if (user == null)
             if (user == null)
             {
             {
-                throw new ArgumentNullException(nameof(user));
+                throw new ResourceNotFoundException(nameof(userId));
             }
             }
 
 
-            var dbContext = _dbProvider.CreateContext();
-
             if (dbContext.Users.Find(user.Id) == null)
             if (dbContext.Users.Find(user.Id) == null)
             {
             {
                 throw new ArgumentException(string.Format(
                 throw new ArgumentException(string.Format(
@@ -226,9 +227,18 @@ namespace Jellyfin.Server.Implementations.Users
                         CultureInfo.InvariantCulture,
                         CultureInfo.InvariantCulture,
                         "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
                         "The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
                         user.Username),
                         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.Users.Remove(user);
             dbContext.SaveChanges();
             dbContext.SaveChanges();
             OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
             OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));

+ 5 - 0
Jellyfin.Server/StartupOptions.cs

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

+ 10 - 33
MediaBrowser.Api/SyncPlay/SyncPlayService.cs

@@ -11,21 +11,16 @@ using Microsoft.Extensions.Logging;
 
 
 namespace MediaBrowser.Api.SyncPlay
 namespace MediaBrowser.Api.SyncPlay
 {
 {
-    [Route("/SyncPlay/{SessionId}/NewGroup", "POST", Summary = "Create a new SyncPlay group")]
+    [Route("/SyncPlay/NewGroup", "POST", Summary = "Create a new SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayNewGroup : IReturnVoid
     public class SyncPlayNewGroup : 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/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayJoinGroup : IReturnVoid
     public class SyncPlayJoinGroup : IReturnVoid
     {
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the Group id.
         /// Gets or sets the Group id.
         /// </summary>
         /// </summary>
@@ -41,63 +36,48 @@ namespace MediaBrowser.Api.SyncPlay
         public string PlayingItemId { get; set; }
         public string PlayingItemId { get; set; }
     }
     }
 
 
-    [Route("/SyncPlay/{SessionId}/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")]
+    [Route("/SyncPlay/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayLeaveGroup : IReturnVoid
     public class SyncPlayLeaveGroup : 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/ListGroups", "GET", Summary = "List SyncPlay groups")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayListGroups : IReturnVoid
     public class SyncPlayListGroups : IReturnVoid
     {
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the filter item id.
         /// Gets or sets the filter item id.
         /// </summary>
         /// </summary>
         /// <value>The filter item id.</value>
         /// <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; }
         public string FilterItemId { get; set; }
     }
     }
 
 
-    [Route("/SyncPlay/{SessionId}/PlayRequest", "POST", Summary = "Request play in SyncPlay group")]
+    [Route("/SyncPlay/PlayRequest", "POST", Summary = "Request play in SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayPlayRequest : IReturnVoid
     public class SyncPlayPlayRequest : 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/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayPauseRequest : IReturnVoid
     public class SyncPlayPauseRequest : 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/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")]
     [Authenticated]
     [Authenticated]
     public class SyncPlaySeekRequest : IReturnVoid
     public class SyncPlaySeekRequest : 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")]
         [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")]
         public long PositionTicks { get; set; }
         public long PositionTicks { get; set; }
     }
     }
 
 
-    [Route("/SyncPlay/{SessionId}/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
+    [Route("/SyncPlay/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayBufferingRequest : IReturnVoid
     public class SyncPlayBufferingRequest : IReturnVoid
     {
     {
-        [ApiMember(Name = "SessionId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
-        public string SessionId { get; set; }
-
         /// <summary>
         /// <summary>
         /// Gets or sets the date used to pin PositionTicks in time.
         /// Gets or sets the date used to pin PositionTicks in time.
         /// </summary>
         /// </summary>
@@ -116,13 +96,10 @@ namespace MediaBrowser.Api.SyncPlay
         public bool BufferingDone { get; set; }
         public bool BufferingDone { get; set; }
     }
     }
 
 
-    [Route("/SyncPlay/{SessionId}/UpdatePing", "POST", Summary = "Update session ping")]
+    [Route("/SyncPlay/UpdatePing", "POST", Summary = "Update session ping")]
     [Authenticated]
     [Authenticated]
     public class SyncPlayUpdatePing : IReturnVoid
     public class SyncPlayUpdatePing : 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")]
         [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")]
         public double Ping { get; set; }
         public double Ping { get; set; }
     }
     }

+ 2 - 9
MediaBrowser.Api/UserService.cs

@@ -365,15 +365,8 @@ namespace MediaBrowser.Api
 
 
         public Task DeleteAsync(DeleteUser request)
         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;
             return Task.CompletedTask;
         }
         }
 
 

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

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

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

@@ -560,8 +560,6 @@ namespace MediaBrowser.Controller.Entities
         /// <summary>
         /// <summary>
         /// The logger.
         /// The logger.
         /// </summary>
         /// </summary>
-        public static ILoggerFactory LoggerFactory { get; set; }
-
         public static ILogger<BaseItem> Logger { get; set; }
         public static ILogger<BaseItem> Logger { get; set; }
 
 
         public static ILibraryManager LibraryManager { get; set; }
         public static ILibraryManager LibraryManager { get; set; }

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

@@ -68,7 +68,7 @@ namespace MediaBrowser.Controller.Entities
                 parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
                 parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
             }
             }
 
 
-            return new UserViewBuilder(UserViewManager, LibraryManager, LoggerFactory.CreateLogger<UserViewBuilder>(), UserDataManager, TVSeriesManager, ConfigurationManager)
+            return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager, ConfigurationManager)
                 .GetUserItems(parent, this, CollectionType, query);
                 .GetUserItems(parent, this, CollectionType, query);
         }
         }
 
 

+ 3 - 2
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -7,6 +7,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.TV;
+using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.Entities
     {
     {
         private readonly IUserViewManager _userViewManager;
         private readonly IUserViewManager _userViewManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<UserViewBuilder> _logger;
+        private readonly ILogger<BaseItem> _logger;
         private readonly IUserDataManager _userDataManager;
         private readonly IUserDataManager _userDataManager;
         private readonly ITVSeriesManager _tvSeriesManager;
         private readonly ITVSeriesManager _tvSeriesManager;
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
@@ -30,7 +31,7 @@ namespace MediaBrowser.Controller.Entities
         public UserViewBuilder(
         public UserViewBuilder(
             IUserViewManager userViewManager,
             IUserViewManager userViewManager,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
-            ILogger<UserViewBuilder> logger,
+            ILogger<BaseItem> logger,
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             ITVSeriesManager tvSeriesManager,
             ITVSeriesManager tvSeriesManager,
             IServerConfigurationManager config)
             IServerConfigurationManager config)

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

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

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

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

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

@@ -1,15 +1,45 @@
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Controller.Providers
 namespace MediaBrowser.Controller.Providers
 {
 {
+    /// <summary>
+    /// Represents an identifier for an external provider.
+    /// </summary>
     public interface IExternalId
     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; }
         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; }
         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);
         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>
         /// <value>The name of the device.</value>
         public string DeviceName { get; set; }
         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>
         /// <summary>
         /// Gets or sets the now playing item.
         /// Gets or sets the now playing item.
         /// </summary>
         /// </summary>
@@ -215,8 +221,17 @@ namespace MediaBrowser.Controller.Session
 
 
         public string PlaylistItemId { get; set; }
         public string PlaylistItemId { get; set; }
 
 
+        public string ServerId { get; set; }
+
         public string UserPrimaryImageTag { 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)
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {
         {
             var controllers = SessionControllers.ToList();
             var controllers = SessionControllers.ToList();

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

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

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

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

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

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

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

@@ -1,26 +1,36 @@
-#nullable disable
-#pragma warning disable CS1591
-
 namespace MediaBrowser.Model.Providers
 namespace MediaBrowser.Model.Providers
 {
 {
+    /// <summary>
+    /// Represents the external id information for serialization to the client.
+    /// </summary>
     public class ExternalIdInfo
     public class ExternalIdInfo
     {
     {
         /// <summary>
         /// <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>
         /// </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>
         /// <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>
         /// </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>
         /// <summary>
         /// Gets or sets the URL format string.
         /// Gets or sets the URL format string.
         /// </summary>
         /// </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
+    }
+}

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

@@ -102,7 +102,7 @@ namespace MediaBrowser.Providers.Manager
 
 
             _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
             _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
             _metadataProviders = metadataProviders.ToArray();
             _metadataProviders = metadataProviders.ToArray();
-            _externalIds = externalIds.OrderBy(i => i.Name).ToArray();
+            _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
 
 
             _savers = metadataSavers.Where(i =>
             _savers = metadataSavers.Where(i =>
             {
             {
@@ -900,7 +900,7 @@ namespace MediaBrowser.Providers.Manager
 
 
                 return new ExternalUrl
                 return new ExternalUrl
                 {
                 {
-                    Name = i.Name,
+                    Name = i.ProviderName,
                     Url = string.Format(
                     Url = string.Format(
                         CultureInfo.InvariantCulture,
                         CultureInfo.InvariantCulture,
                         i.UrlFormatString,
                         i.UrlFormatString,
@@ -914,8 +914,9 @@ namespace MediaBrowser.Providers.Manager
             return GetExternalIds(item)
             return GetExternalIds(item)
                 .Select(i => new ExternalIdInfo
                 .Select(i => new ExternalIdInfo
                 {
                 {
-                    Name = i.Name,
+                    Name = i.ProviderName,
                     Key = i.Key,
                     Key = i.Key,
+                    Type = i.Type,
                     UrlFormatString = i.UrlFormatString
                     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.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Movies
 namespace MediaBrowser.Providers.Movies
 {
 {
     public class ImdbExternalId : IExternalId
     public class ImdbExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "IMDb";
+        public string ProviderName => "IMDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Imdb.ToString();
         public string Key => MetadataProvider.Imdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/title/{0}";
         public string UrlFormatString => "https://www.imdb.com/title/{0}";
 
 
@@ -36,11 +40,14 @@ namespace MediaBrowser.Providers.Movies
     public class ImdbPersonExternalId : IExternalId
     public class ImdbPersonExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "IMDb";
+        public string ProviderName => "IMDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Imdb.ToString();
         public string Key => MetadataProvider.Imdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.imdb.com/name/{0}";
         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.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Music
 namespace MediaBrowser.Providers.Music
 {
 {
     public class ImvdbId : IExternalId
     public class ImvdbId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "IMVDb";
+        public string ProviderName => "IMVDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => "IMVDb";
         public string Key => "IMVDb";
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => null;
         public string UrlFormatString => null;
 
 

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

@@ -3,17 +3,21 @@
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Plugins.AudioDb
 namespace MediaBrowser.Providers.Plugins.AudioDb
 {
 {
     public class AudioDbAlbumExternalId : IExternalId
     public class AudioDbAlbumExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheAudioDb";
+        public string ProviderName => "TheAudioDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
 
@@ -24,11 +28,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbOtherAlbumExternalId : IExternalId
     public class AudioDbOtherAlbumExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheAudioDb Album";
+        public string ProviderName => "TheAudioDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
         public string Key => MetadataProvider.AudioDbAlbum.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
         public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
 
 
@@ -39,11 +46,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbArtistExternalId : IExternalId
     public class AudioDbArtistExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheAudioDb";
+        public string ProviderName => "TheAudioDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbArtist.ToString();
         public string Key => MetadataProvider.AudioDbArtist.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
 
 
@@ -54,11 +64,14 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
     public class AudioDbOtherArtistExternalId : IExternalId
     public class AudioDbOtherArtistExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheAudioDb Artist";
+        public string ProviderName => "TheAudioDb";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.AudioDbArtist.ToString();
         public string Key => MetadataProvider.AudioDbArtist.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
         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.Entities.Audio;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using MediaBrowser.Providers.Plugins.MusicBrainz;
 using MediaBrowser.Providers.Plugins.MusicBrainz;
 
 
 namespace MediaBrowser.Providers.Music
 namespace MediaBrowser.Providers.Music
@@ -10,11 +11,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzReleaseGroupExternalId : IExternalId
     public class MusicBrainzReleaseGroupExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz Release Group";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
         public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
 
 
@@ -25,11 +29,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzAlbumArtistExternalId : IExternalId
     public class MusicBrainzAlbumArtistExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz Album Artist";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
         public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
 
@@ -40,11 +47,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzAlbumExternalId : IExternalId
     public class MusicBrainzAlbumExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz Album";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
         public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
 
 
@@ -55,11 +65,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzArtistExternalId : IExternalId
     public class MusicBrainzArtistExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
 
@@ -70,12 +83,15 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzOtherArtistExternalId : IExternalId
     public class MusicBrainzOtherArtistExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz Artist";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
 
 
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
         public string Key => MetadataProvider.MusicBrainzArtist.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
 
 
@@ -86,11 +102,14 @@ namespace MediaBrowser.Providers.Music
     public class MusicBrainzTrackId : IExternalId
     public class MusicBrainzTrackId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "MusicBrainz Track";
+        public string ProviderName => "MusicBrainz";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.MusicBrainzTrack.ToString();
         public string Key => MetadataProvider.MusicBrainzTrack.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
         public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
 
 

+ 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;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 {
 {
+    /// <summary>
+    /// External ID for a TMDB box set.
+    /// </summary>
     public class TmdbBoxSetExternalId : IExternalId
     public class TmdbBoxSetExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.TmdbCollection.ToString();
         public string Key => MetadataProvider.TmdbCollection.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
         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;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 {
 {
+    /// <summary>
+    /// External ID for a TMBD movie.
+    /// </summary>
     public class TmdbMovieExternalId : IExternalId
     public class TmdbMovieExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
         public string Key => MetadataProvider.Tmdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
 
 

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

@@ -1,19 +1,24 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.People
 namespace MediaBrowser.Providers.Plugins.Tmdb.People
 {
 {
+    /// <summary>
+    /// External ID for a TMDB person.
+    /// </summary>
     public class TmdbPersonExternalId : IExternalId
     public class TmdbPersonExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
         public string Key => MetadataProvider.Tmdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
         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.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 namespace MediaBrowser.Providers.Plugins.Tmdb.TV
 {
 {
+    /// <summary>
+    /// External ID for a TMDB series.
+    /// </summary>
     public class TmdbSeriesExternalId : IExternalId
     public class TmdbSeriesExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => TmdbUtils.ProviderName;
+        public string ProviderName => TmdbUtils.ProviderName;
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tmdb.ToString();
         public string Key => MetadataProvider.Tmdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
         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.Entities.TV;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 using MediaBrowser.Providers.Plugins.TheTvdb;
 
 
 namespace MediaBrowser.Providers.TV
 namespace MediaBrowser.Providers.TV
@@ -10,11 +11,14 @@ namespace MediaBrowser.Providers.TV
     public class Zap2ItExternalId : IExternalId
     public class Zap2ItExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "Zap2It";
+        public string ProviderName => "Zap2It";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Zap2It.ToString();
         public string Key => MetadataProvider.Zap2It.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
         public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
 
 
@@ -25,11 +29,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbExternalId : IExternalId
     public class TvdbExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
         public string Key => MetadataProvider.Tvdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => null;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
         public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}";
 
 
@@ -40,11 +47,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbSeasonExternalId : IExternalId
     public class TvdbSeasonExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
         public string Key => MetadataProvider.Tvdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Season;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => null;
         public string UrlFormatString => null;
 
 
@@ -55,11 +65,14 @@ namespace MediaBrowser.Providers.TV
     public class TvdbEpisodeExternalId : IExternalId
     public class TvdbEpisodeExternalId : IExternalId
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
-        public string Name => "TheTVDB";
+        public string ProviderName => "TheTVDB";
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Key => MetadataProvider.Tvdb.ToString();
         public string Key => MetadataProvider.Tvdb.ToString();
 
 
+        /// <inheritdoc />
+        public ExternalIdMediaType? Type => ExternalIdMediaType.Episode;
+
         /// <inheritdoc />
         /// <inheritdoc />
         public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}";
         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">
 <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"/>
 <img alt="Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-core/svg-badge.svg"/>
 </a>
 </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>
 <a href="https://hub.docker.com/r/jellyfin/jellyfin">
 <a href="https://hub.docker.com/r/jellyfin/jellyfin">
 <img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/>
 <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()
         private ISocket ListenForBroadcastsAsync()
         {
         {
             var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort);
             var socket = _SocketFactory.CreateUdpMulticastSocket(SsdpConstants.MulticastLocalAdminAddress, _MulticastTtl, SsdpConstants.MulticastPort);
-
             _ = ListenToSocketInternal(socket);
             _ = ListenToSocketInternal(socket);
 
 
             return socket;
             return socket;

+ 15 - 0
deployment/Dockerfile.docker.amd64

@@ -0,0 +1,15 @@
+ARG DOTNET_VERSION=3.1
+
+FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+
+ARG SOURCE_DIR=/src
+ARG ARTIFACT_DIR=/jellyfin
+
+WORKDIR ${SOURCE_DIR}
+COPY . .
+
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"

+ 15 - 0
deployment/Dockerfile.docker.arm64

@@ -0,0 +1,15 @@
+ARG DOTNET_VERSION=3.1
+
+FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+
+ARG SOURCE_DIR=/src
+ARG ARTIFACT_DIR=/jellyfin
+
+WORKDIR ${SOURCE_DIR}
+COPY . .
+
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"

+ 15 - 0
deployment/Dockerfile.docker.armhf

@@ -0,0 +1,15 @@
+ARG DOTNET_VERSION=3.1
+
+FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster
+
+ARG SOURCE_DIR=/src
+ARG ARTIFACT_DIR=/jellyfin
+
+WORKDIR ${SOURCE_DIR}
+COPY . .
+
+ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
+
+# because of changes in docker and systemd we need to not build in parallel at the moment
+# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none"

+ 16 - 0
deployment/build.centos.amd64

@@ -8,6 +8,22 @@ set -o xtrace
 # Move to source directory
 # Move to source directory
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd fedora
+
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    sed -i "s/Version:.*/Version:        ${BUILD_ID}/" jellyfin.spec
+    sed -i "/%changelog/q" jellyfin.spec
+
+    cat <<EOF >>jellyfin.spec
+* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
+- Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+EOF
+    popd
+fi
+
 # Build RPM
 # Build RPM
 make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
 make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
 rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
 rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm

+ 15 - 0
deployment/build.debian.amd64

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 dpkg-buildpackage -us -uc --pre-clean --post-clean
 dpkg-buildpackage -us -uc --pre-clean --post-clean
 
 

+ 15 - 0
deployment/build.debian.arm64

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
 dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean

+ 15 - 0
deployment/build.debian.armhf

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
 dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean

+ 16 - 0
deployment/build.fedora.amd64

@@ -8,6 +8,22 @@ set -o xtrace
 # Move to source directory
 # Move to source directory
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd fedora
+
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    sed -i "s/Version:.*/Version:        ${BUILD_ID}/" jellyfin.spec
+    sed -i "/%changelog/q" jellyfin.spec
+
+    cat <<EOF >>jellyfin.spec
+* $( LANG=C date '+%a %b %d %Y' ) Jellyfin Packaging Team <packaging@jellyfin.org>
+- Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+EOF
+    popd
+fi
+
 # Build RPM
 # Build RPM
 make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
 make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS
 rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm
 rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm

+ 5 - 1
deployment/build.linux.amd64

@@ -9,7 +9,11 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 # Get version
 # Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    version="${BUILD_ID}"
+else
+    version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
 
 
 # Build archives
 # Build archives
 dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
 dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"

+ 5 - 1
deployment/build.macos

@@ -9,7 +9,11 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 # Get version
 # Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    version="${BUILD_ID}"
+else
+    version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
 
 
 # Build archives
 # Build archives
 dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
 dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"

+ 5 - 1
deployment/build.portable

@@ -9,7 +9,11 @@ set -o xtrace
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 # Get version
 # Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    version="${BUILD_ID}"
+else
+    version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
 
 
 # Build archives
 # Build archives
 dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"
 dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true"

+ 15 - 0
deployment/build.ubuntu.amd64

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 dpkg-buildpackage -us -uc --pre-clean --post-clean
 dpkg-buildpackage -us -uc --pre-clean --post-clean
 
 

+ 15 - 0
deployment/build.ubuntu.arm64

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean
 dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean

+ 15 - 0
deployment/build.ubuntu.armhf

@@ -14,6 +14,21 @@ if [[ ${IS_DOCKER} == YES ]]; then
     sed -i '/dotnet-sdk-3.1,/d' debian/control
     sed -i '/dotnet-sdk-3.1,/d' debian/control
 fi
 fi
 
 
+# Modify changelog to unstable configuration if IS_UNSTABLE
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    pushd debian
+    PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' )
+
+    cat <<EOF >changelog
+jellyfin-server (${BUILD_ID}-unstable) unstable; urgency=medium
+
+  * Jellyfin Server unstable build ${BUILD_ID} for merged PR #${PR_ID}
+
+ -- Jellyfin Packaging Team <packaging@jellyfin.org>  $( date --rfc-2822 )
+EOF
+    popd
+fi
+
 # Build DEB
 # Build DEB
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH}
 dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean
 dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean

+ 5 - 1
deployment/build.windows.amd64

@@ -15,7 +15,11 @@ FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip
 pushd ${SOURCE_DIR}
 pushd ${SOURCE_DIR}
 
 
 # Get version
 # Get version
-version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+if [[ ${IS_UNSTABLE} == 'yes' ]]; then
+    version="${BUILD_ID}"
+else
+    version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )"
+fi
 
 
 output_dir="dist/jellyfin-server_${version}"
 output_dir="dist/jellyfin-server_${version}"
 
 

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

@@ -13,15 +13,15 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <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.12.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.Extensions.Options" Version="3.1.5" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
-    <PackageReference Include="Moq" Version="4.14.3" />
+    <PackageReference Include="Moq" Version="4.14.4" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers -->
   <!-- Code Analyzers -->

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

@@ -14,9 +14,9 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <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.12.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" />
+    <PackageReference Include="Moq" Version="4.14.4" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
     <PackageReference Include="coverlet.collector" Version="1.3.0" />
     <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]
         [Theory]
         [InlineData("/media/small.jpg", true)]
         [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/test.txt", true)]
         [InlineData("/media/movies/#recycle/", true)]
         [InlineData("/media/movies/#recycle/", true)]
+        [InlineData("/media/movies/#recycle", true)]
         [InlineData("thumbs.db", true)]
         [InlineData("thumbs.db", true)]
         [InlineData(@"C:\media\movies\movie.avi", false)]
         [InlineData(@"C:\media\movies\movie.avi", false)]
         [InlineData("/media/.hiddendir/file.mp4", true)]
         [InlineData("/media/.hiddendir/file.mp4", true)]
         [InlineData("/media/dir/.hiddenfile.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)
         public void PathIgnored(string path, bool expected)
         {
         {
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
             Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));