Explorar o código

Merge branch 'master' into network-rewrite

Shadowghost %!s(int64=2) %!d(string=hai) anos
pai
achega
4fc52a840c
Modificáronse 100 ficheiros con 768 adicións e 587 borrados
  1. 2 0
      .ci/azure-pipelines-package.yml
  2. 4 0
      CONTRIBUTORS.md
  3. 3 3
      Dockerfile
  4. 1 4
      Dockerfile.arm
  5. 1 1
      Dockerfile.arm64
  6. 1 1
      Emby.Dlna/Didl/DidlBuilder.cs
  7. 1 1
      Emby.Dlna/DlnaManager.cs
  8. 2 2
      Emby.Dlna/IDlnaEventManager.cs
  9. 17 16
      Emby.Dlna/PlayTo/Device.cs
  10. 108 0
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  11. 0 141
      Emby.Dlna/PlayTo/SsdpHttpClient.cs
  12. 1 1
      Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs
  13. 1 1
      Emby.Photos/Emby.Photos.csproj
  14. 2 10
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  15. 48 2
      Emby.Server.Implementations/ApplicationHost.cs
  16. 1 5
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  17. 6 2
      Emby.Server.Implementations/Dto/DtoService.cs
  18. 2 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  19. 0 1
      Emby.Server.Implementations/HttpServer/Security/SessionContext.cs
  20. 32 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  21. 15 4
      Emby.Server.Implementations/Library/LibraryManager.cs
  22. 4 2
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  23. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  24. 2 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  25. 8 10
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  26. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  27. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  28. 11 11
      Emby.Server.Implementations/Localization/Core/ar.json
  29. 2 1
      Emby.Server.Implementations/Localization/Core/et.json
  30. 12 12
      Emby.Server.Implementations/Localization/Core/fr.json
  31. 2 1
      Emby.Server.Implementations/Localization/Core/hr.json
  32. 4 1
      Emby.Server.Implementations/Localization/Core/ko.json
  33. 1 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  34. 4 3
      Emby.Server.Implementations/Localization/Core/lv.json
  35. 25 3
      Emby.Server.Implementations/Localization/Core/mk.json
  36. 1 1
      Emby.Server.Implementations/Localization/Core/ms.json
  37. 49 49
      Emby.Server.Implementations/Localization/Core/my.json
  38. 3 3
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  39. 9 9
      Emby.Server.Implementations/Localization/Core/pt.json
  40. 3 2
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  41. 5 3
      Emby.Server.Implementations/Localization/Core/sr.json
  42. 11 2
      Emby.Server.Implementations/Localization/Core/ug.json
  43. 4 1
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  44. 10 0
      Emby.Server.Implementations/Localization/Ratings/fi.csv
  45. 6 0
      Emby.Server.Implementations/Localization/Ratings/no.csv
  46. 5 0
      Emby.Server.Implementations/Localization/Ratings/se.csv
  47. 2 2
      Emby.Server.Implementations/Net/UdpSocket.cs
  48. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  49. 1 2
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  50. 18 1
      Emby.Server.Implementations/Session/WebSocketController.cs
  51. 0 1
      Emby.Server.Implementations/Sorting/StudioComparer.cs
  52. 21 32
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  53. 1 6
      Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
  54. 1 6
      Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
  55. 37 0
      Jellyfin.Api/BaseJellyfinApiController.cs
  56. 1 1
      Jellyfin.Api/Controllers/AudioController.cs
  57. 0 1
      Jellyfin.Api/Controllers/ConfigurationController.cs
  58. 2 5
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  59. 4 4
      Jellyfin.Api/Controllers/DlnaServerController.cs
  60. 4 3
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  61. 42 15
      Jellyfin.Api/Controllers/ItemsController.cs
  62. 0 1
      Jellyfin.Api/Controllers/MediaInfoController.cs
  63. 1 1
      Jellyfin.Api/Controllers/MoviesController.cs
  64. 4 1
      Jellyfin.Api/Controllers/PersonsController.cs
  65. 4 4
      Jellyfin.Api/Controllers/QuickConnectController.cs
  66. 8 6
      Jellyfin.Api/Controllers/SearchController.cs
  67. 5 4
      Jellyfin.Api/Controllers/TrailersController.cs
  68. 5 5
      Jellyfin.Api/Controllers/TvShowsController.cs
  69. 38 59
      Jellyfin.Api/Controllers/UniversalAudioController.cs
  70. 13 10
      Jellyfin.Api/Controllers/UserController.cs
  71. 2 2
      Jellyfin.Api/Controllers/UserLibraryController.cs
  72. 2 8
      Jellyfin.Api/Controllers/UserViewsController.cs
  73. 1 1
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  74. 2 2
      Jellyfin.Api/Jellyfin.Api.csproj
  75. 11 4
      Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs
  76. 1 1
      Jellyfin.Api/Models/StreamingDtos/StreamState.cs
  77. 2 2
      Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs
  78. 21 0
      Jellyfin.Api/Results/OkResultOfT.cs
  79. 2 2
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  80. 4 2
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  81. 1 1
      Jellyfin.Drawing.Skia/SkiaHelper.cs
  82. 4 4
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  83. 1 1
      Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs
  84. 1 1
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  85. 4 4
      Jellyfin.Server.Implementations/Users/UserManager.cs
  86. 2 9
      Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs
  87. 5 5
      Jellyfin.Server/Jellyfin.Server.csproj
  88. 1 1
      Jellyfin.Server/Program.cs
  89. 17 0
      Jellyfin.Server/Startup.cs
  90. 18 7
      MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs
  91. 0 2
      MediaBrowser.Common/Configuration/IApplicationPaths.cs
  92. 5 0
      MediaBrowser.Common/Net/NamedClient.cs
  93. 2 2
      MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs
  94. 2 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  95. 1 3
      MediaBrowser.Controller/Entities/BasePluginFolder.cs
  96. 4 4
      MediaBrowser.Controller/Entities/Extensions.cs
  97. 4 26
      MediaBrowser.Controller/Entities/Folder.cs
  98. 1 1
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  99. 1 1
      MediaBrowser.Controller/Entities/TV/Season.cs
  100. 1 1
      MediaBrowser.Controller/Entities/TV/Series.cs

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

@@ -26,6 +26,8 @@ jobs:
         BuildConfiguration: linux.amd64-musl
       Linux.arm64:
         BuildConfiguration: linux.arm64
+      Linux.musl-linux-arm64:
+        BuildConfiguration: linux.musl-linux-arm64
       Linux.armhf:
         BuildConfiguration: linux.armhf
       Windows.amd64:

+ 4 - 0
CONTRIBUTORS.md

@@ -36,6 +36,7 @@
  - [dmitrylyzo](https://github.com/dmitrylyzo)
  - [DMouse10462](https://github.com/DMouse10462)
  - [DrPandemic](https://github.com/DrPandemic)
+ - [eglia](https://github.com/eglia)
  - [EraYaN](https://github.com/EraYaN)
  - [escabe](https://github.com/escabe)
  - [excelite](https://github.com/excelite)
@@ -147,6 +148,7 @@
  - [xosdy](https://github.com/xosdy)
  - [XVicarious](https://github.com/XVicarious)
  - [YouKnowBlom](https://github.com/YouKnowBlom)
+ - [ZachPhelan](https://github.com/ZachPhelan)
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
@@ -157,6 +159,7 @@
  - [jonas-resch](https://github.com/jonas-resch)
  - [vgambier](https://github.com/vgambier)
  - [MinecraftPlaye](https://github.com/MinecraftPlaye)
+ - [RealGreenDragon](https://github.com/RealGreenDragon)
 
 # Emby Contributors
 
@@ -225,3 +228,4 @@
  - [gnuyent](https://github.com/gnuyent)
  - [Matthew Jones](https://github.com/matthew-jones-uk)
  - [Jakob Kukla](https://github.com/jakobkukla)
+ - [Utku Özdemir](https://github.com/utkuozdemir)

+ 3 - 3
Dockerfile

@@ -31,7 +31,7 @@ ARG LEVEL_ZERO_VERSION=1.3.22549
 # mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
 # curl: healthcheck
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https curl \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget curl \
  && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
  && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
  && apt-get update \
@@ -53,7 +53,7 @@ RUN apt-get update \
  && dpkg -i *.deb \
  && cd .. \
  && rm -rf intel-compute-runtime \
- && apt-get remove gnupg wget apt-transport-https -y \
+ && apt-get remove gnupg wget -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
@@ -72,7 +72,7 @@ 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="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
 

+ 1 - 4
Dockerfile.arm

@@ -38,9 +38,6 @@ RUN apt-get update \
  libssl-dev \
  libfontconfig1 \
  libfreetype6 \
- libomxil-bellagio0 \
- libomxil-bellagio-bin \
- libraspberrypi0 \
  vainfo \
  libva2 \
  locales \
@@ -64,7 +61,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
 

+ 1 - 1
Dockerfile.arm64

@@ -55,7 +55,7 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
 # Discard objs - may cause failures if exists
 RUN find . -type d -name obj | xargs -r rm -r
 # Build
-RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
 

+ 1 - 1
Emby.Dlna/Didl/DidlBuilder.cs

@@ -446,7 +446,7 @@ namespace Emby.Dlna.Didl
         /// </summary>
         /// <remarks>
         /// If context is a season, this will return a string containing just episode number and name.
-        /// Otherwise the result will include series nams and season number.
+        /// Otherwise the result will include series names and season number.
         /// </remarks>
         /// <param name="episode">The episode.</param>
         /// <param name="context">Current context.</param>

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -123,7 +123,7 @@ namespace Emby.Dlna
         /// <summary>
         /// Attempts to match a device with a profile.
         /// Rules:
-        /// - If the profile field has no value, the field matches irregardless of its contents.
+        /// - If the profile field has no value, the field matches regardless of its contents.
         /// - the profile field can be an exact match, or a reg exp.
         /// </summary>
         /// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>

+ 2 - 2
Emby.Dlna/IDlnaEventManager.cs

@@ -16,7 +16,7 @@ namespace Emby.Dlna
         /// </summary>
         /// <param name="subscriptionId">The subscription identifier.</param>
         /// <param name="notificationType">The notification type.</param>
-        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
         /// <param name="callbackUrl">The callback url.</param>
         /// <returns>The response.</returns>
         EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
@@ -25,7 +25,7 @@ namespace Emby.Dlna
         /// Creates the event subscription.
         /// </summary>
         /// <param name="notificationType">The notification type.</param>
-        /// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
+        /// <param name="requestedTimeoutString">The requested timeout as a string.</param>
         /// <param name="callbackUrl">The callback url.</param>
         /// <returns>The response.</returns>
         EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);

+ 17 - 16
Emby.Dlna/PlayTo/Device.cs

@@ -235,7 +235,7 @@ namespace Emby.Dlna.PlayTo
             _logger.LogDebug("Setting mute");
             var value = mute ? 1 : 0;
 
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -276,7 +276,7 @@ namespace Emby.Dlna.PlayTo
             // Remote control will perform better
             Volume = value;
 
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -303,7 +303,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -343,7 +343,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -400,7 +400,8 @@ namespace Emby.Dlna.PlayTo
             }
 
             var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
-            await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
+                .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
                 .ConfigureAwait(false);
         }
 
@@ -428,7 +429,7 @@ namespace Emby.Dlna.PlayTo
                 throw new InvalidOperationException("Unable to find service");
             }
 
-            return new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -461,7 +462,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -485,7 +486,7 @@ namespace Emby.Dlna.PlayTo
 
             var service = GetAvTransportService();
 
-            await new SsdpHttpClient(_httpClientFactory)
+            await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
@@ -618,7 +619,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -668,7 +669,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -701,7 +702,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -747,7 +748,7 @@ namespace Emby.Dlna.PlayTo
                 return null;
             }
 
-            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -819,7 +820,7 @@ namespace Emby.Dlna.PlayTo
                 return (false, null);
             }
 
-            var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
+            var result = await new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
                 command.Name,
@@ -997,7 +998,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClientFactory);
+            var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
 
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
             if (document == null)
@@ -1029,7 +1030,7 @@ namespace Emby.Dlna.PlayTo
 
             string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl);
 
-            var httpClient = new SsdpHttpClient(_httpClientFactory);
+            var httpClient = new DlnaHttpClient(_logger, _httpClientFactory);
             _logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
             var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
             if (document == null)
@@ -1064,7 +1065,7 @@ namespace Emby.Dlna.PlayTo
 
         public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
         {
-            var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
+            var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
 
             var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
             if (document == null)

+ 108 - 0
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -0,0 +1,108 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Globalization;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using System.Xml.Linq;
+using Emby.Dlna.Common;
+using MediaBrowser.Common.Net;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Dlna.PlayTo
+{
+    public class DlnaHttpClient
+    {
+        private readonly ILogger _logger;
+        private readonly IHttpClientFactory _httpClientFactory;
+
+        public DlnaHttpClient(ILogger logger, IHttpClientFactory httpClientFactory)
+        {
+            _logger = logger;
+            _httpClientFactory = httpClientFactory;
+        }
+
+        private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
+        {
+            // If it's already a complete url, don't stick anything onto the front of it
+            if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                return serviceUrl;
+            }
+
+            if (!serviceUrl.StartsWith('/'))
+            {
+                serviceUrl = "/" + serviceUrl;
+            }
+
+            return baseUrl + serviceUrl;
+        }
+
+        private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+        {
+            using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            response.EnsureSuccessStatusCode();
+            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            try
+            {
+                return await XDocument.LoadAsync(
+                    stream,
+                    LoadOptions.None,
+                    cancellationToken).ConfigureAwait(false);
+            }
+            catch (XmlException ex)
+            {
+                _logger.LogError(ex, "Failed to parse response");
+                if (_logger.IsEnabled(LogLevel.Debug))
+                {
+                    _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+                }
+
+                return null;
+            }
+        }
+
+        public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
+        {
+            using var request = new HttpRequestMessage(HttpMethod.Get, url);
+
+            // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
+            return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
+        }
+
+        public async Task<XDocument?> SendCommandAsync(
+            string baseUrl,
+            DeviceService service,
+            string command,
+            string postData,
+            string? header = null,
+            CancellationToken cancellationToken = default)
+        {
+            using var request = new HttpRequestMessage(HttpMethod.Post, NormalizeServiceUrl(baseUrl, service.ControlUrl))
+            {
+                Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml)
+            };
+
+            request.Headers.TryAddWithoutValidation(
+                "SOAPACTION",
+                string.Format(
+                    CultureInfo.InvariantCulture,
+                    "\"{0}#{1}\"",
+                    service.ServiceType,
+                    command));
+            request.Headers.Pragma.ParseAdd("no-cache");
+
+            if (!string.IsNullOrEmpty(header))
+            {
+                request.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
+            }
+
+            // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
+            return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
+        }
+    }
+}

+ 0 - 141
Emby.Dlna/PlayTo/SsdpHttpClient.cs

@@ -1,141 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Net.Http;
-using System.Net.Mime;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Xml.Linq;
-using Emby.Dlna.Common;
-using MediaBrowser.Common.Net;
-
-namespace Emby.Dlna.PlayTo
-{
-    public class SsdpHttpClient
-    {
-        private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50";
-        private const string FriendlyName = "Jellyfin";
-
-        private readonly IHttpClientFactory _httpClientFactory;
-
-        public SsdpHttpClient(IHttpClientFactory httpClientFactory)
-        {
-            _httpClientFactory = httpClientFactory;
-        }
-
-        public async Task<XDocument> SendCommandAsync(
-            string baseUrl,
-            DeviceService service,
-            string command,
-            string postData,
-            string header = null,
-            CancellationToken cancellationToken = default)
-        {
-            var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
-            using var response = await PostSoapDataAsync(
-                    url,
-                    $"\"{service.ServiceType}#{command}\"",
-                    postData,
-                    header,
-                    cancellationToken)
-                .ConfigureAwait(false);
-            response.EnsureSuccessStatusCode();
-
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            return await XDocument.LoadAsync(
-                stream,
-                LoadOptions.None,
-                cancellationToken).ConfigureAwait(false);
-        }
-
-        private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
-        {
-            // If it's already a complete url, don't stick anything onto the front of it
-            if (serviceUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                return serviceUrl;
-            }
-
-            if (!serviceUrl.StartsWith('/'))
-            {
-                serviceUrl = "/" + serviceUrl;
-            }
-
-            return baseUrl + serviceUrl;
-        }
-
-        public async Task SubscribeAsync(
-            string url,
-            string ip,
-            int port,
-            string localIp,
-            int eventport,
-            int timeOut = 3600)
-        {
-            using var options = new HttpRequestMessage(new HttpMethod("SUBSCRIBE"), url);
-            options.Headers.UserAgent.ParseAdd(USERAGENT);
-            options.Headers.TryAddWithoutValidation("HOST", ip + ":" + port.ToString(CultureInfo.InvariantCulture));
-            options.Headers.TryAddWithoutValidation("CALLBACK", "<" + localIp + ":" + eventport.ToString(CultureInfo.InvariantCulture) + ">");
-            options.Headers.TryAddWithoutValidation("NT", "upnp:event");
-            options.Headers.TryAddWithoutValidation("TIMEOUT", "Second-" + timeOut.ToString(CultureInfo.InvariantCulture));
-
-            using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
-                .SendAsync(options, HttpCompletionOption.ResponseHeadersRead)
-                .ConfigureAwait(false);
-            response.EnsureSuccessStatusCode();
-        }
-
-        public async Task<XDocument> GetDataAsync(string url, CancellationToken cancellationToken)
-        {
-            using var options = new HttpRequestMessage(HttpMethod.Get, url);
-            options.Headers.UserAgent.ParseAdd(USERAGENT);
-            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
-            using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-            response.EnsureSuccessStatusCode();
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            try
-            {
-                return await XDocument.LoadAsync(
-                    stream,
-                    LoadOptions.None,
-                    cancellationToken).ConfigureAwait(false);
-            }
-            catch
-            {
-                return null;
-            }
-        }
-
-        private async Task<HttpResponseMessage> PostSoapDataAsync(
-            string url,
-            string soapAction,
-            string postData,
-            string header,
-            CancellationToken cancellationToken)
-        {
-            if (soapAction[0] != '\"')
-            {
-                soapAction = $"\"{soapAction}\"";
-            }
-
-            using var options = new HttpRequestMessage(HttpMethod.Post, url);
-            options.Headers.UserAgent.ParseAdd(USERAGENT);
-            options.Headers.TryAddWithoutValidation("SOAPACTION", soapAction);
-            options.Headers.TryAddWithoutValidation("Pragma", "no-cache");
-            options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
-
-            if (!string.IsNullOrEmpty(header))
-            {
-                options.Headers.TryAddWithoutValidation("contentFeatures.dlna.org", header);
-            }
-
-            options.Content = new StringContent(postData, Encoding.UTF8, MediaTypeNames.Text.Xml);
-
-            return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-        }
-    }
-}

+ 1 - 1
Emby.Naming/AudioBook/AudioBookFilePathParserResult.cs

@@ -3,7 +3,7 @@ namespace Emby.Naming.AudioBook
     /// <summary>
     /// Data object for passing result of audiobook part/chapter extraction.
     /// </summary>
-    public struct AudioBookFilePathParserResult
+    public record struct AudioBookFilePathParserResult
     {
         /// <summary>
         /// Gets or sets optional number of path extracted from audiobook filename.

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

@@ -15,7 +15,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="TagLibSharp" Version="2.2.0" />
+    <PackageReference Include="TagLibSharp" Version="2.3.0" />
   </ItemGroup>
 
   <PropertyGroup>

+ 2 - 10
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -365,11 +365,7 @@ namespace Emby.Server.Implementations.AppBase
                 validatingStore.Validate(currentConfiguration, configuration);
             }
 
-            NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs
-            {
-                Key = key,
-                NewConfiguration = configuration
-            });
+            NamedConfigurationUpdating?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
 
             _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
 
@@ -391,11 +387,7 @@ namespace Emby.Server.Implementations.AppBase
         /// <param name="configuration">The old configuration.</param>
         protected virtual void OnNamedConfigurationUpdated(string key, object configuration)
         {
-            NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs
-            {
-                Key = key,
-                NewConfiguration = configuration
-            });
+            NamedConfigurationUpdated?.Invoke(this, new ConfigurationUpdateEventArgs(key, configuration));
         }
 
         /// <inheritdoc />

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

@@ -83,6 +83,7 @@ using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
+using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
@@ -111,7 +112,7 @@ namespace Emby.Server.Implementations
     /// <summary>
     /// Class CompositionRoot.
     /// </summary>
-    public abstract class ApplicationHost : IServerApplicationHost, IDisposable
+    public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
     {
         /// <summary>
         /// The environment variable prefixes to log at server startup.
@@ -634,7 +635,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IAuthService, AuthService>();
             serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 
-            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+            serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
+            serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
 
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
@@ -1233,5 +1235,49 @@ namespace Emby.Server.Implementations
 
             _disposed = true;
         }
+
+        public async ValueTask DisposeAsync()
+        {
+            await DisposeAsyncCore().ConfigureAwait(false);
+            Dispose(false);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
+        /// </summary>
+        /// <returns>A ValueTask.</returns>
+        protected virtual async ValueTask DisposeAsyncCore()
+        {
+            var type = GetType();
+
+            Logger.LogInformation("Disposing {Type}", type.Name);
+
+            foreach (var (part, _) in _disposableParts)
+            {
+                var partType = part.GetType();
+                if (partType == type)
+                {
+                    continue;
+                }
+
+                Logger.LogInformation("Disposing {Type}", partType.Name);
+
+                try
+                {
+                    part.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    Logger.LogError(ex, "Error disposing {Type}", partType.Name);
+                }
+            }
+
+            // used for closing websockets
+            foreach (var session in _sessionManager.Sessions)
+            {
+                await session.DisposeAsync().ConfigureAwait(false);
+            }
+        }
     }
 }

+ 1 - 5
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -4934,6 +4934,7 @@ SELECT key FROM UserDatas WHERE isFavorite=@IsFavorite AND userId=@UserId)
 AND Type = @InternalPersonType)");
                 statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
                 statement?.TryBind("@InternalPersonType", typeof(Person).FullName);
+                statement?.TryBind("@UserId", query.User.InternalId);
             }
 
             if (!query.ItemId.Equals(default))
@@ -4988,11 +4989,6 @@ AND Type = @InternalPersonType)");
                 statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
             }
 
-            if (query.User != null)
-            {
-                statement?.TryBind("@UserId", query.User.InternalId);
-            }
-
             return whereClauses;
         }
 

+ 6 - 2
Emby.Server.Implementations/Dto/DtoService.cs

@@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.Dto
 
             if (options.ContainsField(ItemFields.People))
             {
-                AttachPeople(dto, item);
+                AttachPeople(dto, item, user);
             }
 
             if (options.ContainsField(ItemFields.PrimaryImageAspectRatio))
@@ -503,7 +503,8 @@ namespace Emby.Server.Implementations.Dto
         /// </summary>
         /// <param name="dto">The dto.</param>
         /// <param name="item">The item.</param>
-        private void AttachPeople(BaseItemDto dto, BaseItem item)
+        /// <param name="user">The requesting user.</param>
+        private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
         {
             // Ordering by person type to ensure actors and artists are at the front.
             // This is taking advantage of the fact that they both begin with A
@@ -560,6 +561,9 @@ namespace Emby.Server.Implementations.Dto
                         return null;
                     }
                 }).Where(i => i != null)
+                .Where(i => user == null ?
+                    true :
+                    i.IsVisible(user))
                 .GroupBy(i => i.Name, StringComparer.OrdinalIgnoreCase)
                 .Select(x => x.First())
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);

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

@@ -29,10 +29,10 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
     <PackageReference Include="Mono.Nat" Version="3.0.3" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
-    <PackageReference Include="sharpcompress" Version="0.32.1" />
+    <PackageReference Include="sharpcompress" Version="0.32.2" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.3" />
   </ItemGroup>

+ 0 - 1
Emby.Server.Implementations/HttpServer/Security/SessionContext.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CS1591
 
-using System;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using MediaBrowser.Common.Extensions;

+ 32 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -11,7 +11,6 @@ using Jellyfin.Extensions.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.HttpServer
@@ -19,7 +18,7 @@ namespace Emby.Server.Implementations.HttpServer
     /// <summary>
     /// Class WebSocketConnection.
     /// </summary>
-    public class WebSocketConnection : IWebSocketConnection, IDisposable
+    public class WebSocketConnection : IWebSocketConnection
     {
         /// <summary>
         /// The logger.
@@ -36,6 +35,8 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         private readonly WebSocket _socket;
 
+        private bool _disposed = false;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
         /// </summary>
@@ -244,10 +245,39 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
         protected virtual void Dispose(bool dispose)
         {
+            if (_disposed)
+            {
+                return;
+            }
+
             if (dispose)
             {
                 _socket.Dispose();
             }
+
+            _disposed = true;
+        }
+
+        /// <inheritdoc />
+        public async ValueTask DisposeAsync()
+        {
+            await DisposeAsyncCore().ConfigureAwait(false);
+            Dispose(false);
+            GC.SuppressFinalize(this);
+        }
+
+        /// <summary>
+        /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
+        /// </summary>
+        /// <returns>A ValueTask.</returns>
+        protected virtual async ValueTask DisposeAsyncCore()
+        {
+            if (_socket.State == WebSocketState.Open)
+            {
+                await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "System Shutdown", CancellationToken.None).ConfigureAwait(false);
+            }
+
+            _socket.Dispose();
         }
     }
 }

+ 15 - 4
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -46,7 +46,6 @@ using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -2453,6 +2452,12 @@ namespace Emby.Server.Implementations.Library
             return RootFolder;
         }
 
+        /// <inheritdoc />
+        public void QueueLibraryScan()
+        {
+            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
+        }
+
         /// <inheritdoc />
         public int? GetSeasonNumberFromPath(string path)
             => SeasonPathParser.Parse(path, true, true).SeasonNumber;
@@ -2523,7 +2528,7 @@ namespace Emby.Server.Implementations.Library
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error reading the episode informations with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
+                _logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", episodeInfo.Path);
             }
 
             var changed = false;
@@ -2760,7 +2765,8 @@ namespace Emby.Server.Implementations.Library
 
         public List<Person> GetPeopleItems(InternalPeopleQuery query)
         {
-            return _itemRepository.GetPeopleNames(query).Select(i =>
+            return _itemRepository.GetPeopleNames(query)
+            .Select(i =>
             {
                 try
                 {
@@ -2771,7 +2777,12 @@ namespace Emby.Server.Implementations.Library
                     _logger.LogError(ex, "Error getting person");
                     return null;
                 }
-            }).Where(i => i != null).ToList();
+            })
+            .Where(i => i != null)
+            .Where(i => query.User == null ? 
+                true :
+                i.IsVisible(query.User))
+            .ToList();
         }
 
         public List<string> GetPeopleNames(InternalPeopleQuery query)

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

@@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
                 if (!string.IsNullOrEmpty(item.Path))
                 {
-                    // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
+                    // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name)
                     var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
 
                     if (!string.IsNullOrWhiteSpace(imdbid))
@@ -464,7 +464,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
                 new MultiItemResolverResult();
 
-            if (result.Items.Count == 1)
+            var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
+                                         || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
+            if (!isPhotosCollection && result.Items.Count == 1)
             {
                 var videoPath = result.Items[0].Path;
                 var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));

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

@@ -995,7 +995,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 }
             }
 
-            throw new Exception("Tuner not found.");
+            throw new ResourceNotFoundException("Tuner not found.");
         }
 
         public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken)

+ 2 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -13,6 +13,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
+using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
@@ -297,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 else
                 {
                     _taskCompletionSource.TrySetException(
-                        new Exception(
+                        new FfmpegException(
                             string.Format(
                                 CultureInfo.InvariantCulture,
                                 "Recording for {0} failed. Exit code {1}",

+ 8 - 10
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -20,6 +20,7 @@ using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -591,13 +592,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
             catch (HttpRequestException ex)
             {
-                if (ex.StatusCode.HasValue)
+                if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
                 {
-                    if ((int)ex.StatusCode.Value == 400)
-                    {
-                        _tokens.Clear();
-                        _lastErrorResponse = DateTime.UtcNow;
-                    }
+                    _tokens.Clear();
+                    _lastErrorResponse = DateTime.UtcNow;
                 }
 
                 throw;
@@ -662,7 +660,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 return root.Token;
             }
 
-            throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message);
+            throw new AuthenticationException("Could not authenticate with Schedules Direct Error: " + root.Message);
         }
 
         private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
@@ -697,7 +695,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (string.IsNullOrEmpty(token))
             {
-                throw new Exception("token required");
+                throw new ArgumentException("token required");
             }
 
             _logger.LogInformation("Headends on account ");
@@ -768,14 +766,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             var listingsId = info.ListingsId;
             if (string.IsNullOrEmpty(listingsId))
             {
-                throw new Exception("ListingsId required");
+                throw new ArgumentException("ListingsId required");
             }
 
             var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
 
             if (string.IsNullOrEmpty(token))
             {
-                throw new Exception("token required");
+                throw new ArgumentException("token required");
             }
 
             using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId);

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -196,7 +196,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 IsInfiniteStream = true,
                 IsRemote = isRemote,
 
-                IgnoreDts = true,
+                IgnoreDts = info.IgnoreDts,
                 SupportsDirectPlay = supportsDirectPlay,
                 SupportsDirectStream = supportsDirectStream,
 

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -199,7 +199,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 if (string.IsNullOrWhiteSpace(numberString))
                 {
                     // Using this as a fallback now as this leads to Problems with channels like "5 USA"
-                    // where 5 isn't ment to be the channel number
+                    // where 5 isn't meant to be the channel number
                     // Check for channel number with the format from SatIp
                     // #EXTINF:0,84. VOX Schweiz
                     // #EXTINF:0,84.0 - VOX Schweiz

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

@@ -92,22 +92,22 @@
     "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط",
     "ValueSpecialEpisodeName": "حلقه خاصه - {0}",
     "VersionNumber": "النسخة {0}",
-    "TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.",
-    "TaskCleanCache": "احذف مجلد ذاكرة التخزين المؤقت",
+    "TaskCleanCacheDescription": "يحذف الملفات المؤقتة التي لم يعد النظام بحاجة إليها.",
+    "TaskCleanCache": "احذف ما بمجلد الملفات المؤقتة",
     "TasksChannelsCategory": "قنوات الإنترنت",
     "TasksLibraryCategory": "مكتبة",
     "TasksMaintenanceCategory": "صيانة",
-    "TaskRefreshLibraryDescription": "يقوم بفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة وتحديث البيانات الوصفية.",
+    "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
     "TaskRefreshLibrary": "افحص مكتبة الوسائط",
-    "TaskRefreshChapterImagesDescription": "يقوم بانشاء صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
+    "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
     "TaskRefreshChapterImages": "استخراج صور الفصل",
     "TasksApplicationCategory": "تطبيق",
-    "TaskDownloadMissingSubtitlesDescription": "يقوم بالبحث في الإنترنت على الترجمات المفقودة إستنادا على البيانات الوصفية.",
-    "TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة",
-    "TaskRefreshChannelsDescription": "يقوم بتحديث معلومات قنوات الإنترنت.",
+    "TaskDownloadMissingSubtitlesDescription": "يبحث في الإنترنت على الترجمات الناقصة استنادا على البيانات الوصفية.",
+    "TaskDownloadMissingSubtitles": "تحميل الترجمات الناقصة",
+    "TaskRefreshChannelsDescription": "يحدث معلومات قنوات الإنترنت.",
     "TaskRefreshChannels": "إعادة تحديث القنوات",
-    "TaskCleanTranscodeDescription": "يقوم بحذف ملفات الترميز الأقدم من يوم واحد.",
-    "TaskCleanTranscode": "حذف سجلات الترميز",
+    "TaskCleanTranscodeDescription": "يحذف ملفات الترميز الأقدم من يوم واحد.",
+    "TaskCleanTranscode": "حذف ما بمجلد الترميز",
     "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.",
     "TaskUpdatePlugins": "تحديث الإضافات",
     "TaskRefreshPeopleDescription": "يقوم بتحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
@@ -116,12 +116,12 @@
     "TaskCleanLogs": "حذف مسار السجل",
     "TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الذي تم تحديده.",
     "TaskCleanActivityLog": "حذف سجل الأنشطة",
-    "Default": "إفتراضي",
+    "Default": "افتراضي",
     "Undefined": "غير معرف",
     "Forced": "ملحقة",
     "TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تتضمن تعديلات في قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
     "TaskOptimizeDatabase": "تحسين قاعدة البيانات",
-    "TaskKeyframeExtractorDescription": "يقوم باستخراج الإطارات الرئيسيه من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. هذه المهمه قد تستمر لاوقات طويلة.",
+    "TaskKeyframeExtractorDescription": "يستخرج الإطارات الرئيسية من ملفات الفيديو لكي ينشئ قوائم تشغيل بث HTTP المباشر. قد تستمر هذه العملية لوقت طويل.",
     "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
     "External": "خارجي"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/et.json

@@ -119,5 +119,6 @@
     "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
     "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
     "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
-    "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
+    "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
+    "External": "Väline"
 }

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

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}",
+    "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
@@ -42,13 +42,13 @@
     "MusicVideos": "Clips musicaux",
     "NameInstallFailed": "{0} échec de l'installation",
     "NameSeasonNumber": "Saison {0}",
-    "NameSeasonUnknown": "Saison Inconnue",
+    "NameSeasonUnknown": "Saison inconnue",
     "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.",
     "NotificationOptionApplicationUpdateAvailable": "Mise à jour de l'application disponible",
     "NotificationOptionApplicationUpdateInstalled": "Mise à jour de l'application installée",
     "NotificationOptionAudioPlayback": "Lecture audio démarrée",
     "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée",
-    "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée",
+    "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été téléversée",
     "NotificationOptionInstallationFailed": "Échec de l'installation",
     "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté",
     "NotificationOptionPluginError": "Erreur d'extension",
@@ -93,33 +93,33 @@
     "ValueSpecialEpisodeName": "Spécial - {0}",
     "VersionNumber": "Version {0}",
     "TasksChannelsCategory": "Chaînes en ligne",
-    "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur internet en se basant sur la configuration des métadonnées.",
+    "TaskDownloadMissingSubtitlesDescription": "Recherche les sous-titres manquants sur Internet en se basant sur la configuration des métadonnées.",
     "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
-    "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaînes en ligne.",
-    "TaskRefreshChannels": "Rafraîchir les chaînes",
+    "TaskRefreshChannelsDescription": "Actualise les informations des chaînes en ligne.",
+    "TaskRefreshChannels": "Actualiser les chaînes",
     "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
-    "TaskCleanTranscode": "Nettoyer les dossier des transcodages",
+    "TaskCleanTranscode": "Nettoyer le dossier des transcodages",
     "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurées pour être mises à jour automatiquement.",
     "TaskUpdatePlugins": "Mettre à jour les extensions",
-    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
-    "TaskRefreshPeople": "Rafraîchir les acteurs",
+    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre médiathèque.",
+    "TaskRefreshPeople": "Actualiser les acteurs",
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
-    "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et rafraîchit les métadonnées.",
+    "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
     "TaskRefreshLibrary": "Scanner la médiathèque",
     "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
     "TaskCleanCache": "Vider le répertoire cache",
     "TasksApplicationCategory": "Application",
-    "TasksLibraryCategory": "Bibliothèque",
+    "TasksLibraryCategory": "Médiathèque",
     "TasksMaintenanceCategory": "Maintenance",
     "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
     "TaskCleanActivityLog": "Nettoyer le journal d'activité",
     "Undefined": "Non défini",
     "Forced": "Forcé",
     "Default": "Par défaut",
-    "TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
+    "TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
     "TaskOptimizeDatabase": "Optimiser la base de données",
     "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
     "TaskKeyframeExtractor": "Extracteur d'image clé",

+ 2 - 1
Emby.Server.Implementations/Localization/Core/hr.json

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "Optimiziraj bazu podataka",
     "External": "Vanjski",
     "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
-    "TaskKeyframeExtractor": "Izvoditelj ključnog okvira"
+    "TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
+    "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka."
 }

+ 4 - 1
Emby.Server.Implementations/Localization/Core/ko.json

@@ -120,5 +120,8 @@
     "Forced": "강제하기",
     "Default": "기본 설정",
     "TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.",
-    "TaskOptimizeDatabase": "데이터베이스 최적화"
+    "TaskOptimizeDatabase": "데이터베이스 최적화",
+    "TaskKeyframeExtractorDescription": "비디오 파일에서 키프레임을 추출하여 더 정확한 HLS 재생 목록을 만듭니다. 이 작업은 오랫동안 진행될 수 있습니다.",
+    "TaskKeyframeExtractor": "키프레임 추출",
+    "External": "외부"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -39,7 +39,7 @@
     "MixedContent": "Mixed content",
     "Movies": "Filmai",
     "Music": "Muzika",
-    "MusicVideos": "Muzikiniai klipai",
+    "MusicVideos": "Muzikiniai vaizdo įrašai",
     "NameInstallFailed": "{0} diegimo klaida",
     "NameSeasonNumber": "Sezonas {0}",
     "NameSeasonUnknown": "Sezonas neatpažintas",

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

@@ -84,7 +84,7 @@
     "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
     "Books": "Grāmatas",
     "Artists": "Izpildītāji",
-    "Albums": "Albumi",
+    "Albums": "Albūmi",
     "ProviderValue": "Provider: {0}",
     "HeaderFavoriteSongs": "Dziesmu Favorīti",
     "HeaderFavoriteShows": "Raidījumu Favorīti",
@@ -117,7 +117,8 @@
     "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
     "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
     "Undefined": "Nenoteikts",
-    "Default": "Noklusējums",
+    "Default": "Noklusējuma",
     "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
-    "TaskOptimizeDatabase": "Optimizēt datubāzi"
+    "TaskOptimizeDatabase": "Optimizēt datubāzi",
+    "External": "Ārējais"
 }

+ 25 - 3
Emby.Server.Implementations/Localization/Core/mk.json

@@ -64,9 +64,9 @@
     "CameraImageUploadedFrom": "Нова слика од камера беше поставена од {0}",
     "Books": "Книги",
     "AuthenticationSucceededWithUserName": "{0} успешно поврзан",
-    "Artists": "Изведувач",
+    "Artists": "Изведувачи",
     "Application": "Апликација",
-    "AppDeviceValues": "Аплиакција: {0}, Уред: {1}",
+    "AppDeviceValues": "Апликација: {0}, Уред: {1}",
     "Albums": "Албуми",
     "VersionNumber": "Верзија {0}",
     "ValueSpecialEpisodeName": "Специјално - {0}",
@@ -100,5 +100,27 @@
     "TasksMaintenanceCategory": "Одржување",
     "Undefined": "Недефинирано",
     "Forced": "Принудно",
-    "Default": "Зададено"
+    "Default": "Зададено",
+    "TaskKeyframeExtractorDescription": "Извлекува клучни рамки од видео фајлови за да се направат попрецизни HLS плејлисти. Оваа задача може да работи многу долго време.",
+    "TaskKeyframeExtractor": "Извлекувач на клучни рамки",
+    "TaskOptimizeDatabaseDescription": "Компактира датабазата и смалува празното место. Извршувањето на оваа задача по скенирање на библиотеката или правење други промени што прават модификации на датабазата може да подобри перформансите.",
+    "TaskOptimizeDatabase": "Оптимизирај датабаза",
+    "TaskDownloadMissingSubtitlesDescription": "Пребарува интернет за преводи што недостиваат според метадата конфигурација.",
+    "TaskDownloadMissingSubtitles": "Симни преводи што недостигаат",
+    "TaskRefreshChannelsDescription": "Ажурирај информации за интернет канали.",
+    "TaskRefreshChannels": "Ажурирај Канали",
+    "TaskCleanTranscodeDescription": "Избриши транскодирани фајлови постари од еден ден.",
+    "TaskCleanTranscode": "Исчисти Директориум за Транскодирање",
+    "TaskUpdatePluginsDescription": "Симни и инсталирај ажурирања за плагини што се конфигурирани за автоматско ажурирање.",
+    "TaskUpdatePlugins": "Ажурирај Плагини",
+    "TaskRefreshPeopleDescription": "Ажурирај метадата за акери и директори во вашата медиска библиотека.",
+    "TaskRefreshPeople": "Ажурирајте ги Луѓето",
+    "TaskCleanLogsDescription": "Избриши лог фајлови постари од {0} денови.",
+    "TaskCleanLogs": "Избриши Директориум на Логови",
+    "TaskRefreshLibraryDescription": "Скенирајте ја вашата медиска библиотека за нови фајлови и ажурирај метадата.",
+    "TaskRefreshLibrary": "Скенирај Медиумска Библиотека",
+    "TaskRefreshChapterImagesDescription": "Создава тамбнеил за видеата шти имаат поглавја.",
+    "TaskCleanActivityLogDescription": "Избришува логови на активности постари од определеното време.",
+    "TaskCleanActivityLog": "Избриши Лог на Активности",
+    "External": "Надворешен"
 }

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

@@ -61,7 +61,7 @@
     "NotificationOptionVideoPlayback": "Ulangmain video bermula",
     "NotificationOptionVideoPlaybackStopped": "Ulangmain video dihentikan",
     "Photos": "Gambar-gambar",
-    "Playlists": "Senarai main",
+    "Playlists": "Senarai ulangmain",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} telah dipasang",
     "PluginUninstalledWithName": "{0} telah dinyahpasang",

+ 49 - 49
Emby.Server.Implementations/Localization/Core/my.json

@@ -6,97 +6,97 @@
     "Artists": "အနုပညာရှင်များ",
     "Albums": "သီချင်းအခွေများ",
     "TaskOptimizeDatabaseDescription": "ဒေတာဘေ့စ်ကို ကျစ်လစ်စေပြီး နေရာလွတ်များကို ဖြတ်တောက်ပေးသည်။ စာကြည့်တိုက်ကို စကင်န်ဖတ်ပြီးနောက် ဤလုပ်ငန်းကို လုပ်ဆောင်ခြင်း သို့မဟုတ် ဒေတာဘေ့စ်မွမ်းမံမှုများ စွမ်းဆောင်ရည်ကို မြှင့်တင်ပေးနိုင်သည်ဟု ရည်ညွှန်းသော အခြားပြောင်းလဲမှုများကို လုပ်ဆောင်ခြင်း။.",
-    "TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ",
+    "TaskOptimizeDatabase": "ဒေတာဘေ့စ်ကို အကောင်းဆုံးဖြစ်အောင်လုပ်ပါ",
     "TaskDownloadMissingSubtitlesDescription": "မက်တာဒေတာ ဖွဲ့စည်းမှုပုံစံအပေါ် အခြေခံ၍ ပျောက်ဆုံးနေသော စာတန်းထိုးများအတွက် အင်တာနက်ကို ရှာဖွေသည်။",
-    "TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ",
+    "TaskDownloadMissingSubtitles": "ပျောက်ဆုံးနေသော စာတန်းထိုးများကို ဒေါင်းလုဒ်လုပ်ပါ",
     "TaskRefreshChannelsDescription": "အင်တာနက်ချန်နယ်အချက်အလက်ကို ပြန်လည်စတင်သည်။",
-    "TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ",
+    "TaskRefreshChannels": "ချန်နယ်များကို ပြန်လည်စတင်ပါ",
     "TaskCleanTranscodeDescription": "သက်တမ်း တစ်ရက်ထက်ပိုသော အသွင်ပြောင်းကုဒ်ဖိုင်များကို ဖျက်ပါ။",
-    "TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ",
+    "TaskCleanTranscode": "Transcode လမ်းညွှန်ကို သန့်ရှင်းပါ",
     "TaskUpdatePluginsDescription": "အလိုအလျောက် အပ်ဒိတ်လုပ်ရန် စီစဉ်ထားသော ပလပ်အင်များအတွက် အပ်ဒိတ်များကို ဒေါင်းလုဒ်လုပ်ပြီး ထည့်သွင်းပါ။",
-    "TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ",
+    "TaskUpdatePlugins": "ပလပ်အင်များကို အပ်ဒိတ်လုပ်ပါ",
     "TaskRefreshPeopleDescription": "သင့်မီဒီယာစာကြည့်တိုက်ရှိ သရုပ်ဆောင်များနှင့် ဒါရိုက်တာများအတွက် မက်တာဒေတာကို အပ်ဒိတ်လုပ်ပါ။",
-    "TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ",
+    "TaskRefreshPeople": "လူများကို ပြန်လည်ဆန်းသစ်ပါ",
     "TaskCleanLogsDescription": "{0} ရက်ထက်ပိုသော မှတ်တမ်းဖိုင်များကို ဖျက်သည်။",
-    "TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ",
+    "TaskCleanLogs": "မှတ်တမ်းလမ်းညွှန်ကို သန့်ရှင်းပါ",
     "TaskRefreshLibraryDescription": "သင့်မီဒီယာဒစ်ဂျစ်တိုက်ကို ဖိုင်အသစ်များရှိမရှိ စကင်န်ဖတ်ပြီး ဖိုင်ရဲ့အကြောင်းအရာများ ကို ပြန်ပြုပြင်မွမ်းမံပါ။",
-    "TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ",
+    "TaskRefreshLibrary": "မီဒီယာစာကြည့်တိုက်ကို စကင်န်ဖတ်ပါ",
     "TaskRefreshChapterImagesDescription": "အခန်းများပါရှိသော ဗီဒီယိုများအတွက် ပုံသေးများကို ဖန်တီးပါ။",
-    "TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ",
+    "TaskRefreshChapterImages": "အခန်းတစ်ခုစီ ပုံများကို ထုတ်ယူပါ",
     "TaskCleanCacheDescription": "စနစ်မှ မလိုအပ်တော့သော ကက်ရှ်ဖိုင်များကို ဖျက်ပါ။.",
-    "TaskCleanCache": "Cache Directory ကို ရှင်းပါ",
+    "TaskCleanCache": "Cache Directory ကို ရှင်းပါ",
     "TaskCleanActivityLogDescription": "စီစဉ်သတ်မှတ်ထားသော အသက်ထက် ပိုကြီးသော လုပ်ဆောင်ချက်မှတ်တမ်းများကို ဖျက်ပါ။",
-    "TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ",
+    "TaskCleanActivityLog": "လုပ်ဆောင်ချက်မှတ်တမ်းကို ရှင်းလင်းပါ",
     "TasksChannelsCategory": "အင်တာနက် ချန်နယ်လိုင်းများ",
     "TasksApplicationCategory": "အပလီကေးရှင်း",
     "TasksLibraryCategory": "မီဒီယာတိုက်",
     "TasksMaintenanceCategory": "ပြုပြင် ထိန်းသိမ်းခြင်း",
     "VersionNumber": "ဗားရှင်း {0}",
     "ValueSpecialEpisodeName": "အထူး- {0}",
-    "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ",
+    "ValueHasBeenAddedToLibrary": "{0} ကို သင့်မီဒီယာဒစ်ဂျစ်တိုက်သို့ ပေါင်းထည့်လိုက်ပါပြီ",
     "UserStoppedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ဖွင့်ပြီးပါပြီ",
     "UserStartedPlayingItemWithValues": "{0} သည် {1} ကို {2} တွင် ပြသနေသည်",
     "UserPolicyUpdatedWithName": "{0} အတွက် အသုံးပြုသူမူဝါဒကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
     "UserPasswordChangedWithName": "အသုံးပြုသူ {0} အတွက် စကားဝှက်ကို ပြောင်းထားသည်",
     "UserOnlineFromDevice": "{0} သည် {1} မှ အွန်လိုင်းဖြစ်သည်",
     "UserOfflineFromDevice": "{0} သည် {1} မှ ချိတ်ဆက်မှုပြတ်တောက်သွားသည်",
-    "UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်",
+    "UserLockedOutWithName": "အသုံးပြုသူ {0} အား လော့ခ်ချထားသည်",
     "UserDownloadingItemWithValues": "{0} သည် {1} ကို ဒေါင်းလုဒ်လုပ်နေသည်",
-    "UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ",
-    "UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ",
+    "UserDeletedWithName": "အသုံးပြုသူ {0} ကို ဖျက်လိုက်ပါပြီ",
+    "UserCreatedWithName": "အသုံးပြုသူ {0} ကို ဖန်တီးပြီးပါပြီ",
     "User": "အသုံးပြုသူ",
     "Undefined": "သတ်မှတ်မထားသော",
     "TvShows": "တီဗီ ဇာတ်လမ်းတွဲများ",
     "System": "စနစ်",
     "Sync": "ထပ်တူကျသည်။",
-    "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
+    "SubtitleDownloadFailureFromForItem": "{1} အတွက် {0} မှ စာတန်းထိုးများ ဒေါင်းလုဒ်လုပ်ခြင်း မအောင်မြင်ပါ",
     "StartupEmbyServerIsLoading": "Jellyfin ဆာဗာကို အသင့်ပြင်နေပါသည်။ ခဏနေ ထပ်စမ်းကြည့်ပါ။",
     "Songs": "သီချင်းများ",
     "Shows": "ဇာတ်လမ်းတွဲများ",
-    "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်",
-    "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်",
-    "ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ",
+    "ServerNameNeedsToBeRestarted": "{0} ကို ပြန်လည်စတင်ရန် လိုအပ်သည်",
+    "ScheduledTaskStartedWithName": "{0} စတင်ခဲ့သည်",
+    "ScheduledTaskFailedWithName": "{0} မအောင်မြင်ပါ",
     "ProviderValue": "ဝန်ဆောင်မှုပေးသူ- {0}",
-    "PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်",
-    "PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ",
-    "PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်",
+    "PluginUpdatedWithName": "ပလပ်ခ်အင် {0} ကို အပ်ဒိတ်လုပ်ထားသည်",
+    "PluginUninstalledWithName": "ပလပ်ခ်အင် {0} ကို ဖြုတ်လိုက်ပါပြီ",
+    "PluginInstalledWithName": "ပလပ်ခ်အင် {0} ကို ထည့်သွင်းခဲ့သည်",
     "Plugin": "ပလပ်အင်",
     "Playlists": "အစီအစဉ်များ",
     "Photos": "ဓာတ်ပုံများ",
-    "NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်",
-    "NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ",
-    "NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်",
+    "NotificationOptionVideoPlaybackStopped": "ဗီဒီယိုဖွင့်ခြင်း ရပ်သွားသည်",
+    "NotificationOptionVideoPlayback": "ဗီဒီယိုဖွင့်ခြင်း စတင်ပါပြီ",
+    "NotificationOptionUserLockedOut": "အသုံးပြုသူ ဝင်ရန် တားမြစ်ခံရသည်",
     "NotificationOptionTaskFailed": "စီစဉ်ထားသော အလုပ်ပျက်ကွက်",
-    "NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်",
-    "NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ",
-    "NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ",
-    "NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်",
-    "NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း",
-    "NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်",
-    "NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ",
-    "NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ",
-    "NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်",
-    "NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ",
-    "NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်",
-    "NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ",
+    "NotificationOptionServerRestartRequired": "ဆာဗာ ပြန်လည်စတင်ရန် လိုအပ်သည်",
+    "NotificationOptionPluginUpdateInstalled": "ပလပ်အင် အပ်ဒိတ် ထည့်သွင်းပြီးပါပြီ",
+    "NotificationOptionPluginUninstalled": "ပလပ်အင်ကို ဖြုတ်လိုက်ပါပြီ",
+    "NotificationOptionPluginInstalled": "ပလပ်အင် ထည့်သွင်းထားသည်",
+    "NotificationOptionPluginError": "ပလပ်အင် ချို့ယွင်းခြင်း",
+    "NotificationOptionNewLibraryContent": "အသစ်များ ထပ်ထည့်ထားပါတယ်",
+    "NotificationOptionInstallationFailed": "ထည့်သွင်းမှု မအောင်မြင်ပါ",
+    "NotificationOptionCameraImageUploaded": "ကင်မရာမှ ဓာတ်ပုံ အပ်လုဒ် ပြီးပါပြီ",
+    "NotificationOptionAudioPlaybackStopped": "အသံဖိုင်ဖွင့်ခြင်း ရပ်သွားသည်",
+    "NotificationOptionAudioPlayback": "အသံဖွင့်ခြင်း စတင်ပါပြီ",
+    "NotificationOptionApplicationUpdateInstalled": "အပလီကေးရှင်း အပ်ဒိတ်ကို ထည့်သွင်းထားသည်",
+    "NotificationOptionApplicationUpdateAvailable": "အပလီကေးရှင်း အပ်ဒိတ် ရနိုင်ပါပြီ",
     "NewVersionIsAvailable": "Jellyfin Server ၏ ဗားရှင်းအသစ်ကို ဒေါင်းလုဒ်လုပ်နိုင်ပါပြီ။",
     "NameSeasonUnknown": "ဇာတ်လမ်းတွဲ အပိုင်းမသိ",
     "NameSeasonNumber": "ဇာတ်လမ်းတွဲ အပိုင်း {0}",
-    "NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ",
+    "NameInstallFailed": "{0} ထည့်သွင်းမှု မအောင်မြင်ပါ",
     "MusicVideos": "ဂီတဗီဒီယိုများ",
     "Music": "တေးဂီတ",
     "Movies": "ရုပ်ရှင်များ",
     "MixedContent": "ရောနှောပါဝင်မှု",
-    "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
-    "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
+    "MessageServerConfigurationUpdated": "ဆာဗာဖွဲ့စည်းပုံကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
+    "MessageNamedServerConfigurationUpdatedWithValue": "ဆာဗာဖွဲ့စည်းပုံကဏ္ဍ {0} ကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
     "MessageApplicationUpdatedTo": "Jellyfin ဆာဗာကို {0} သို့ အပ်ဒိတ်လုပ်ထားသည်",
-    "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
+    "MessageApplicationUpdated": "Jellyfin ဆာဗာကို အပ်ဒိတ်လုပ်ပြီးပါပြီ",
     "Latest": "နောက်ဆုံး",
     "LabelRunningTimeValue": "ကြာချိန် - {0}",
     "LabelIpAddressValue": "IP လိပ်စာ- {0}",
-    "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်",
-    "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်",
-    "Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်",
+    "ItemRemovedWithName": "{0} ကို ဒစ်ဂျစ်တိုက်မှ ဖယ်ရှားခဲ့သည်",
+    "ItemAddedWithName": "{0} ကို စာကြည့်တိုက်သို့ ထည့်ထားသည်",
+    "Inherit": "ဆက်ခံ၍ လုပ်ဆောင်သည်",
     "HomeVideos": "ကိုယ်တိုင်ရိုက် ဗီဒီယိုများ",
     "HeaderRecordingGroups": "အသံဖမ်းအဖွဲ့များ",
     "HeaderNextUp": "နောက်ထပ်",
@@ -106,18 +106,18 @@
     "HeaderFavoriteEpisodes": "အကြိုက်ဆုံး ဇာတ်လမ်းအပိုင်းများ",
     "HeaderFavoriteArtists": "အကြိုက်ဆုံးအနုပညာရှင်များ",
     "HeaderFavoriteAlbums": "အကြိုက်ဆုံး အယ်လ်ဘမ်များ",
-    "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
+    "HeaderContinueWatching": "ဆက်လက်ကြည့်ရှုပါ",
     "HeaderAlbumArtists": "အယ်လ်ဘမ်အနုပညာရှင်များ",
     "Genres": "အမျိုးအစားများ",
     "Forced": "အတင်းအကြပ်",
     "Folders": "ဖိုလ်ဒါများ",
     "Favorites": "အကြိုက်ဆုံးများ",
     "FailedLoginAttemptWithUserName": "{0} မှ အကောင့်ဝင်ရန် မအောင်မြင်ပါ",
-    "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်",
-    "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ",
+    "DeviceOnlineWithName": "{0} ကို ချိတ်ဆက်ထားသည်",
+    "DeviceOfflineWithName": "{0} နှင့် အဆက်ပြတ်သွားပါပြီ",
     "ChapterNameValue": "အခန်း {0}",
-    "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်",
-    "AuthenticationSucceededWithUserName": "{0} စစ်မှန်ကြောင်း အောင်မြင်စွာ အတည်ပြုပြီးပါပြီ",
+    "CameraImageUploadedFrom": "ကင်မရာပုံအသစ်ကို {0} မှ ထည့်သွင်းလိုက်သည်",
+    "AuthenticationSucceededWithUserName": "{0} အောင်မြင်စွာ စစ်မှန်ကြောင်း အတည်ပြုပြီးပါပြီ",
     "Application": "အပလီကေးရှင်း",
     "AppDeviceValues": "အက်ပ်- {0}၊ စက်- {1}",
     "External": "ပြင်ပ"

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

@@ -8,15 +8,15 @@
     "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
     "Channels": "Canais",
     "ChapterNameValue": "Capítulo {0}",
-    "Collections": "Coleções",
+    "Collections": "Colecções",
     "DeviceOfflineWithName": "{0} desligou-se",
     "DeviceOnlineWithName": "{0} ligou-se",
     "FailedLoginAttemptWithUserName": "Tentativa de login falhada a partir de {0}",
     "Favorites": "Favoritos",
     "Folders": "Pastas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas do Álbum",
-    "HeaderContinueWatching": "Continuar a Ver",
+    "HeaderAlbumArtists": "Artistas do álbum",
+    "HeaderContinueWatching": "Continuar a ver",
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteArtists": "Artistas Favoritos",
     "HeaderFavoriteEpisodes": "Episódios Favoritos",

+ 9 - 9
Emby.Server.Implementations/Localization/Core/pt.json

@@ -1,5 +1,5 @@
 {
-    "HeaderLiveTV": "TV Ao Vivo",
+    "HeaderLiveTV": "TV Em Direto",
     "Collections": "Coleções",
     "Books": "Livros",
     "Artists": "Artistas",
@@ -10,9 +10,9 @@
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteEpisodes": "Episódios Favoritos",
     "HeaderFavoriteShows": "Séries Favoritas",
-    "HeaderContinueWatching": "Continuar assistindo",
+    "HeaderContinueWatching": "Continuar a ver",
     "HeaderAlbumArtists": "Artistas do Álbum",
-    "Genres": "Gêneros",
+    "Genres": "Géneros",
     "Folders": "Diretórios",
     "Favorites": "Favoritos",
     "Channels": "Canais",
@@ -74,7 +74,7 @@
     "ItemRemovedWithName": "{0} foi removido da biblioteca",
     "ItemAddedWithName": "{0} foi adicionado à biblioteca",
     "Inherit": "Herdar",
-    "HomeVideos": "Vídeos principais",
+    "HomeVideos": "Vídeos Caseiros",
     "HeaderRecordingGroups": "Grupos de Gravação",
     "ValueSpecialEpisodeName": "Episódio Especial - {0}",
     "Sync": "Sincronização",
@@ -83,14 +83,14 @@
     "Playlists": "Listas de Reprodução",
     "Photos": "Fotografias",
     "Movies": "Filmes",
-    "FailedLoginAttemptWithUserName": "Tentativa falha de login a partir de {0}",
-    "DeviceOnlineWithName": "{0} está conectado",
-    "DeviceOfflineWithName": "{0} desconectou-se",
+    "FailedLoginAttemptWithUserName": "Tentativa de início de sessão falhada a partir de {0}",
+    "DeviceOnlineWithName": "{0} está ligado",
+    "DeviceOfflineWithName": "{0} desligou-se",
     "ChapterNameValue": "Capítulo {0}",
     "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
     "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
-    "Application": "Aplicativo",
-    "AppDeviceValues": "Aplicativo {0}, Dispositivo: {1}",
+    "Application": "Aplicação",
+    "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
     "TaskCleanCache": "Limpar Diretório de Cache",
     "TasksApplicationCategory": "Aplicativo",
     "TasksLibraryCategory": "Biblioteca",

+ 3 - 2
Emby.Server.Implementations/Localization/Core/sl-SI.json

@@ -90,7 +90,7 @@
     "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
     "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
     "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
-    "ValueSpecialEpisodeName": "Bonus - {0}",
+    "ValueSpecialEpisodeName": "Posebna epizoda - {0}",
     "VersionNumber": "Različica {0}",
     "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
     "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@@ -122,5 +122,6 @@
     "TaskOptimizeDatabaseDescription": "Stisne bazo podatkov in uredi prazen prostor. Zagon tega opravila po iskanju predstavnosti ali drugih spremembah ki vplivajo na bazo podatkov lahko izboljša hitrost delovanja.",
     "TaskOptimizeDatabase": "Optimiziraj bazo podatkov",
     "TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
-    "External": "Zunanje"
+    "External": "Zunanji",
+    "TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa."
 }

+ 5 - 3
Emby.Server.Implementations/Localization/Core/sr.json

@@ -86,7 +86,7 @@
     "Channels": "Канали",
     "CameraImageUploadedFrom": "Нова фотографија је учитана са {0}",
     "Books": "Књиге",
-    "AuthenticationSucceededWithUserName": "{0} Успешна аутентикација",
+    "AuthenticationSucceededWithUserName": "{0} Успешна аутентификација",
     "Artists": "Извођачи",
     "Application": "Апликација",
     "AppDeviceValues": "Апликација: {0}, Уређај: {1}",
@@ -118,7 +118,9 @@
     "Undefined": "Недефинисано",
     "Forced": "Принудно",
     "Default": "Подразумевано",
-    "TaskOptimizeDatabase": "Оптимизуј датабазу",
+    "TaskOptimizeDatabase": "Оптимизуј банку података",
     "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе.",
-    "External": "Спољно"
+    "External": "Спољно",
+    "TaskKeyframeExtractorDescription": "Екстрактује кљулне сличице из видео датотека да би креирао више преицзну HLS плеј-листу. Овај задатак може да потраје дуже време.",
+    "TaskKeyframeExtractor": "Екстрактор кључних сличица"
 }

+ 11 - 2
Emby.Server.Implementations/Localization/Core/ug.json

@@ -3,7 +3,16 @@
     "Channels": "قانال",
     "CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى",
     "Books": "كىتاب",
-    "AuthenticationSucceededWithUserName": "تىزىملىتىش مۇۋەپپەقىيەتلىك بول",
+    "AuthenticationSucceededWithUserName": "{0} تەستىقلاش مۇۋاپىقىيەتلىك بولدى",
     "Artists": "سەنئەتكار",
-    "Albums": "پىلاستىنكا"
+    "Albums": "پىلاستىنكا",
+    "DeviceOnlineWithName": "{0} ئۇلاندى",
+    "DeviceOfflineWithName": "{0} ئۈزۈلدى",
+    "Collections": "توپلام",
+    "Application": "ئەپ",
+    "AppDeviceValues": "ئەپ: {0}، ئۈسكۈنە: {1}",
+    "HeaderLiveTV": "تور تېلېۋىزىيەسى",
+    "Default": "سۈكۈتتىكى",
+    "Folders": "ھۆججەت خالتىسى",
+    "Favorites": "ساقلىغۇچ"
 }

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

@@ -120,5 +120,8 @@
     "Default": "預設",
     "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
     "TaskOptimizeDatabase": "最佳化數據庫",
-    "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。"
+    "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
+    "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
+    "TaskKeyframeExtractor": "關鍵幀提取器",
+    "External": "外部"
 }

+ 10 - 0
Emby.Server.Implementations/Localization/Ratings/fi.csv

@@ -0,0 +1,10 @@
+FI-S,1
+FI-T,1
+FI-7,4
+FI-12,5
+FI-16,8
+FI-18,9
+FI-K7,4
+FI-K12,5
+FI-K16,8
+FI-K18,9

+ 6 - 0
Emby.Server.Implementations/Localization/Ratings/no.csv

@@ -0,0 +1,6 @@
+NO-A,1
+NO-6,3
+NO-9,4
+NO-12,5
+NO-15,8
+NO-18,9

+ 5 - 0
Emby.Server.Implementations/Localization/Ratings/se.csv

@@ -0,0 +1,5 @@
+SE-Btl,1
+SE-Barntillåten,1
+SE-7,3
+SE-11,5
+SE-15,8

+ 2 - 2
Emby.Server.Implementations/Net/UdpSocket.cs

@@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Net
                 }
                 else
                 {
-                    tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+                    tcs.TrySetException(new SocketException((int)e.SocketError));
                 }
             }
         }
@@ -114,7 +114,7 @@ namespace Emby.Server.Implementations.Net
                 }
                 else
                 {
-                    tcs.TrySetException(new Exception("SocketError: " + e.SocketError));
+                    tcs.TrySetException(new SocketException((int)e.SocketError));
                 }
             }
         }

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

@@ -1242,7 +1242,7 @@ namespace Emby.Server.Implementations.Session
 
             if (item == null)
             {
-                _logger.LogError("A non-existant item Id {0} was passed into TranslateItemForPlayback", id);
+                _logger.LogError("A non-existent item Id {0} was passed into TranslateItemForPlayback", id);
                 return Array.Empty<BaseItem>();
             }
 

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

@@ -6,7 +6,6 @@ using System.Linq;
 using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
@@ -37,7 +36,7 @@ namespace Emby.Server.Implementations.Session
         private const float ForceKeepAliveFactor = 0.75f;
 
         /// <summary>
-        /// Lock used for accesing the KeepAlive cancellation token.
+        /// Lock used for accessing the KeepAlive cancellation token.
         /// </summary>
         private readonly object _keepAliveLock = new object();
 

+ 18 - 1
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Session
 {
-    public sealed class WebSocketController : ISessionController, IDisposable
+    public sealed class WebSocketController : ISessionController, IAsyncDisposable, IDisposable
     {
         private readonly ILogger<WebSocketController> _logger;
         private readonly ISessionManager _sessionManager;
@@ -99,6 +99,23 @@ namespace Emby.Server.Implementations.Session
             foreach (var socket in _sockets)
             {
                 socket.Closed -= OnConnectionClosed;
+                socket.Dispose();
+            }
+
+            _disposed = true;
+        }
+
+        public async ValueTask DisposeAsync()
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            foreach (var socket in _sockets)
+            {
+                socket.Closed -= OnConnectionClosed;
+                await socket.DisposeAsync().ConfigureAwait(false);
             }
 
             _disposed = true;

+ 0 - 1
Emby.Server.Implementations/Sorting/StudioComparer.cs

@@ -3,7 +3,6 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Linq;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;

+ 21 - 32
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -43,9 +43,9 @@ namespace Emby.Server.Implementations.TV
             }
 
             string presentationUniqueKey = null;
-            if (!string.IsNullOrEmpty(query.SeriesId))
+            if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
             {
-                if (_libraryManager.GetItemById(query.SeriesId) is Series series)
+                if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
                 {
                     presentationUniqueKey = GetUniqueSeriesKey(series);
                 }
@@ -93,9 +93,9 @@ namespace Emby.Server.Implementations.TV
 
             string presentationUniqueKey = null;
             int? limit = null;
-            if (!string.IsNullOrEmpty(request.SeriesId))
+            if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
             {
-                if (_libraryManager.GetItemById(request.SeriesId) is Series series)
+                if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
                 {
                     presentationUniqueKey = GetUniqueSeriesKey(series);
                     limit = 1;
@@ -135,25 +135,20 @@ namespace Emby.Server.Implementations.TV
             return GetResult(episodes, request);
         }
 
-        public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
+        private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
         {
-            // Avoid implicitly captured closure
-            var currentUser = user;
-
-            var allNextUp = seriesKeys
-                .Select(i => GetNextUp(i, currentUser, dtoOptions, false));
+            var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
 
             if (request.EnableRewatching)
             {
                 allNextUp = allNextUp.Concat(
-                    seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true))
-                )
-                .OrderByDescending(i => i.Item1);
+                    seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
+                .OrderByDescending(i => i.LastWatchedDate);
             }
 
             // If viewing all next up for all series, remove first episodes
             // But if that returns empty, keep those first episodes (avoid completely empty view)
-            var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
+            var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default);
             var anyFound = false;
 
             return allNextUp
@@ -161,23 +156,18 @@ namespace Emby.Server.Implementations.TV
                 {
                     if (request.DisableFirstEpisode)
                     {
-                        return i.Item1 != DateTime.MinValue;
+                        return i.LastWatchedDate != DateTime.MinValue;
                     }
 
-                    if (alwaysEnableFirstEpisode || (i.Item1 != DateTime.MinValue && i.Item1.Date >= request.NextUpDateCutoff))
+                    if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff))
                     {
                         anyFound = true;
                         return true;
                     }
 
-                    if (!anyFound && i.Item1 == DateTime.MinValue)
-                    {
-                        return true;
-                    }
-
-                    return false;
+                    return !anyFound && i.LastWatchedDate == DateTime.MinValue;
                 })
-                .Select(i => i.Item2())
+                .Select(i => i.GetEpisodeFunction())
                 .Where(i => i != null);
         }
 
@@ -195,14 +185,14 @@ namespace Emby.Server.Implementations.TV
         /// Gets the next up.
         /// </summary>
         /// <returns>Task{Episode}.</returns>
-        private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+        private (DateTime LastWatchedDate, Func<Episode> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
         {
             var lastQuery = new InternalItemsQuery(user)
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
                 IncludeItemTypes = new[] { BaseItemKind.Episode },
-                OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Descending) },
+                OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
                 IsPlayed = true,
                 Limit = 1,
                 ParentIndexNumberNotEquals = 0,
@@ -216,24 +206,23 @@ namespace Emby.Server.Implementations.TV
             if (rewatching)
             {
                 // find last watched by date played, not by newest episode watched
-                lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) };
+                lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
             }
 
             var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
 
-            Func<Episode> getEpisode = () =>
+            Episode GetEpisode()
             {
                 var nextQuery = new InternalItemsQuery(user)
                 {
                     AncestorWithPresentationUniqueKey = null,
                     SeriesPresentationUniqueKey = seriesKey,
                     IncludeItemTypes = new[] { BaseItemKind.Episode },
-                    OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
+                    OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
                     Limit = 1,
                     IsPlayed = rewatching,
                     IsVirtualItem = false,
                     ParentIndexNumberNotEquals = 0,
-                    MinSortName = lastWatchedEpisode?.SortName,
                     DtoOptions = dtoOptions
                 };
 
@@ -297,7 +286,7 @@ namespace Emby.Server.Implementations.TV
                 }
 
                 return nextEpisode;
-            };
+            }
 
             if (lastWatchedEpisode != null)
             {
@@ -305,11 +294,11 @@ namespace Emby.Server.Implementations.TV
 
                 var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1);
 
-                return new Tuple<DateTime, Func<Episode>>(lastWatchedDate, getEpisode);
+                return (lastWatchedDate, GetEpisode);
             }
 
             // Return the first episode
-            return new Tuple<DateTime, Func<Episode>>(DateTime.MinValue, getEpisode);
+            return (DateTime.MinValue, GetEpisode);
         }
 
         private static QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, NextUpQuery query)

+ 1 - 6
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs

@@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
         /// <param name="template">The route template. May not be null.</param>
         public HttpSubscribeAttribute(string template)
             : base(_supportedMethods, template)
-        {
-            if (template == null)
-            {
-                throw new ArgumentNullException(nameof(template));
-            }
-        }
+            => ArgumentNullException.ThrowIfNull(template, nameof(template));
     }
 }

+ 1 - 6
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs

@@ -25,11 +25,6 @@ namespace Jellyfin.Api.Attributes
         /// <param name="template">The route template. May not be null.</param>
         public HttpUnsubscribeAttribute(string template)
             : base(_supportedMethods, template)
-        {
-            if (template == null)
-            {
-                throw new ArgumentNullException(nameof(template));
-            }
-        }
+            => ArgumentNullException.ThrowIfNull(template, nameof(template));
     }
 }

+ 37 - 0
Jellyfin.Api/BaseJellyfinApiController.cs

@@ -1,4 +1,6 @@
+using System.Collections.Generic;
 using System.Net.Mime;
+using Jellyfin.Api.Results;
 using Jellyfin.Extensions.Json;
 using Microsoft.AspNetCore.Mvc;
 
@@ -15,5 +17,40 @@ namespace Jellyfin.Api
         JsonDefaults.PascalCaseMediaType)]
     public class BaseJellyfinApiController : ControllerBase
     {
+        /// <summary>
+        /// Create a new <see cref="OkResult{T}"/>.
+        /// </summary>
+        /// <param name="value">The value to return.</param>
+        /// <typeparam name="T">The type to return.</typeparam>
+        /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+        protected ActionResult<IEnumerable<T>> Ok<T>(List<T> value)
+            => new OkResult<IEnumerable<T>>(value);
+
+        /// <summary>
+        /// Create a new <see cref="OkResult{T}"/>.
+        /// </summary>
+        /// <param name="value">The value to return.</param>
+        /// <typeparam name="T">The type to return.</typeparam>
+        /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+        protected ActionResult<IEnumerable<T>> Ok<T>(IReadOnlyList<T> value)
+            => new OkResult<IEnumerable<T>>(value);
+
+        /// <summary>
+        /// Create a new <see cref="OkResult{T}"/>.
+        /// </summary>
+        /// <param name="value">The value to return.</param>
+        /// <typeparam name="T">The type to return.</typeparam>
+        /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+        protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
+            => new OkResult<IEnumerable<T>?>(value);
+
+        /// <summary>
+        /// Create a new <see cref="OkResult{T}"/>.
+        /// </summary>
+        /// <param name="value">The value to return.</param>
+        /// <typeparam name="T">The type to return.</typeparam>
+        /// <returns>The <see cref="ActionResult{T}"/>.</returns>
+        protected ActionResult<T> Ok<T>(T value)
+            => new OkResult<T>(value);
     }
 }

+ 1 - 1
Jellyfin.Api/Controllers/AudioController.cs

@@ -207,7 +207,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
         /// <param name="playSessionId">The play session id.</param>
         /// <param name="segmentContainer">The segment container.</param>
-        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="segmentLength">The segment length.</param>
         /// <param name="minSegments">The minimum number of segments.</param>
         /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>

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

@@ -2,7 +2,6 @@ using System;
 using System.ComponentModel.DataAnnotations;
 using System.Net.Mime;
 using System.Text.Json;
-using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.ConfigurationDtos;

+ 2 - 5
Jellyfin.Api/Controllers/DisplayPreferencesController.cs

@@ -89,12 +89,9 @@ namespace Jellyfin.Api.Controllers
 
             // Load all custom display preferences
             var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
-            if (customDisplayPreferences != null)
+            foreach (var (key, value) in customDisplayPreferences)
             {
-                foreach (var (key, value) in customDisplayPreferences)
-                {
-                    dto.CustomPrefs.TryAdd(key, value);
-                }
+                dto.CustomPrefs.TryAdd(key, value);
             }
 
             // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.

+ 4 - 4
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
         [Produces(MediaTypeNames.Text.Xml)]
         [ProducesFile(MediaTypeNames.Text.Xml)]
-        public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
+        public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
         {
             var url = GetAbsoluteUri();
             var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
@@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Text.Xml)]
         [ProducesFile(MediaTypeNames.Text.Xml)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
-        public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
+        public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
         {
             return Ok(_contentDirectory.GetServiceXml());
         }
@@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Text.Xml)]
         [ProducesFile(MediaTypeNames.Text.Xml)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
-        public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
+        public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
         {
             return Ok(_mediaReceiverRegistrar.GetServiceXml());
         }
@@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers
         [Produces(MediaTypeNames.Text.Xml)]
         [ProducesFile(MediaTypeNames.Text.Xml)]
         [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
-        public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
+        public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId)
         {
             return Ok(_connectionManager.GetServiceXml());
         }

+ 4 - 3
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
         /// <param name="playSessionId">The play session id.</param>
         /// <param name="segmentContainer">The segment container.</param>
-        /// <param name="segmentLength">The segment lenght.</param>
+        /// <param name="segmentLength">The segment length.</param>
         /// <param name="minSegments">The minimum number of segments.</param>
         /// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
         /// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@@ -1790,7 +1790,8 @@ namespace Jellyfin.Api.Controllers
                 || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
             {
                 if (EncodingHelper.IsCopyCodec(codec)
-                    && (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
+                    && (string.Equals(state.VideoStream.VideoRangeType, "DOVI", StringComparison.OrdinalIgnoreCase)
+                        || string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
                         || string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
                         || string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
                 {
@@ -1831,7 +1832,7 @@ namespace Jellyfin.Api.Controllers
                 // Set the key frame params for video encoding to match the hls segment time.
                 args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, isEventPlaylist, startNumber);
 
-                // Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
+                // Currently b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
                 if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
                 {
                     args += " -bf 0";

+ 42 - 15
Jellyfin.Api/Controllers/ItemsController.cs

@@ -1,6 +1,7 @@
 using System;
 using System.ComponentModel.DataAnnotations;
 using System.Linq;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
@@ -9,6 +10,7 @@ using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
@@ -32,6 +34,7 @@ namespace Jellyfin.Api.Controllers
         private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
         private readonly IDtoService _dtoService;
+        private readonly IAuthorizationContext _authContext;
         private readonly ILogger<ItemsController> _logger;
         private readonly ISessionManager _sessionManager;
 
@@ -42,6 +45,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
+        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
         /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
         public ItemsController(
@@ -49,6 +53,7 @@ namespace Jellyfin.Api.Controllers
             ILibraryManager libraryManager,
             ILocalizationManager localization,
             IDtoService dtoService,
+            IAuthorizationContext authContext,
             ILogger<ItemsController> logger,
             ISessionManager sessionManager)
         {
@@ -56,6 +61,7 @@ namespace Jellyfin.Api.Controllers
             _libraryManager = libraryManager;
             _localization = localization;
             _dtoService = dtoService;
+            _authContext = authContext;
             _logger = logger;
             _sessionManager = sessionManager;
         }
@@ -63,7 +69,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Gets items based on a query.
         /// </summary>
-        /// <param name="userId">The user id supplied as query parameter.</param>
+        /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
         /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
         /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
         /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
@@ -151,15 +157,15 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
         [HttpGet("Items")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetItems(
-            [FromQuery] Guid userId,
+        public async Task<ActionResult<QueryResult<BaseItemDto>>> GetItems(
+            [FromQuery] Guid? userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
             [FromQuery] bool? hasThemeVideo,
             [FromQuery] bool? hasSubtitles,
             [FromQuery] bool? hasSpecialFeature,
             [FromQuery] bool? hasTrailer,
-            [FromQuery] string? adjacentTo,
+            [FromQuery] Guid? adjacentTo,
             [FromQuery] int? parentIndexNumber,
             [FromQuery] bool? hasParentalRating,
             [FromQuery] bool? isHd,
@@ -238,7 +244,19 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool enableTotalRecordCount = true,
             [FromQuery] bool? enableImages = true)
         {
-            var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+            var auth = await _authContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+
+            // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
+            var user = !auth.IsApiKey && userId.HasValue && !userId.Value.Equals(default)
+                ? _userManager.GetUserById(userId.Value)
+                : null;
+
+            // beyond this point, we're either using an api key or we have a valid user
+            if (!auth.IsApiKey && user is null)
+            {
+                return BadRequest("userId is required");
+            }
+
             var dtoOptions = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
                 .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@@ -270,30 +288,39 @@ namespace Jellyfin.Api.Controllers
                 includeItemTypes = new[] { BaseItemKind.Playlist };
             }
 
-            var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
+            var enabledChannels = auth.IsApiKey
+                ? Array.Empty<Guid>()
+                : user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
 
-            bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
+            // api keys are always enabled for all folders
+            bool isInEnabledFolder = auth.IsApiKey
+                                     || Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
                                      // Assume all folders inside an EnabledChannel are enabled
                                      || Array.IndexOf(enabledChannels, item.Id) != -1
                                      // Assume all items inside an EnabledChannel are enabled
                                      || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
 
-            var collectionFolders = _libraryManager.GetCollectionFolders(item);
-            foreach (var collectionFolder in collectionFolders)
+            if (!isInEnabledFolder)
             {
-                if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
+                var collectionFolders = _libraryManager.GetCollectionFolders(item);
+                foreach (var collectionFolder in collectionFolders)
                 {
-                    isInEnabledFolder = true;
+                    // api keys never enter this block, so user is never null
+                    if (user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
+                    {
+                        isInEnabledFolder = true;
+                    }
                 }
             }
 
+            // api keys are always enabled for all folders, so user is never null
             if (item is not UserRootFolder
                 && !isInEnabledFolder
-                && !user.HasPermission(PermissionKind.EnableAllFolders)
+                && !user!.HasPermission(PermissionKind.EnableAllFolders)
                 && !user.HasPermission(PermissionKind.EnableAllChannels)
                 && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
             {
-                _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
+                _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name);
                 return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
             }
 
@@ -606,7 +633,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
         [HttpGet("Users/{userId}/Items")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
+        public Task<ActionResult<QueryResult<BaseItemDto>>> GetItemsByUserId(
             [FromRoute] Guid userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
@@ -614,7 +641,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool? hasSubtitles,
             [FromQuery] bool? hasSpecialFeature,
             [FromQuery] bool? hasTrailer,
-            [FromQuery] string? adjacentTo,
+            [FromQuery] Guid? adjacentTo,
             [FromQuery] int? parentIndexNumber,
             [FromQuery] bool? hasParentalRating,
             [FromQuery] bool? isHd,

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

@@ -12,7 +12,6 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;

+ 1 - 1
Jellyfin.Api/Controllers/MoviesController.cs

@@ -170,7 +170,7 @@ namespace Jellyfin.Api.Controllers
                 }
             }
 
-            return Ok(categories.OrderBy(i => i.RecommendationType));
+            return Ok(categories.OrderBy(i => i.RecommendationType).AsEnumerable());
         }
 
         private IEnumerable<RecommendationDto> GetWithDirector(

+ 4 - 1
Jellyfin.Api/Controllers/PersonsController.cs

@@ -98,7 +98,10 @@ namespace Jellyfin.Api.Controllers
                 Limit = limit ?? 0
             });
 
-            return new QueryResult<BaseItemDto>(peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray());
+            return new QueryResult<BaseItemDto>(
+                peopleItems
+                .Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user))
+                .ToArray());
         }
 
         /// <summary>

+ 4 - 4
Jellyfin.Api/Controllers/QuickConnectController.cs

@@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>Whether Quick Connect is enabled on the server or not.</returns>
         [HttpGet("Enabled")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<bool> GetEnabled()
+        public ActionResult<bool> GetQuickConnectEnabled()
         {
             return _quickConnect.IsEnabled;
         }
@@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns>
         [HttpGet("Initiate")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public async Task<ActionResult<QuickConnectResult>> Initiate()
+        public async Task<ActionResult<QuickConnectResult>> InitiateQuickConnect()
         {
             try
             {
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet("Connect")]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status404NotFound)]
-        public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret)
+        public ActionResult<QuickConnectResult> GetQuickConnectState([FromQuery, Required] string secret)
         {
             try
             {
@@ -102,7 +102,7 @@ namespace Jellyfin.Api.Controllers
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         [ProducesResponseType(StatusCodes.Status403Forbidden)]
-        public async Task<ActionResult<bool>> Authorize([FromQuery, Required] string code)
+        public async Task<ActionResult<bool>> AuthorizeQuickConnect([FromQuery, Required] string code)
         {
             var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
             if (!userId.HasValue)

+ 8 - 6
Jellyfin.Api/Controllers/SearchController.cs

@@ -60,9 +60,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
         /// <param name="searchTerm">The search term to filter on.</param>
-        /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
-        /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
-        /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
+        /// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimited.</param>
+        /// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimited.</param>
+        /// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimited.</param>
         /// <param name="parentId">If specified, only children of the parent are returned.</param>
         /// <param name="isMovie">Optional filter for movies.</param>
         /// <param name="isSeries">Optional filter for series.</param>
@@ -79,7 +79,7 @@ namespace Jellyfin.Api.Controllers
         [HttpGet]
         [Description("Gets search hints based on a search term")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<SearchHintResult> Get(
+        public ActionResult<SearchHintResult> GetSearchHints(
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] Guid? userId,
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
                 IndexNumber = item.IndexNumber,
                 ParentIndexNumber = item.ParentIndexNumber,
                 Id = item.Id,
-                Type = item.GetClientTypeName(),
+                Type = item.GetBaseItemKind(),
                 MediaType = item.MediaType,
                 MatchedTerm = hintInfo.MatchedTerm,
                 RunTimeTicks = item.RunTimeTicks,
@@ -149,8 +149,10 @@ namespace Jellyfin.Api.Controllers
                 EndDate = item.EndDate
             };
 
-            // legacy
+#pragma warning disable CS0618
+            // Kept for compatibility with older clients
             result.ItemId = result.Id;
+#pragma warning restore CS0618
 
             if (item.IsFolder)
             {

+ 5 - 4
Jellyfin.Api/Controllers/TrailersController.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
@@ -31,7 +32,7 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Finds movies and trailers similar to a given trailer.
         /// </summary>
-        /// <param name="userId">The user id.</param>
+        /// <param name="userId">The user id supplied as query parameter; this is required when not using an API key.</param>
         /// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
         /// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
         /// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
@@ -118,15 +119,15 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
         [HttpGet]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetTrailers(
-            [FromQuery] Guid userId,
+        public Task<ActionResult<QueryResult<BaseItemDto>>> GetTrailers(
+            [FromQuery] Guid? userId,
             [FromQuery] string? maxOfficialRating,
             [FromQuery] bool? hasThemeSong,
             [FromQuery] bool? hasThemeVideo,
             [FromQuery] bool? hasSubtitles,
             [FromQuery] bool? hasSpecialFeature,
             [FromQuery] bool? hasTrailer,
-            [FromQuery] string? adjacentTo,
+            [FromQuery] Guid? adjacentTo,
             [FromQuery] int? parentIndexNumber,
             [FromQuery] bool? hasParentalRating,
             [FromQuery] bool? isHd,

+ 5 - 5
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -77,7 +77,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
-            [FromQuery] string? seriesId,
+            [FromQuery] Guid? seriesId,
             [FromQuery] Guid? parentId,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? season,
             [FromQuery] Guid? seasonId,
             [FromQuery] bool? isMissing,
-            [FromQuery] string? adjacentTo,
+            [FromQuery] Guid? adjacentTo,
             [FromQuery] Guid? startItemId,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
@@ -278,9 +278,9 @@ namespace Jellyfin.Api.Controllers
             }
 
             // This must be the last filter
-            if (!string.IsNullOrEmpty(adjacentTo))
+            if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
             {
-                episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo).ToList();
+                episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
             }
 
             if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
@@ -326,7 +326,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] bool? isSpecialSeason,
             [FromQuery] bool? isMissing,
-            [FromQuery] string? adjacentTo,
+            [FromQuery] Guid? adjacentTo,
             [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,

+ 38 - 59
Jellyfin.Api/Controllers/UniversalAudioController.cs

@@ -10,13 +10,11 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.StreamingDtos;
 using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
@@ -31,7 +29,6 @@ namespace Jellyfin.Api.Controllers
     public class UniversalAudioController : BaseJellyfinApiController
     {
         private readonly IAuthorizationContext _authorizationContext;
-        private readonly IDeviceManager _deviceManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILogger<UniversalAudioController> _logger;
         private readonly MediaInfoHelper _mediaInfoHelper;
@@ -42,7 +39,6 @@ namespace Jellyfin.Api.Controllers
         /// Initializes a new instance of the <see cref="UniversalAudioController"/> class.
         /// </summary>
         /// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
-        /// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="ILogger{UniversalAudioController}"/> interface.</param>
         /// <param name="mediaInfoHelper">Instance of <see cref="MediaInfoHelper"/>.</param>
@@ -50,7 +46,6 @@ namespace Jellyfin.Api.Controllers
         /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
         public UniversalAudioController(
             IAuthorizationContext authorizationContext,
-            IDeviceManager deviceManager,
             ILibraryManager libraryManager,
             ILogger<UniversalAudioController> logger,
             MediaInfoHelper mediaInfoHelper,
@@ -58,7 +53,6 @@ namespace Jellyfin.Api.Controllers
             DynamicHlsHelper dynamicHlsHelper)
         {
             _authorizationContext = authorizationContext;
-            _deviceManager = deviceManager;
             _libraryManager = libraryManager;
             _logger = logger;
             _mediaInfoHelper = mediaInfoHelper;
@@ -117,76 +111,61 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] bool enableRedirection = true)
         {
             var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
-            (await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false)).DeviceId = deviceId;
+            var authorizationInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
+            authorizationInfo.DeviceId = deviceId;
+
+            if (!userId.HasValue || userId.Value.Equals(Guid.Empty))
+            {
+                userId = authorizationInfo.UserId;
+            }
 
             var authInfo = await _authorizationContext.GetAuthorizationInfo(Request).ConfigureAwait(false);
 
             _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile);
 
-            if (deviceProfile == null)
-            {
-                var clientCapabilities = _deviceManager.GetCapabilities(authInfo.DeviceId);
-                if (clientCapabilities != null)
-                {
-                    deviceProfile = clientCapabilities.DeviceProfile;
-                }
-            }
-
             var info = await _mediaInfoHelper.GetPlaybackInfo(
                     itemId,
                     userId,
                     mediaSourceId)
                 .ConfigureAwait(false);
 
-            if (deviceProfile != null)
-            {
-                // set device specific data
-                var item = _libraryManager.GetItemById(itemId);
+            // set device specific data
+            var item = _libraryManager.GetItemById(itemId);
 
-                foreach (var sourceInfo in info.MediaSources)
-                {
-                    _mediaInfoHelper.SetDeviceSpecificData(
-                        item,
-                        sourceInfo,
-                        deviceProfile,
-                        authInfo,
-                        maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
-                        startTimeTicks ?? 0,
-                        mediaSourceId ?? string.Empty,
-                        null,
-                        null,
-                        maxAudioChannels,
-                        info.PlaySessionId!,
-                        userId ?? Guid.Empty,
-                        true,
-                        true,
-                        true,
-                        true,
-                        true,
-                        Request.HttpContext.GetNormalizedRemoteIp());
-                }
-
-                _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+            foreach (var sourceInfo in info.MediaSources)
+            {
+                _mediaInfoHelper.SetDeviceSpecificData(
+                    item,
+                    sourceInfo,
+                    deviceProfile,
+                    authInfo,
+                    maxStreamingBitrate ?? deviceProfile.MaxStreamingBitrate,
+                    startTimeTicks ?? 0,
+                    mediaSourceId ?? string.Empty,
+                    null,
+                    null,
+                    maxAudioChannels,
+                    info.PlaySessionId!,
+                    userId ?? Guid.Empty,
+                    true,
+                    true,
+                    true,
+                    true,
+                    true,
+                    Request.HttpContext.GetNormalizedRemoteIp());
             }
 
-            if (info.MediaSources != null)
+            _mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
+
+            foreach (var source in info.MediaSources)
             {
-                foreach (var source in info.MediaSources)
-                {
-                    _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile!, DlnaProfileType.Video);
-                }
+                _mediaInfoHelper.NormalizeMediaSourceContainer(source, deviceProfile, DlnaProfileType.Video);
             }
 
-            var mediaSource = info.MediaSources![0];
-            if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http)
+            var mediaSource = info.MediaSources[0];
+            if (mediaSource.SupportsDirectPlay && mediaSource.Protocol == MediaProtocol.Http && enableRedirection && mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
             {
-                if (enableRedirection)
-                {
-                    if (mediaSource.IsRemote && enableRemoteMedia.HasValue && enableRemoteMedia.Value)
-                    {
-                        return Redirect(mediaSource.Path);
-                    }
-                }
+                return Redirect(mediaSource.Path);
             }
 
             var isStatic = mediaSource.SupportsDirectStream;
@@ -249,7 +228,7 @@ namespace Jellyfin.Api.Controllers
                 BreakOnNonKeyFrames = breakOnNonKeyFrames,
                 AudioSampleRate = maxAudioSampleRate,
                 MaxAudioChannels = maxAudioChannels,
-                AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
+                AudioBitRate = isStatic ? null : (audioBitRate ?? maxStreamingBitrate),
                 MaxAudioBitDepth = maxAudioBitDepth,
                 AudioChannels = maxAudioChannels,
                 CopyTimestamps = true,

+ 13 - 10
Jellyfin.Api/Controllers/UserController.cs

@@ -282,16 +282,19 @@ namespace Jellyfin.Api.Controllers
             }
             else
             {
-                var success = await _userManager.AuthenticateUser(
-                    user.Username,
-                    request.CurrentPw,
-                    request.CurrentPw,
-                    HttpContext.GetNormalizedRemoteIp().ToString(),
-                    false).ConfigureAwait(false);
-
-                if (success == null)
+                if (!HttpContext.User.IsInRole(UserRoles.Administrator))
                 {
-                    return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
+                    var success = await _userManager.AuthenticateUser(
+                        user.Username,
+                        request.CurrentPw,
+                        request.CurrentPw,
+                        HttpContext.GetNormalizedRemoteIp().ToString(),
+                        false).ConfigureAwait(false);
+
+                    if (success == null)
+                    {
+                        return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
+                    }
                 }
 
                 await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -499,7 +502,7 @@ namespace Jellyfin.Api.Controllers
 
             if (isLocal)
             {
-                _logger.LogWarning("Password reset proccess initiated from outside the local network with IP: {IP}", ip);
+                _logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
             }
 
             var result = await _userManager.StartForgotPasswordProcess(forgotPasswordRequest.EnteredUsername, isLocal).ConfigureAwait(false);

+ 2 - 2
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -8,7 +8,6 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -234,7 +233,8 @@ namespace Jellyfin.Api.Controllers
             var dtoOptions = new DtoOptions().AddClientFields(Request);
 
             return Ok(item
-                .GetExtras(BaseItem.DisplayExtraTypes)
+                .GetExtras()
+                .Where(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value))
                 .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
         }
 

+ 2 - 8
Jellyfin.Api/Controllers/UserViewsController.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
-using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
@@ -11,9 +10,7 @@ using Jellyfin.Api.Models.UserViewDtos;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using Microsoft.AspNetCore.Authorization;
@@ -32,7 +29,6 @@ namespace Jellyfin.Api.Controllers
         private readonly IUserManager _userManager;
         private readonly IUserViewManager _userViewManager;
         private readonly IDtoService _dtoService;
-        private readonly IAuthorizationContext _authContext;
         private readonly ILibraryManager _libraryManager;
 
         /// <summary>
@@ -41,19 +37,16 @@ namespace Jellyfin.Api.Controllers
         /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
         /// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
         /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
-        /// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
         /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
         public UserViewsController(
             IUserManager userManager,
             IUserViewManager userViewManager,
             IDtoService dtoService,
-            IAuthorizationContext authContext,
             ILibraryManager libraryManager)
         {
             _userManager = userManager;
             _userViewManager = userViewManager;
             _dtoService = dtoService;
-            _authContext = authContext;
             _libraryManager = libraryManager;
         }
 
@@ -138,7 +131,8 @@ namespace Jellyfin.Api.Controllers
                     Name = i.Name,
                     Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
                 })
-                .OrderBy(i => i.Name));
+                .OrderBy(i => i.Name)
+                .AsEnumerable());
         }
     }
 }

+ 1 - 1
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -654,7 +654,7 @@ namespace Jellyfin.Api.Helpers
         {
             if (EnableThrottling(state))
             {
-                transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem);
+                transcodingJob.TranscodingThrottler = new TranscodingThrottler(transcodingJob, new Logger<TranscodingThrottler>(new LoggerFactory()), _serverConfigurationManager, _fileSystem, _mediaEncoder);
                 transcodingJob.TranscodingThrottler.Start();
             }
         }

+ 2 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -17,10 +17,10 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.6" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
-    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
+    <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 11 - 4
Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs

@@ -2,6 +2,7 @@
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
@@ -17,6 +18,7 @@ namespace Jellyfin.Api.Models.PlaybackDtos
         private readonly ILogger<TranscodingThrottler> _logger;
         private readonly IConfigurationManager _config;
         private readonly IFileSystem _fileSystem;
+        private readonly IMediaEncoder _mediaEncoder;
         private Timer? _timer;
         private bool _isPaused;
 
@@ -27,12 +29,14 @@ namespace Jellyfin.Api.Models.PlaybackDtos
         /// <param name="logger">Instance of the <see cref="ILogger{TranscodingThrottler}"/> interface.</param>
         /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
         /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem)
+        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
+        public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder)
         {
             _job = job;
             _logger = logger;
             _config = config;
             _fileSystem = fileSystem;
+            _mediaEncoder = mediaEncoder;
         }
 
         /// <summary>
@@ -55,7 +59,8 @@ namespace Jellyfin.Api.Models.PlaybackDtos
 
                 try
                 {
-                    await _job.Process!.StandardInput.WriteLineAsync().ConfigureAwait(false);
+                    var resumeKey = _mediaEncoder.IsPkeyPauseSupported ? "u" : Environment.NewLine;
+                    await _job.Process!.StandardInput.WriteAsync(resumeKey).ConfigureAwait(false);
                     _isPaused = false;
                 }
                 catch (Exception ex)
@@ -125,11 +130,13 @@ namespace Jellyfin.Api.Models.PlaybackDtos
         {
             if (!_isPaused)
             {
-                _logger.LogDebug("Sending pause command to ffmpeg");
+                var pauseKey = _mediaEncoder.IsPkeyPauseSupported ? "p" : "c";
+
+                _logger.LogDebug("Sending pause command [{Key}] to ffmpeg", pauseKey);
 
                 try
                 {
-                    await _job.Process!.StandardInput.WriteAsync("c").ConfigureAwait(false);
+                    await _job.Process!.StandardInput.WriteAsync(pauseKey).ConfigureAwait(false);
                     _isPaused = true;
                 }
                 catch (Exception ex)

+ 1 - 1
Jellyfin.Api/Models/StreamingDtos/StreamState.cs

@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Models.StreamingDtos
         /// <summary>
         /// Disposes the stream state.
         /// </summary>
-        /// <param name="disposing">Whether the object is currently beeing disposed.</param>
+        /// <param name="disposing">Whether the object is currently being disposed.</param>
         protected virtual void Dispose(bool disposing)
         {
             if (_disposed)

+ 2 - 2
Jellyfin.Api/Models/SyncPlayDtos/RemoveFromPlaylistRequestDto.cs

@@ -17,9 +17,9 @@ namespace Jellyfin.Api.Models.SyncPlayDtos
         }
 
         /// <summary>
-        /// Gets or sets the playlist identifiers ot the items. Ignored when clearing the playlist.
+        /// Gets or sets the playlist identifiers of the items. Ignored when clearing the playlist.
         /// </summary>
-        /// <value>The playlist identifiers ot the items.</value>
+        /// <value>The playlist identifiers of the items.</value>
         public IReadOnlyList<Guid> PlaylistItemIds { get; set; }
 
         /// <summary>

+ 21 - 0
Jellyfin.Api/Results/OkResultOfT.cs

@@ -0,0 +1,21 @@
+#pragma warning disable SA1649 // File name should match type name.
+
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Results;
+
+/// <summary>
+/// Ok result with type specified.
+/// </summary>
+/// <typeparam name="T">The type to return.</typeparam>
+public class OkResult<T> : OkObjectResult
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="OkResult{T}"/> class.
+    /// </summary>
+    /// <param name="value">The value to return.</param>
+    public OkResult(T value)
+        : base(value)
+    {
+    }
+}

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

@@ -18,8 +18,8 @@
   <ItemGroup>
     <PackageReference Include="BlurHashSharp" Version="1.2.0" />
     <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
-    <PackageReference Include="SkiaSharp" Version="2.88.1-preview.71" />
-    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.71" />
+    <PackageReference Include="SkiaSharp" Version="2.88.2" />
+    <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.2" />
     <PackageReference Include="SkiaSharp.Svg" Version="1.60.0" />
   </ItemGroup>
 

+ 4 - 2
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -145,9 +145,11 @@ namespace Jellyfin.Drawing.Skia
         /// <exception cref="SkiaCodecException">The file at the specified path could not be used to generate a codec.</exception>
         public string GetImageBlurHash(int xComp, int yComp, string path)
         {
-            if (path == null)
+            ArgumentNullException.ThrowIfNull(path, nameof(path));
+
+            if (path.Length == 0)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("String can't be empty", nameof(path));
             }
 
             var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');

+ 1 - 1
Jellyfin.Drawing.Skia/SkiaHelper.cs

@@ -13,7 +13,7 @@ namespace Jellyfin.Drawing.Skia
         /// </summary>
         /// <param name="skiaEncoder">The current skia encoder.</param>
         /// <param name="paths">The list of image paths.</param>
-        /// <param name="currentIndex">The current checked indes.</param>
+        /// <param name="currentIndex">The current checked index.</param>
         /// <param name="newIndex">The new index.</param>
         /// <returns>A valid bitmap, or null if no bitmap exists after <c>currentIndex</c>.</returns>
         public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)

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

@@ -27,13 +27,13 @@
 
   <ItemGroup>
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.6" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.6">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.6">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 1 - 1
Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs

@@ -67,7 +67,7 @@ namespace Jellyfin.Server.Implementations.Users
                 else if (string.Equals(
                     spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
                     pin.Replace("-", string.Empty, StringComparison.Ordinal),
-                    StringComparison.OrdinalIgnoreCase))
+                    StringComparison.Ordinal))
                 {
                     var resetUser = userManager.GetUserByName(spr.UserName)
                         ?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");

+ 1 - 1
Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs

@@ -79,7 +79,7 @@ namespace Jellyfin.Server.Implementations.Users
         }
 
         /// <inheritdoc />
-        public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences)
+        public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string?> customPreferences)
         {
             var existingPrefs = _dbContext.CustomItemDisplayPreferences
                 .AsQueryable()

+ 4 - 4
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -326,10 +326,10 @@ namespace Jellyfin.Server.Implementations.Users
                     EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
                     RememberSubtitleSelections = user.RememberSubtitleSelections,
                     SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
-                    OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
-                    GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
-                    MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
-                    LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
+                    OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
+                    GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
+                    MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
+                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
                 },
                 Policy = new UserPolicy
                 {

+ 2 - 9
Jellyfin.Server/Infrastructure/SymlinkFollowingPhysicalFileResultExecutor.cs

@@ -69,15 +69,8 @@ namespace Jellyfin.Server.Infrastructure
         /// <inheritdoc />
         protected override Task WriteFileAsync(ActionContext context, PhysicalFileResult result, RangeItemHeaderValue? range, long rangeLength)
         {
-            if (context == null)
-            {
-                throw new ArgumentNullException(nameof(context));
-            }
-
-            if (result == null)
-            {
-                throw new ArgumentNullException(nameof(result));
-            }
+            ArgumentNullException.ThrowIfNull(context, nameof(context));
+            ArgumentNullException.ThrowIfNull(result, nameof(result));
 
             if (range != null && rangeLength == 0)
             {

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

@@ -37,18 +37,18 @@
     <PackageReference Include="CommandLineParser" Version="2.9.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.6" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.6" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
     <PackageReference Include="prometheus-net" Version="6.0.0" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
     <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
-    <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
+    <PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />
     <PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
-    <PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
+    <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
     <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
-    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" />
+    <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
Jellyfin.Server/Program.cs

@@ -243,7 +243,7 @@ namespace Jellyfin.Server
                     }
                 }
 
-                appHost.Dispose();
+                await appHost.DisposeAsync().ConfigureAwait(false);
             }
 
             if (_restartOnShutdown)

+ 17 - 0
Jellyfin.Server/Startup.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Globalization;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http.Headers;
@@ -103,6 +104,22 @@ namespace Jellyfin.Server
                 })
                 .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
 
+            services.AddHttpClient(NamedClient.Dlna, c =>
+                {
+                    c.DefaultRequestHeaders.UserAgent.ParseAdd(
+                        string.Format(
+                            CultureInfo.InvariantCulture,
+                            "{0}/{1} UPnP/1.0 {2}/{3}",
+                            MediaBrowser.Common.System.OperatingSystem.Name,
+                            Environment.OSVersion,
+                            _serverApplicationHost.Name,
+                            _serverApplicationHost.ApplicationVersionString));
+
+                    c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+                    c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from?
+                })
+                .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
+
             services.AddHealthChecks()
                 .AddDbContextCheck<JellyfinDb>();
 

+ 18 - 7
MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs

@@ -1,22 +1,33 @@
-#nullable disable
-#pragma warning disable CS1591
-
 using System;
 
 namespace MediaBrowser.Common.Configuration
 {
+    /// <summary>
+    /// <see cref="EventArgs" /> for the ConfigurationUpdated event.
+    /// </summary>
     public class ConfigurationUpdateEventArgs : EventArgs
     {
         /// <summary>
-        /// Gets or sets the key.
+        /// Initializes a new instance of the <see cref="ConfigurationUpdateEventArgs"/> class.
+        /// </summary>
+        /// <param name="key">The configuration key.</param>
+        /// <param name="newConfiguration">The new configuration.</param>
+        public ConfigurationUpdateEventArgs(string key, object newConfiguration)
+        {
+            Key = key;
+            NewConfiguration = newConfiguration;
+        }
+
+        /// <summary>
+        /// Gets the key.
         /// </summary>
         /// <value>The key.</value>
-        public string Key { get; set; }
+        public string Key { get; }
 
         /// <summary>
-        /// Gets or sets the new configuration.
+        /// Gets the new configuration.
         /// </summary>
         /// <value>The new configuration.</value>
-        public object NewConfiguration { get; set; }
+        public object NewConfiguration { get; }
     }
 }

+ 0 - 2
MediaBrowser.Common/Configuration/IApplicationPaths.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 namespace MediaBrowser.Common.Configuration
 {
     /// <summary>

+ 5 - 0
MediaBrowser.Common/Net/NamedClient.cs

@@ -14,5 +14,10 @@
         /// Gets the value for the MusicBrainz named http client.
         /// </summary>
         public const string MusicBrainz = nameof(MusicBrainz);
+
+        /// <summary>
+        /// Gets the value for the DLNA named http client.
+        /// </summary>
+        public const string Dlna = nameof(Dlna);
     }
 }

+ 2 - 2
MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs

@@ -169,8 +169,8 @@ namespace MediaBrowser.Controller.Entities.Audio
 
             var childUpdateType = ItemUpdateType.None;
 
-            // Refresh songs
-            foreach (var item in items)
+            // Refresh songs only and not m3u files in album folder
+            foreach (var item in items.OfType<Audio>())
             {
                 cancellationToken.ThrowIfCancellationRequested();
 

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

@@ -2616,7 +2616,8 @@ namespace MediaBrowser.Controller.Entities
             return ExtraIds
                 .Select(LibraryManager.GetItemById)
                 .Where(i => i != null)
-                .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value));
+                .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value))
+                .OrderBy(i => i.SortName);
         }
 
         public virtual long GetRunTimeTicksForPlayState()

+ 1 - 3
MediaBrowser.Controller/Entities/BasePluginFolder.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System.Text.Json.Serialization;
@@ -13,7 +11,7 @@ namespace MediaBrowser.Controller.Entities
     public abstract class BasePluginFolder : Folder, ICollectionFolder
     {
         [JsonIgnore]
-        public virtual string CollectionType => null;
+        public virtual string? CollectionType => null;
 
         [JsonIgnore]
         public override bool SupportsInheritedParentImages => false;

+ 4 - 4
MediaBrowser.Controller/Entities/Extensions.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Linq;
 using Jellyfin.Extensions;
@@ -19,9 +17,11 @@ namespace MediaBrowser.Controller.Entities
         /// <param name="url">Trailer URL.</param>
         public static void AddTrailerUrl(this BaseItem item, string url)
         {
-            if (string.IsNullOrEmpty(url))
+            ArgumentNullException.ThrowIfNull(url, nameof(url));
+
+            if (url.Length == 0)
             {
-                throw new ArgumentNullException(nameof(url));
+                throw new ArgumentException("String can't be empty", nameof(url));
             }
 
             var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase));

+ 4 - 26
MediaBrowser.Controller/Entities/Folder.cs

@@ -860,7 +860,7 @@ namespace MediaBrowser.Controller.Entities
                 return true;
             }
 
-            if (!string.IsNullOrEmpty(query.AdjacentTo))
+            if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
             {
                 Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
                 return true;
@@ -892,29 +892,7 @@ namespace MediaBrowser.Controller.Entities
 
         private static BaseItem[] SortItemsByRequest(InternalItemsQuery query, IReadOnlyList<BaseItem> items)
         {
-            var ids = query.ItemIds;
-            int size = items.Count;
-
-            // ids can potentially contain non-unique guids, but query result cannot,
-            // so we include only first occurrence of each guid
-            var positions = new Dictionary<Guid, int>(size);
-            int index = 0;
-            for (int i = 0; i < ids.Length; i++)
-            {
-                if (positions.TryAdd(ids[i], index))
-                {
-                    index++;
-                }
-            }
-
-            var newItems = new BaseItem[size];
-            for (int i = 0; i < size; i++)
-            {
-                var item = items[i];
-                newItems[positions[item.Id]] = item;
-            }
-
-            return newItems;
+            return items.OrderBy(i => Array.IndexOf(query.ItemIds, i.Id)).ToArray();
         }
 
         public QueryResult<BaseItem> GetItems(InternalItemsQuery query)
@@ -1029,9 +1007,9 @@ namespace MediaBrowser.Controller.Entities
             #pragma warning restore CA1309
 
             // This must be the last filter
-            if (!string.IsNullOrEmpty(query.AdjacentTo))
+            if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
             {
-                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo);
+                items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
             }
 
             return UserViewBuilder.SortAndPage(items, null, query, LibraryManager, enableSorting);

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

@@ -129,7 +129,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Guid[] ExcludeItemIds { get; set; }
 
-        public string? AdjacentTo { get; set; }
+        public Guid? AdjacentTo { get; set; }
 
         public string[] PersonTypes { get; set; }
 

+ 1 - 1
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -244,7 +244,7 @@ namespace MediaBrowser.Controller.Entities.TV
         /// <summary>
         /// This is called before any metadata refresh and returns true or false indicating if changes were made.
         /// </summary>
-        /// <param name="replaceAllMetadata"><c>true</c> to replace metdata, <c>false</c> to not.</param>
+        /// <param name="replaceAllMetadata"><c>true</c> to replace metadata, <c>false</c> to not.</param>
         /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
         public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
         {

+ 1 - 1
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -266,7 +266,7 @@ namespace MediaBrowser.Controller.Entities.TV
                 DtoOptions = options
             };
 
-            if (!user.DisplayMissingEpisodes)
+            if (user == null || !user.DisplayMissingEpisodes)
             {
                 query.IsMissing = false;
             }

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio