From 37b50fe13c689e8fb89288da12f79bf7164e1194 Mon Sep 17 00:00:00 2001 From: Cosmin Dumitru Date: Wed, 18 Feb 2026 21:08:35 +0100 Subject: [PATCH 1/6] Fix malformed query string in StreamInfo.ToUrl() causing 500 error via proxies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StreamInfo.ToUrl() generated URLs like `/master.m3u8?&DeviceId=...` (note `?&`) because `?` was appended to the path and all parameters started with `&`. When the first optional parameter (DeviceProfileId) was null, the result was a malformed query string. This is harmless when clients hit Jellyfin directly (ASP.NET Core tolerates `?&`), but when accessed through a reverse proxy that parses and re-serializes the URL (e.g. Home Assistant ingress via aiohttp/yarl), `?&` becomes `?=&` — introducing an empty-key query parameter. ParseStreamOptions then crashes on `param.Key[0]` with IndexOutOfRangeException. Changes: - StreamInfo.ToUrl(): Track query start position and replace the first `&` with `?` after all parameters are appended, producing valid query strings - ParseStreamOptions: Guard against empty query parameter keys - Tests: Remove .Replace("?&", "?") workaround that masked the bug Co-Authored-By: Claude Opus 4.6 --- Jellyfin.Api/Helpers/StreamingHelpers.cs | 2 +- MediaBrowser.Model/Dlna/StreamInfo.cs | 12 +++++++++--- tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs | 6 ++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 1e984542ec..c6823fa807 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -268,7 +268,7 @@ public static class StreamingHelpers Dictionary streamOptions = new Dictionary(); foreach (var param in queryString) { - if (char.IsLower(param.Key[0])) + if (param.Key.Length > 0 && char.IsLower(param.Key[0])) { // This was probably not parsed initially and should be a StreamOptions // or the generated URL should correctly serialize it diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 551bee89e3..7aad97ce01 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -895,7 +895,7 @@ public class StreamInfo if (SubProtocol == MediaStreamProtocol.hls) { - sb.Append("/master.m3u8?"); + sb.Append("/master.m3u8"); } else { @@ -906,10 +906,10 @@ public class StreamInfo sb.Append('.'); sb.Append(Container); } - - sb.Append('?'); } + var queryStart = sb.Length; + if (!string.IsNullOrEmpty(DeviceProfileId)) { sb.Append("&DeviceProfileId="); @@ -1133,6 +1133,12 @@ public class StreamInfo sb.Append(query); } + // Replace the first '&' with '?' to form a valid query string. + if (sb.Length > queryStart) + { + sb[queryStart] = '?'; + } + return sb.ToString(); } diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs index 8dea468064..4b3126fe11 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamInfoTests.cs @@ -216,8 +216,7 @@ public class StreamInfoTests string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123"); - // New version will return and & after the ? due to optional parameters. - string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase); + string newUrl = streamInfo.ToUrl(BaseUrl, "123", null); Assert.Equal(legacyUrl, newUrl, ignoreCase: true); } @@ -234,8 +233,7 @@ public class StreamInfoTests FillAllProperties(streamInfo); string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123"); - // New version will return and & after the ? due to optional parameters. - string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase); + string newUrl = streamInfo.ToUrl(BaseUrl, "123", null); Assert.Equal(legacyUrl, newUrl, ignoreCase: true); } From 94dcaf2ea24e428f2957674ac46b864625782194 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 18 Feb 2026 22:39:49 +0100 Subject: [PATCH 2/6] Upgrade Swashbuckle to v10 --- Directory.Packages.props | 4 +- .../ApiApplicationBuilderExtensions.cs | 2 +- .../ApiServiceCollectionExtensions.cs | 21 ++-- .../Filters/AdditionalModelFilter.cs | 101 ++++++++---------- .../Filters/CachingOpenApiProvider.cs | 4 +- Jellyfin.Server/Filters/FileRequestFilter.cs | 5 +- Jellyfin.Server/Filters/FileResponseFilter.cs | 11 +- .../Filters/FlagsEnumSchemaFilter.cs | 23 ++-- .../Filters/IgnoreEnumSchemaFilter.cs | 17 +-- .../Filters/ParameterObsoleteFilter.cs | 12 ++- .../RetryOnTemporarilyUnavailableFilter.cs | 10 +- .../SecurityRequirementsOperationFilter.cs | 15 +-- .../SecuritySchemeReferenceFixupFilter.cs | 56 ++++++++++ 13 files changed, 168 insertions(+), 113 deletions(-) create mode 100644 Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a520b87e2b..cee12a48ae 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,8 +75,8 @@ - - + + diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 9fd853cf2e..2aadedfa61 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -3,7 +3,7 @@ using Jellyfin.Api.Middleware; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; namespace Jellyfin.Server.Extensions { diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 9df24fa0d7..c71c193e2e 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Security.Claims; +using System.Text.Json.Nodes; using Emby.Server.Implementations; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.AnonymousLanAccessPolicy; @@ -26,7 +26,6 @@ using Jellyfin.Server.Filters; using MediaBrowser.Common.Api; using MediaBrowser.Common.Net; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -34,9 +33,7 @@ using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Interfaces; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes; @@ -208,7 +205,7 @@ namespace Jellyfin.Server.Extensions { { "x-jellyfin-version", - new OpenApiString(version) + new JsonNodeExtension(JsonValue.Create(version)) } } }); @@ -262,6 +259,7 @@ namespace Jellyfin.Server.Extensions c.OperationFilter(); c.OperationFilter(); c.DocumentFilter(); + c.DocumentFilter(); }) .Replace(ServiceDescriptor.Transient()); } @@ -333,10 +331,10 @@ namespace Jellyfin.Server.Extensions options.MapType>(() => new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, AdditionalProperties = new OpenApiSchema { - Type = "string" + Type = JsonSchemaType.String } }); @@ -344,18 +342,17 @@ namespace Jellyfin.Server.Extensions options.MapType>(() => new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, AdditionalProperties = new OpenApiSchema { - Type = "string", - Nullable = true + Type = JsonSchemaType.String | JsonSchemaType.Null } }); // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it. options.MapType(() => new OpenApiSchema { - Type = "string" + Type = JsonSchemaType.String }); } } diff --git a/Jellyfin.Server/Filters/AdditionalModelFilter.cs b/Jellyfin.Server/Filters/AdditionalModelFilter.cs index 7407bd2eb7..efa2f4cca5 100644 --- a/Jellyfin.Server/Filters/AdditionalModelFilter.cs +++ b/Jellyfin.Server/Filters/AdditionalModelFilter.cs @@ -3,18 +3,17 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; +using System.Text.Json.Nodes; using Jellyfin.Extensions; using Jellyfin.Server.Migrations; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; -using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.ApiClient; using MediaBrowser.Model.Session; using MediaBrowser.Model.SyncPlay; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -25,7 +24,7 @@ namespace Jellyfin.Server.Filters public class AdditionalModelFilter : IDocumentFilter { // Array of options that should not be visible in the api spec. - private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions) }; + private static readonly Type[] _ignoredConfigurations = [typeof(MigrationOptions), typeof(MediaBrowser.Model.Branding.BrandingOptions)]; private readonly IServerConfigurationManager _serverConfigurationManager; /// @@ -48,8 +47,8 @@ namespace Jellyfin.Server.Filters && t != typeof(WebSocketMessageInfo)) .ToList(); - var inboundWebSocketSchemas = new List(); - var inboundWebSocketDiscriminators = new Dictionary(); + var inboundWebSocketSchemas = new List(); + var inboundWebSocketDiscriminators = new Dictionary(); foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value; @@ -60,18 +59,16 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); inboundWebSocketSchemas.Add(schema); - inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3; + if (schema is OpenApiSchemaReference schemaRef) + { + inboundWebSocketDiscriminators[messageType.ToString()!] = schemaRef; + } } var inboundWebSocketMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible inbound websocket types", - Reference = new OpenApiReference - { - Id = nameof(InboundWebSocketMessage), - Type = ReferenceType.Schema - }, OneOf = inboundWebSocketSchemas, Discriminator = new OpenApiDiscriminator { @@ -82,8 +79,8 @@ namespace Jellyfin.Server.Filters context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema); - var outboundWebSocketSchemas = new List(); - var outboundWebSocketDiscriminators = new Dictionary(); + var outboundWebSocketSchemas = new List(); + var outboundWebSocketDiscriminators = new Dictionary(); foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t))) { var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute()?.Value; @@ -94,58 +91,55 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); outboundWebSocketSchemas.Add(schema); - outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3); + if (schema is OpenApiSchemaReference schemaRef) + { + outboundWebSocketDiscriminators.Add(messageType.ToString()!, schemaRef); + } } // Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us var syncPlayGroupUpdateMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Untyped sync play command.", - Properties = new Dictionary + Properties = new Dictionary { { "Data", new OpenApiSchema { - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate) } } - ], + AllOf = new List + { + new OpenApiSchemaReference(nameof(GroupUpdate), null, null) + }, Description = "Group update data", - Nullable = false, } }, - { "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } }, + { "MessageId", new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid", Description = "Gets or sets the message id." } }, { "MessageType", new OpenApiSchema { - Enum = Enum.GetValues().Select(type => new OpenApiString(type.ToString())).ToList(), - AllOf = - [ - new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } } - ], + Enum = Enum.GetValues().Select(type => (JsonNode)JsonValue.Create(type.ToString())!).ToList(), + AllOf = new List + { + new OpenApiSchemaReference(nameof(SessionMessageType), null, null) + }, Description = "The different kinds of messages that are used in the WebSocket api.", - Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)), + Default = JsonValue.Create(nameof(SessionMessageType.SyncPlayGroupUpdate)), ReadOnly = true } }, }, AdditionalPropertiesAllowed = false, - Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" } }; context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema); - outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema); - outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3; + var syncPlayRef = new OpenApiSchemaReference("SyncPlayGroupUpdateMessage", null, null); + outboundWebSocketSchemas.Add(syncPlayRef); + outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayRef; var outboundWebSocketMessageSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible outbound websocket types", - Reference = new OpenApiReference - { - Id = nameof(OutboundWebSocketMessage), - Type = ReferenceType.Schema - }, OneOf = outboundWebSocketSchemas, Discriminator = new OpenApiDiscriminator { @@ -159,17 +153,12 @@ namespace Jellyfin.Server.Filters nameof(WebSocketMessage), new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the possible websocket types", - Reference = new OpenApiReference + OneOf = new List { - Id = nameof(WebSocketMessage), - Type = ReferenceType.Schema - }, - OneOf = new[] - { - inboundWebSocketMessageSchema, - outboundWebSocketMessageSchema + new OpenApiSchemaReference(nameof(InboundWebSocketMessage), null, null), + new OpenApiSchemaReference(nameof(OutboundWebSocketMessage), null, null) } }); @@ -180,8 +169,8 @@ namespace Jellyfin.Server.Filters && t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>)) .ToList(); - var groupUpdateSchemas = new List(); - var groupUpdateDiscriminators = new Dictionary(); + var groupUpdateSchemas = new List(); + var groupUpdateDiscriminators = new Dictionary(); foreach (var type in groupUpdateTypes) { var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate.Type))?.GetCustomAttribute()?.Value; @@ -192,18 +181,16 @@ namespace Jellyfin.Server.Filters var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); groupUpdateSchemas.Add(schema); - groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3; + if (schema is OpenApiSchemaReference schemaRef) + { + groupUpdateDiscriminators[groupUpdateType.ToString()!] = schemaRef; + } } var groupUpdateSchema = new OpenApiSchema { - Type = "object", + Type = JsonSchemaType.Object, Description = "Represents the list of possible group update types", - Reference = new OpenApiReference - { - Id = nameof(GroupUpdate), - Type = ReferenceType.Schema - }, OneOf = groupUpdateSchemas, Discriminator = new OpenApiDiscriminator { diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index 833b684444..fdc49a9840 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -48,7 +48,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider } /// - public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null) + public OpenApiDocument GetSwagger(string documentName, string host, string basePath) { if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null) { diff --git a/Jellyfin.Server/Filters/FileRequestFilter.cs b/Jellyfin.Server/Filters/FileRequestFilter.cs index 86dbf7657e..3d5b1fdf1f 100644 --- a/Jellyfin.Server/Filters/FileRequestFilter.cs +++ b/Jellyfin.Server/Filters/FileRequestFilter.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -28,10 +28,11 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; + body.Content ??= new System.Collections.Generic.Dictionary(); foreach (var contentType in contentTypes) { body.Content.Add(contentType, mediaType); diff --git a/Jellyfin.Server/Filters/FileResponseFilter.cs b/Jellyfin.Server/Filters/FileResponseFilter.cs index cd0acadf32..64aea62519 100644 --- a/Jellyfin.Server/Filters/FileResponseFilter.cs +++ b/Jellyfin.Server/Filters/FileResponseFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -14,7 +14,7 @@ namespace Jellyfin.Server.Filters { Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "binary" } }; @@ -22,6 +22,11 @@ namespace Jellyfin.Server.Filters /// public void Apply(OpenApiOperation operation, OperationFilterContext context) { + if (operation.Responses is null) + { + return; + } + foreach (var attribute in context.ApiDescription.ActionDescriptor.EndpointMetadata) { if (attribute is ProducesFileAttribute producesFileAttribute) @@ -31,7 +36,7 @@ namespace Jellyfin.Server.Filters .FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal)); // Operation doesn't have a response. - if (response.Value is null) + if (response.Value?.Content is null) { continue; } diff --git a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs index 3e0b69d017..0c1f4197ce 100644 --- a/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/FlagsEnumSchemaFilter.cs @@ -1,5 +1,5 @@ using System; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class FlagsEnumSchemaFilter : ISchemaFilter { /// - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type); if (type is null || !type.IsEnum) @@ -29,11 +29,16 @@ public class FlagsEnumSchemaFilter : ISchemaFilter return; } + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + if (context.MemberInfo is null) { // Processing the enum definition itself - ensure it's type "string" not "integer" - schema.Type = "string"; - schema.Format = null; + concreteSchema.Type = JsonSchemaType.String; + concreteSchema.Format = null; } else { @@ -43,11 +48,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter // Flags enums should be represented as arrays referencing the enum schema // since multiple values can be combined - schema.Type = "array"; - schema.Format = null; - schema.Enum = null; - schema.AllOf = null; - schema.Items = enumSchema; + concreteSchema.Type = JsonSchemaType.Array; + concreteSchema.Format = null; + concreteSchema.Enum = null; + concreteSchema.AllOf = null; + concreteSchema.Items = enumSchema; } } } diff --git a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs index eb9ad03c21..3dcf29d9c2 100644 --- a/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs +++ b/Jellyfin.Server/Filters/IgnoreEnumSchemaFilter.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.Json.Nodes; using Jellyfin.Data.Attributes; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters; public class IgnoreEnumSchemaFilter : ISchemaFilter { /// - public void Apply(OpenApiSchema schema, SchemaFilterContext context) + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) { if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false)) { @@ -25,18 +25,23 @@ public class IgnoreEnumSchemaFilter : ISchemaFilter return; } - var enumOpenApiStrings = new List(); + if (schema is not OpenApiSchema concreteSchema) + { + return; + } + + var enumOpenApiNodes = new List(); foreach (var enumName in Enum.GetNames(type)) { var member = type.GetMember(enumName)[0]; if (!member.GetCustomAttributes().Any()) { - enumOpenApiStrings.Add(new OpenApiString(enumName)); + enumOpenApiNodes.Add(JsonValue.Create(enumName)!); } } - schema.Enum = enumOpenApiStrings; + concreteSchema.Enum = enumOpenApiNodes; } } } diff --git a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs index 98a8dc0f18..90bca884b1 100644 --- a/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs +++ b/Jellyfin.Server/Filters/ParameterObsoleteFilter.cs @@ -1,7 +1,7 @@ using System; using System.Linq; using Jellyfin.Api.Attributes; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters @@ -21,11 +21,17 @@ namespace Jellyfin.Server.Filters .OfType() .Any()) { + if (operation.Parameters is null) + { + continue; + } + foreach (var parameter in operation.Parameters) { - if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal)) + if (parameter is OpenApiParameter concreteParam + && string.Equals(concreteParam.Name, parameterDescription.Name, StringComparison.Ordinal)) { - parameter.Deprecated = true; + concreteParam.Deprecated = true; break; } } diff --git a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs index 8b7268513a..435f55496a 100644 --- a/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs +++ b/Jellyfin.Server/Filters/RetryOnTemporarilyUnavailableFilter.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -8,12 +8,12 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { - operation.Responses.TryAdd( + operation.Responses?.TryAdd( "503", new OpenApiResponse { Description = "The server is currently starting or is temporarily not available.", - Headers = new Dictionary + Headers = new Dictionary { { "Retry-After", new OpenApiHeader @@ -23,7 +23,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A hint for when to retry the operation in full seconds.", Schema = new OpenApiSchema { - Type = "integer", + Type = JsonSchemaType.Integer, Format = "int32" } } @@ -36,7 +36,7 @@ internal class RetryOnTemporarilyUnavailableFilter : IOperationFilter Description = "A short plain-text reason why the server is not available.", Schema = new OpenApiSchema { - Type = "string", + Type = JsonSchemaType.String, Format = "text" } } diff --git a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs index 8f57572696..5b048be913 100644 --- a/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs +++ b/Jellyfin.Server/Filters/SecurityRequirementsOperationFilter.cs @@ -5,7 +5,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Extensions; using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Filters; @@ -66,17 +66,10 @@ public class SecurityRequirementsOperationFilter : IOperationFilter return; } - operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); - operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + operation.Responses?.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses?.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); - var scheme = new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthenticationSchemes.CustomAuthentication - }, - }; + var scheme = new OpenApiSecuritySchemeReference(AuthenticationSchemes.CustomAuthentication, null, null); // Add DefaultAuthorization scope to any endpoint that has a policy with a requirement that is a subset of DefaultAuthorization. if (!requiredScopes.Contains(DefaultAuthPolicy.AsSpan(), StringComparison.Ordinal)) diff --git a/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs new file mode 100644 index 0000000000..e4eb5be2b9 --- /dev/null +++ b/Jellyfin.Server/Filters/SecuritySchemeReferenceFixupFilter.cs @@ -0,0 +1,56 @@ +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Jellyfin.Server.Filters; + +/// +/// Document filter that fixes security scheme references after document generation. +/// +/// +/// In Microsoft.OpenApi v2, requires a resolved +/// Target to serialize correctly. References created without a host document (as in +/// operation filters) serialize as empty objects. This filter re-creates all security scheme +/// references with the document context so they resolve properly during serialization. +/// +internal class SecuritySchemeReferenceFixupFilter : IDocumentFilter +{ + /// + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + swaggerDoc.RegisterComponents(); + + if (swaggerDoc.Paths is null) + { + return; + } + + foreach (var pathItem in swaggerDoc.Paths.Values) + { + if (pathItem.Operations is null) + { + continue; + } + + foreach (var operation in pathItem.Operations.Values) + { + if (operation.Security is null) + { + continue; + } + + for (int i = 0; i < operation.Security.Count; i++) + { + var oldReq = operation.Security[i]; + var newReq = new OpenApiSecurityRequirement(); + foreach (var kvp in oldReq) + { + var fixedRef = new OpenApiSecuritySchemeReference(kvp.Key.Reference.Id!, swaggerDoc); + newReq[fixedRef] = kvp.Value; + } + + operation.Security[i] = newReq; + } + } + } + } +} From 56a469d8c3c0eed0d809d42a60d847e67ab61671 Mon Sep 17 00:00:00 2001 From: INOUE Daisuke Date: Fri, 20 Feb 2026 20:46:18 -0500 Subject: [PATCH 3/6] Translated using Weblate (Japanese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ja/ --- .../Localization/Core/ja.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index d564d54cef..bdca8ae1cd 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -43,32 +43,32 @@ "NameInstallFailed": "{0}のインストールに失敗しました", "NameSeasonNumber": "シーズン {0}", "NameSeasonUnknown": "シーズン不明", - "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。", + "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロードできます。", "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります", "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です", "NotificationOptionAudioPlayback": "オーディオの再生を開始", - "NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました", + "NotificationOptionAudioPlaybackStopped": "オーディオの再生を停止", "NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました", "NotificationOptionInstallationFailed": "インストール失敗", "NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました", "NotificationOptionPluginError": "プラグインに障害が発生しました", - "NotificationOptionPluginInstalled": "プラグインがインストールされました", - "NotificationOptionPluginUninstalled": "プラグインがアンインストールされました", + "NotificationOptionPluginInstalled": "プラグインをインストールしました", + "NotificationOptionPluginUninstalled": "プラグインをアンインストールしました", "NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました", "NotificationOptionServerRestartRequired": "サーバーを再起動してください", "NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗", "NotificationOptionUserLockedOut": "ユーザーはロックされています", - "NotificationOptionVideoPlayback": "ビデオの再生を開始しました", - "NotificationOptionVideoPlaybackStopped": "ビデオを停止しました", + "NotificationOptionVideoPlayback": "ビデオの再生を開始", + "NotificationOptionVideoPlaybackStopped": "ビデオの再生を停止", "Photos": "フォト", "Playlists": "プレイリスト", "Plugin": "プラグイン", - "PluginInstalledWithName": "{0} がインストールされました", - "PluginUninstalledWithName": "{0} がアンインストールされました", - "PluginUpdatedWithName": "{0} が更新されました", + "PluginInstalledWithName": "{0} をインストールしました", + "PluginUninstalledWithName": "{0} をアンインストールしました", + "PluginUpdatedWithName": "{0} を更新しました", "ProviderValue": "プロバイダ: {0}", "ScheduledTaskFailedWithName": "{0} が失敗しました", - "ScheduledTaskStartedWithName": "{0} が開始されました", + "ScheduledTaskStartedWithName": "{0} を開始", "ServerNameNeedsToBeRestarted": "{0} を再起動してください", "Shows": "番組", "Songs": "曲", From 01b3c6f902cddde6f46727b5f766f3439d2aa223 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:40:26 +0000 Subject: [PATCH 4/6] Update dependency Microsoft.NET.Test.Sdk to 18.3.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a520b87e2b..30e878c614 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ - + From e8232d31ab5367de015a85e4c52f18e1b883297d Mon Sep 17 00:00:00 2001 From: Pavel Miniutka Date: Thu, 26 Feb 2026 01:54:06 -0500 Subject: [PATCH 5/6] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index cb11cc0894..8ef3d9afdc 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -123,10 +123,10 @@ "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.", "TaskRefreshChannels": "Абнавіць каналы", "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры", - "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.", + "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.", "TaskRefreshTrickplayImages": "Стварыць выявы Trickplay", "TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.", - "TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты", + "TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты", "TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.", "TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.", "TaskAudioNormalization": "Нармалізацыя гуку", From bdfb6edfa3bb0ed1b4876edb3c1cf8f42b7486de Mon Sep 17 00:00:00 2001 From: dfederm Date: Thu, 26 Feb 2026 13:54:31 -0500 Subject: [PATCH 6/6] Backport pull request #16150 from jellyfin/release-10.11.z Fix nullref in Season.GetEpisodes when the season is detached from a series Original-merge: b65daeca0baf7a447185205461b4a9d758bbeb26 Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Controller/Entities/TV/Season.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index b972ebaa6b..4360253b01 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -201,12 +201,17 @@ namespace MediaBrowser.Controller.Entities.TV public List GetEpisodes(Series series, User user, IEnumerable allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes) { + if (series is null) + { + return []; + } + return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes); } public List GetEpisodes() { - return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true); + return GetEpisodes(Series, null, null, new DtoOptions(true), true); } public override List GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)