Compare commits

..

2 Commits

Author SHA1 Message Date
JPVenson
bd224b031c Update README.md
Co-authored-by: theguymadmax <theguymadmax@proton.me>
2026-02-23 11:39:50 +01:00
JPVenson
21df053fe4 Revise note on hosting web client for development
Updated recommendation for hosting the web client separately during development.
2026-02-22 23:28:34 +01:00
21 changed files with 139 additions and 204 deletions

View File

@@ -26,7 +26,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-head
retention-days: 14
@@ -65,7 +65,7 @@ jobs:
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-base
retention-days: 14
@@ -85,13 +85,13 @@ jobs:
steps:
- name: Download abi-head
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: abi-head
path: abi-head
- name: Download abi-base
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: abi-base
path: abi-base

View File

@@ -29,7 +29,7 @@ jobs:
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-head
retention-days: 14
@@ -66,7 +66,7 @@ jobs:
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 14
@@ -85,13 +85,13 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-base
path: openapi-base
@@ -119,7 +119,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head
@@ -180,7 +180,7 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head

View File

@@ -47,7 +47,7 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -75,8 +75,8 @@
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="10.1.4" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.4" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.11.0" />

View File

@@ -123,10 +123,10 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субцітры",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа выконвацца доўга.",
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных плэй-лістоў HLS. Гэта задача можа працягнуцца шмат часу.",
"TaskRefreshTrickplayImages": "Стварыць выявы Trickplay",
"TaskRefreshTrickplayImagesDescription": "Стварае перадпрагляды відэаролікаў для Trickplay у падключаных бібліятэках.",
"TaskCleanCollectionsAndPlaylists": "Ачысціць калекцыі і плэй-лісты",
"TaskCleanCollectionsAndPlaylists": "Ачысціце калекцыі і плэй-лісты",
"TaskCleanCollectionsAndPlaylistsDescription": "Выдаляе элементы з калекцый і плэй-лістоў, якія больш не існуюць.",
"TaskAudioNormalizationDescription": "Скануе файлы на прадмет нармалізацыі гуку.",
"TaskAudioNormalization": "Нармалізацыя гуку",

View File

@@ -268,7 +268,7 @@ public static class StreamingHelpers
Dictionary<string, string?> streamOptions = new Dictionary<string, string?>();
foreach (var param in queryString)
{
if (param.Key.Length > 0 && char.IsLower(param.Key[0]))
if (char.IsLower(param.Key[0]))
{
// This was probably not parsed initially and should be a StreamOptions
// or the generated URL should correctly serialize it

View File

@@ -3,7 +3,7 @@ using Jellyfin.Api.Middleware;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
namespace Jellyfin.Server.Extensions
{

View File

@@ -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,6 +26,7 @@ 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;
@@ -33,7 +34,9 @@ using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
@@ -205,7 +208,7 @@ namespace Jellyfin.Server.Extensions
{
{
"x-jellyfin-version",
new JsonNodeExtension(JsonValue.Create(version))
new OpenApiString(version)
}
}
});
@@ -259,7 +262,6 @@ namespace Jellyfin.Server.Extensions
c.OperationFilter<FileRequestFilter>();
c.OperationFilter<ParameterObsoleteFilter>();
c.DocumentFilter<AdditionalModelFilter>();
c.DocumentFilter<SecuritySchemeReferenceFixupFilter>();
})
.Replace(ServiceDescriptor.Transient<ISwaggerProvider, CachingOpenApiProvider>());
}
@@ -331,10 +333,10 @@ namespace Jellyfin.Server.Extensions
options.MapType<Dictionary<ImageType, string>>(() =>
new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
AdditionalProperties = new OpenApiSchema
{
Type = JsonSchemaType.String
Type = "string"
}
});
@@ -342,17 +344,18 @@ namespace Jellyfin.Server.Extensions
options.MapType<Dictionary<string, string?>>(() =>
new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
AdditionalProperties = new OpenApiSchema
{
Type = JsonSchemaType.String | JsonSchemaType.Null
Type = "string",
Nullable = true
}
});
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
options.MapType<Version>(() => new OpenApiSchema
{
Type = JsonSchemaType.String
Type = "string"
});
}
}

View File

@@ -3,17 +3,18 @@ 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;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
@@ -24,7 +25,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;
/// <summary>
@@ -47,8 +48,8 @@ namespace Jellyfin.Server.Filters
&& t != typeof(WebSocketMessageInfo))
.ToList();
var inboundWebSocketSchemas = new List<IOpenApiSchema>();
var inboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
var inboundWebSocketSchemas = new List<OpenApiSchema>();
var inboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IInboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
@@ -59,16 +60,18 @@ namespace Jellyfin.Server.Filters
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
inboundWebSocketSchemas.Add(schema);
if (schema is OpenApiSchemaReference schemaRef)
{
inboundWebSocketDiscriminators[messageType.ToString()!] = schemaRef;
}
inboundWebSocketDiscriminators[messageType.ToString()!] = schema.Reference.ReferenceV3;
}
var inboundWebSocketMessageSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
Description = "Represents the list of possible inbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(InboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = inboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
@@ -79,8 +82,8 @@ namespace Jellyfin.Server.Filters
context.SchemaRepository.AddDefinition(nameof(InboundWebSocketMessage), inboundWebSocketMessageSchema);
var outboundWebSocketSchemas = new List<IOpenApiSchema>();
var outboundWebSocketDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
var outboundWebSocketSchemas = new List<OpenApiSchema>();
var outboundWebSocketDiscriminators = new Dictionary<string, string>();
foreach (var type in webSocketTypes.Where(t => typeof(IOutboundWebSocketMessage).IsAssignableFrom(t)))
{
var messageType = (SessionMessageType?)type.GetProperty(nameof(WebSocketMessage.MessageType))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
@@ -91,55 +94,58 @@ namespace Jellyfin.Server.Filters
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
outboundWebSocketSchemas.Add(schema);
if (schema is OpenApiSchemaReference schemaRef)
{
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schemaRef);
}
outboundWebSocketDiscriminators.Add(messageType.ToString()!, schema.Reference.ReferenceV3);
}
// Add custom "SyncPlayGroupUpdateMessage" schema because Swashbuckle cannot generate it for us
var syncPlayGroupUpdateMessageSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
Description = "Untyped sync play command.",
Properties = new Dictionary<string, IOpenApiSchema>
Properties = new Dictionary<string, OpenApiSchema>
{
{
"Data", new OpenApiSchema
{
AllOf = new List<IOpenApiSchema>
{
new OpenApiSchemaReference(nameof(GroupUpdate<object>), null, null)
},
AllOf =
[
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(GroupUpdate<object>) } }
],
Description = "Group update data",
Nullable = false,
}
},
{ "MessageId", new OpenApiSchema { Type = JsonSchemaType.String, Format = "uuid", Description = "Gets or sets the message id." } },
{ "MessageId", new OpenApiSchema { Type = "string", Format = "uuid", Description = "Gets or sets the message id." } },
{
"MessageType", new OpenApiSchema
{
Enum = Enum.GetValues<SessionMessageType>().Select(type => (JsonNode)JsonValue.Create(type.ToString())!).ToList(),
AllOf = new List<IOpenApiSchema>
{
new OpenApiSchemaReference(nameof(SessionMessageType), null, null)
},
Enum = Enum.GetValues<SessionMessageType>().Select(type => new OpenApiString(type.ToString())).ToList<IOpenApiAny>(),
AllOf =
[
new OpenApiSchema { Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = nameof(SessionMessageType) } }
],
Description = "The different kinds of messages that are used in the WebSocket api.",
Default = JsonValue.Create(nameof(SessionMessageType.SyncPlayGroupUpdate)),
Default = new OpenApiString(nameof(SessionMessageType.SyncPlayGroupUpdate)),
ReadOnly = true
}
},
},
AdditionalPropertiesAllowed = false,
Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "SyncPlayGroupUpdateMessage" }
};
context.SchemaRepository.AddDefinition("SyncPlayGroupUpdateMessage", syncPlayGroupUpdateMessageSchema);
var syncPlayRef = new OpenApiSchemaReference("SyncPlayGroupUpdateMessage", null, null);
outboundWebSocketSchemas.Add(syncPlayRef);
outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayRef;
outboundWebSocketSchemas.Add(syncPlayGroupUpdateMessageSchema);
outboundWebSocketDiscriminators[nameof(SessionMessageType.SyncPlayGroupUpdate)] = syncPlayGroupUpdateMessageSchema.Reference.ReferenceV3;
var outboundWebSocketMessageSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
Description = "Represents the list of possible outbound websocket types",
Reference = new OpenApiReference
{
Id = nameof(OutboundWebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = outboundWebSocketSchemas,
Discriminator = new OpenApiDiscriminator
{
@@ -153,12 +159,17 @@ namespace Jellyfin.Server.Filters
nameof(WebSocketMessage),
new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
Description = "Represents the possible websocket types",
OneOf = new List<IOpenApiSchema>
Reference = new OpenApiReference
{
new OpenApiSchemaReference(nameof(InboundWebSocketMessage), null, null),
new OpenApiSchemaReference(nameof(OutboundWebSocketMessage), null, null)
Id = nameof(WebSocketMessage),
Type = ReferenceType.Schema
},
OneOf = new[]
{
inboundWebSocketMessageSchema,
outboundWebSocketMessageSchema
}
});
@@ -169,8 +180,8 @@ namespace Jellyfin.Server.Filters
&& t.BaseType.GetGenericTypeDefinition() == typeof(GroupUpdate<>))
.ToList();
var groupUpdateSchemas = new List<IOpenApiSchema>();
var groupUpdateDiscriminators = new Dictionary<string, OpenApiSchemaReference>();
var groupUpdateSchemas = new List<OpenApiSchema>();
var groupUpdateDiscriminators = new Dictionary<string, string>();
foreach (var type in groupUpdateTypes)
{
var groupUpdateType = (GroupUpdateType?)type.GetProperty(nameof(GroupUpdate<object>.Type))?.GetCustomAttribute<DefaultValueAttribute>()?.Value;
@@ -181,16 +192,18 @@ namespace Jellyfin.Server.Filters
var schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository);
groupUpdateSchemas.Add(schema);
if (schema is OpenApiSchemaReference schemaRef)
{
groupUpdateDiscriminators[groupUpdateType.ToString()!] = schemaRef;
}
groupUpdateDiscriminators[groupUpdateType.ToString()!] = schema.Reference.ReferenceV3;
}
var groupUpdateSchema = new OpenApiSchema
{
Type = JsonSchemaType.Object,
Type = "object",
Description = "Represents the list of possible group update types",
Reference = new OpenApiReference
{
Id = nameof(GroupUpdate<object>),
Type = ReferenceType.Schema
},
OneOf = groupUpdateSchemas,
Discriminator = new OpenApiDiscriminator
{

View File

@@ -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;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -48,7 +48,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider
}
/// <inheritdoc />
public OpenApiDocument GetSwagger(string documentName, string host, string basePath)
public OpenApiDocument GetSwagger(string documentName, string? host = null, string? basePath = null)
{
if (_memoryCache.TryGetValue(CacheKey, out OpenApiDocument? openApiDocument) && openApiDocument is not null)
{

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Jellyfin.Api.Attributes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
@@ -28,11 +28,10 @@ namespace Jellyfin.Server.Filters
{
Schema = new OpenApiSchema
{
Type = JsonSchemaType.String,
Type = "string",
Format = "binary"
}
};
body.Content ??= new System.Collections.Generic.Dictionary<string, OpenApiMediaType>();
foreach (var contentType in contentTypes)
{
body.Content.Add(contentType, mediaType);

View File

@@ -1,7 +1,7 @@
using System;
using System.Linq;
using Jellyfin.Api.Attributes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
@@ -14,7 +14,7 @@ namespace Jellyfin.Server.Filters
{
Schema = new OpenApiSchema
{
Type = JsonSchemaType.String,
Type = "string",
Format = "binary"
}
};
@@ -22,11 +22,6 @@ namespace Jellyfin.Server.Filters
/// <inheritdoc />
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)
@@ -36,7 +31,7 @@ namespace Jellyfin.Server.Filters
.FirstOrDefault(o => o.Key.Equals(SuccessCode, StringComparison.Ordinal));
// Operation doesn't have a response.
if (response.Value?.Content is null)
if (response.Value is null)
{
continue;
}

View File

@@ -1,5 +1,5 @@
using System;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters;
public class FlagsEnumSchemaFilter : ISchemaFilter
{
/// <inheritdoc />
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type.IsEnum ? context.Type : Nullable.GetUnderlyingType(context.Type);
if (type is null || !type.IsEnum)
@@ -29,16 +29,11 @@ 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"
concreteSchema.Type = JsonSchemaType.String;
concreteSchema.Format = null;
schema.Type = "string";
schema.Format = null;
}
else
{
@@ -48,11 +43,11 @@ public class FlagsEnumSchemaFilter : ISchemaFilter
// Flags enums should be represented as arrays referencing the enum schema
// since multiple values can be combined
concreteSchema.Type = JsonSchemaType.Array;
concreteSchema.Format = null;
concreteSchema.Enum = null;
concreteSchema.AllOf = null;
concreteSchema.Items = enumSchema;
schema.Type = "array";
schema.Format = null;
schema.Enum = null;
schema.AllOf = null;
schema.Items = enumSchema;
}
}
}

View File

@@ -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;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Filters;
public class IgnoreEnumSchemaFilter : ISchemaFilter
{
/// <inheritdoc />
public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type.IsEnum || (Nullable.GetUnderlyingType(context.Type)?.IsEnum ?? false))
{
@@ -25,23 +25,18 @@ public class IgnoreEnumSchemaFilter : ISchemaFilter
return;
}
if (schema is not OpenApiSchema concreteSchema)
{
return;
}
var enumOpenApiNodes = new List<JsonNode>();
var enumOpenApiStrings = new List<IOpenApiAny>();
foreach (var enumName in Enum.GetNames(type))
{
var member = type.GetMember(enumName)[0];
if (!member.GetCustomAttributes<OpenApiIgnoreEnumAttribute>().Any())
{
enumOpenApiNodes.Add(JsonValue.Create(enumName)!);
enumOpenApiStrings.Add(new OpenApiString(enumName));
}
}
concreteSchema.Enum = enumOpenApiNodes;
schema.Enum = enumOpenApiStrings;
}
}
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Linq;
using Jellyfin.Api.Attributes;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters
@@ -21,17 +21,11 @@ namespace Jellyfin.Server.Filters
.OfType<ParameterObsoleteAttribute>()
.Any())
{
if (operation.Parameters is null)
{
continue;
}
foreach (var parameter in operation.Parameters)
{
if (parameter is OpenApiParameter concreteParam
&& string.Equals(concreteParam.Name, parameterDescription.Name, StringComparison.Ordinal))
if (parameter.Name.Equals(parameterDescription.Name, StringComparison.Ordinal))
{
concreteParam.Deprecated = true;
parameter.Deprecated = true;
break;
}
}

View File

@@ -1,5 +1,5 @@
using System.Collections.Generic;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
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<string, IOpenApiHeader>
Headers = new Dictionary<string, OpenApiHeader>
{
{
"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 = JsonSchemaType.Integer,
Type = "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 = JsonSchemaType.String,
Type = "string",
Format = "text"
}
}

View File

@@ -5,7 +5,7 @@ using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Extensions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
@@ -66,10 +66,17 @@ 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 OpenApiSecuritySchemeReference(AuthenticationSchemes.CustomAuthentication, null, null);
var scheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = AuthenticationSchemes.CustomAuthentication
},
};
// 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))

View File

@@ -1,56 +0,0 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Jellyfin.Server.Filters;
/// <summary>
/// Document filter that fixes security scheme references after document generation.
/// </summary>
/// <remarks>
/// In Microsoft.OpenApi v2, <see cref="OpenApiSecuritySchemeReference"/> requires a resolved
/// <c>Target</c> 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.
/// </remarks>
internal class SecuritySchemeReferenceFixupFilter : IDocumentFilter
{
/// <inheritdoc />
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;
}
}
}
}
}

View File

@@ -201,17 +201,12 @@ namespace MediaBrowser.Controller.Entities.TV
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
if (series is null)
{
return [];
}
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes()
{
return GetEpisodes(Series, null, null, new DtoOptions(true), true);
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)

View File

@@ -895,7 +895,7 @@ public class StreamInfo
if (SubProtocol == MediaStreamProtocol.hls)
{
sb.Append("/master.m3u8");
sb.Append("/master.m3u8?");
}
else
{
@@ -906,9 +906,9 @@ public class StreamInfo
sb.Append('.');
sb.Append(Container);
}
}
var queryStart = sb.Length;
sb.Append('?');
}
if (!string.IsNullOrEmpty(DeviceProfileId))
{
@@ -1133,12 +1133,6 @@ 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();
}

View File

@@ -94,13 +94,12 @@ git clone https://github.com/jellyfin/jellyfin.git
The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly.
Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
Note that it is recommended for development to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step.
There are three options to get the files for the web client.
There are two options to get the files for the web client.
1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=27&_a=summary&repositoryFilter=6&view=branches) of the pipelines page.
2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web)
2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web`
### Running The Server

View File

@@ -216,7 +216,8 @@ public class StreamInfoTests
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
// New version will return and & after the ? due to optional parameters.
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}
@@ -233,7 +234,8 @@ public class StreamInfoTests
FillAllProperties(streamInfo);
string legacyUrl = streamInfo.ToUrl_Original(BaseUrl, "123");
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null);
// New version will return and & after the ? due to optional parameters.
string newUrl = streamInfo.ToUrl(BaseUrl, "123", null).Replace("?&", "?", StringComparison.OrdinalIgnoreCase);
Assert.Equal(legacyUrl, newUrl, ignoreCase: true);
}