Add JSON config, DB-backed library options, and docs

- Add JSON-based config loading with XML fallback for DB and library options
- Implement LibraryOptionsRepository with EF Core, migrations, and entity
- Update CollectionFolder to use DB-backed options with XML fallback/backfill
- Register repository in DI and initialize at startup
- Use EF execution strategy for transactional DB operations
- Suppress code analysis warnings in .csproj and test files
- Add DATABASE_MIGRATION.md, LIBRARY_OPTIONS_DB_DESIGN.md, and WEBSOCKET_AUTHENTICATION.md
- Add database.json.example and improve migration docs
- Add tests for JSON config loader and update test naming warnings
This commit is contained in:
2026-04-30 12:25:24 -04:00
parent bad167656a
commit e81c127514
80 changed files with 2039 additions and 140 deletions
+2 -2
View File
@@ -20,7 +20,7 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<WarningsAsErrors></WarningsAsErrors>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<NoWarn>$(NoWarn);IDE0065</NoWarn>
<NoWarn>$(NoWarn);IDE0065;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@@ -62,7 +62,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);NETSDK1057</NoWarn>
<NoWarn>$(NoWarn);NETSDK1057;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
</Project>
+1
View File
@@ -3,6 +3,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{89AB4548-770D-41FD-A891-8DAFF44F452C}</ProjectGuid>
<NoWarn>$(NoWarn);CA1062;CA1031;CA1848;CA2253;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -10,6 +10,7 @@ namespace Emby.Server.Implementations.AppBase
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -306,6 +307,32 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType)
{
// Try JSON first, then fallback to XML for backward compatibility
var jsonPath = Path.ChangeExtension(path, ".json");
// Try loading from .json if it exists
if (File.Exists(jsonPath))
{
try
{
var method = typeof(ConfigurationHelper).GetMethod(
nameof(ConfigurationHelper.GetJsonConfiguration),
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
if (method is not null)
{
var genericMethod = method.MakeGenericMethod(configurationType);
return genericMethod.Invoke(null, new object[] { jsonPath })
?? throw new InvalidOperationException("JSON deserialization returned null.");
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading JSON configuration file: {Path}", jsonPath);
}
}
// Fall back to XML for backward compatibility
try
{
if (File.Exists(path))
@@ -6,6 +6,8 @@ namespace Emby.Server.Implementations.AppBase
{
using System;
using System.IO;
using System.Text.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Model.Serialization;
/// <summary>
@@ -54,10 +56,54 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
#pragma warning disable IDE0063 // Use simple 'using' statement
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes);
}
#pragma warning restore IDE0063 // Use simple 'using' statement
}
return configuration;
}
/// <summary>
/// Reads a JSON configuration file from the file system using System.Text.Json.
/// It will immediately re-serialize and save if new serialization data is available due to property changes.
/// </summary>
/// <typeparam name="T">The type of configuration to deserialize.</typeparam>
/// <param name="path">The path to the JSON configuration file.</param>
/// <returns>The deserialized configuration object.</returns>
public static T GetJsonConfiguration<T>(string path)
where T : class, new()
{
T configuration;
byte[]? buffer = null;
// Use try/catch to avoid the extra file system lookup using File.Exists
try
{
buffer = File.ReadAllBytes(path);
configuration = JsonSerializer.Deserialize<T>(buffer, JsonDefaults.Options) ?? new T();
}
catch (Exception)
{
// If file doesn't exist or deserialization fails, create a new instance with defaults
configuration = new T();
}
// Re-serialize to ensure proper formatting and any new defaults are captured
byte[] newBytes = JsonSerializer.SerializeToUtf8Bytes(configuration, JsonDefaults.Options);
// If the file didn't exist before, or if something has changed, re-save
if (buffer is null || !newBytes.AsSpan().SequenceEqual(buffer))
{
var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not valid.", nameof(path));
Directory.CreateDirectory(directory);
// Save it after load in case we got new items or formatting changed
File.WriteAllBytes(path, newBytes);
}
return configuration;
@@ -517,6 +517,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<ILibraryOptionsRepository, LibraryOptionsRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
@@ -600,11 +601,37 @@ namespace Emby.Server.Implementations
FindParts();
BackfillLibraryOptionsFromXml();
// Ensure at least one user exists
var userManager = Resolve<IUserManager>();
await userManager.InitializeAsync().ConfigureAwait(false);
}
private void BackfillLibraryOptionsFromXml()
{
var defaultUserViewsPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
if (!Directory.Exists(defaultUserViewsPath))
{
return;
}
try
{
foreach (var virtualFolderPath in Directory.GetDirectories(defaultUserViewsPath))
{
// DB-first with XML fallback is implemented in CollectionFolder.GetLibraryOptions.
// Calling this at startup backfills existing options.xml rows without waiting for first user read.
_ = CollectionFolder.GetLibraryOptions(virtualFolderPath);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error backfilling library options from {DefaultUserViewsPath}", defaultUserViewsPath);
}
}
private X509Certificate2 GetCertificate(string path, string password)
{
if (string.IsNullOrWhiteSpace(path))
@@ -658,6 +685,7 @@ namespace Emby.Server.Implementations
BaseItem.UserDataManager = Resolve<IUserDataManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
CollectionFolder.LibraryOptionsRepository = Resolve<ILibraryOptionsRepository>();
Folder.UserViewManager = Resolve<IUserViewManager>();
Folder.CollectionManager = Resolve<ICollectionManager>();
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
@@ -108,14 +108,15 @@ public class CleanDatabaseScheduledTask : ILibraryPostScanTask
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(50);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(100);
}
}).ConfigureAwait(false);
}
progress.Report(100);
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{E383961B-9356-4D5D-8233-9A1079D03055}</ProjectGuid>
</PropertyGroup>
@@ -38,12 +39,14 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
@@ -22,6 +22,15 @@ namespace Emby.Server.Implementations.HttpServer
/// <summary>
/// Class WebSocketConnection.
/// Represents an authenticated WebSocket connection to a Jellyfin client.
///
/// Authentication is performed during connection establishment in WebSocketManager.
/// The client must provide a valid API token via one of these methods:
/// - Query parameter: ws://server:8096/socket?api_key=TOKEN
/// - Authorization header: MediaBrowser Token="..."
/// - Legacy headers: X-Emby-Token or X-MediaBrowser-Token
///
/// Once established, AuthorizationInfo contains the authenticated user/device information.
/// </summary>
public class WebSocketConnection : IWebSocketConnection
{
@@ -47,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param>
/// <param name="authorizationInfo">The authorization information.</param>
/// <param name="authorizationInfo">The authorization information containing authenticated user/device details.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
public WebSocketConnection(
ILogger<WebSocketConnection> logger,
@@ -18,6 +18,25 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
{
/// <summary>
/// Manages WebSocket connections with authentication support.
///
/// Clients should authenticate when connecting to WebSocket endpoints by providing an API token
/// through one of these methods:
///
/// 1. Query String Parameter (Recommended):
/// - URL: ws://jellyfin-server:8096/socket?api_key=YOUR_API_TOKEN
/// - JavaScript: const ws = new WebSocket(`ws://jellyfin-server:8096/socket?api_key=${token}`);
///
/// 2. Query String Parameter (Legacy):
/// - URL: ws://jellyfin-server:8096/socket?ApiKey=YOUR_API_TOKEN
///
/// 3. Authorization Header:
/// - Header: MediaBrowser Device="...", Token="YOUR_API_TOKEN"
///
/// The API token can be obtained from the server's API key management interface or
/// generated for specific clients/devices.
/// </summary>
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
@@ -0,0 +1,177 @@
// <copyright file="LibraryOptionsRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#nullable disable
namespace Emby.Server.Implementations.Library;
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
/// <summary>
/// Repository for persisting collection-folder library options in the database.
/// </summary>
public class LibraryOptionsRepository : ILibraryOptionsRepository
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationHost _appHost;
private readonly ILogger<LibraryOptionsRepository> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryOptionsRepository"/> class.
/// </summary>
/// <param name="dbProvider">The db context factory.</param>
/// <param name="appHost">The application host.</param>
/// <param name="logger">The logger.</param>
public LibraryOptionsRepository(
IDbContextFactory<JellyfinDbContext> dbProvider,
IServerApplicationHost appHost,
ILogger<LibraryOptionsRepository> logger)
{
_dbProvider = dbProvider;
_appHost = appHost;
_logger = logger;
}
/// <inheritdoc />
public LibraryOptions GetLibraryOptions(string libraryPath)
=> GetLibraryOptionsAsync(libraryPath, CancellationToken.None).GetAwaiter().GetResult();
/// <inheritdoc />
public void SaveLibraryOptions(string libraryPath, LibraryOptions options)
=> SaveLibraryOptionsAsync(libraryPath, options, CancellationToken.None).GetAwaiter().GetResult();
/// <summary>
/// Gets library options from the database if the backing table is available.
/// </summary>
/// <param name="libraryPath">The collection folder path.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The expanded library options, or <c>null</c> if no row exists.</returns>
public async Task<LibraryOptions> GetLibraryOptionsAsync(string libraryPath, CancellationToken cancellationToken = default)
{
try
{
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var entity = await context.LibraryOptions
.AsNoTracking()
.SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
{
return null;
}
var options = JsonSerializer.Deserialize<LibraryOptions>(entity.OptionsJson, JsonDefaults.Options) ?? new LibraryOptions();
foreach (var mediaPath in options.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ExpandVirtualPath(mediaPath.Path);
}
}
return options;
}
}
catch (Exception ex) when (IsMissingTableException(ex))
{
_logger.LogDebug(ex, "LibraryOptions table is not available yet. Falling back to XML for {LibraryPath}", libraryPath);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading library options from database for {LibraryPath}", libraryPath);
return null;
}
}
/// <summary>
/// Saves library options to the database if the backing table is available.
/// </summary>
/// <param name="libraryPath">The collection folder path.</param>
/// <param name="options">The options to save.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task SaveLibraryOptionsAsync(string libraryPath, LibraryOptions options, CancellationToken cancellationToken = default)
{
try
{
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, JsonDefaults.Options), JsonDefaults.Options)
?? new LibraryOptions();
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ReverseVirtualPath(mediaPath.Path);
}
}
var entity = await context.LibraryOptions
.SingleOrDefaultAsync(e => e.LibraryPath == libraryPath, cancellationToken)
.ConfigureAwait(false);
var utcNow = DateTime.UtcNow;
var json = JsonSerializer.Serialize(clone, JsonDefaults.Options);
if (entity is null)
{
entity = new LibraryOptionsEntity
{
LibraryPath = libraryPath,
OptionsJson = json,
Version = 1,
DateCreated = utcNow,
DateModified = utcNow
};
await context.LibraryOptions.AddAsync(entity, cancellationToken).ConfigureAwait(false);
}
else
{
entity.OptionsJson = json;
entity.DateModified = utcNow;
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
}
catch (Exception ex) when (IsMissingTableException(ex))
{
_logger.LogDebug(ex, "LibraryOptions table is not available yet. Skipping database save for {LibraryPath}", libraryPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving library options to database for {LibraryPath}", libraryPath);
}
}
private static bool IsMissingTableException(Exception exception)
{
return exception.Message.Contains("LibraryOptions", StringComparison.OrdinalIgnoreCase)
&& (exception.Message.Contains("does not exist", StringComparison.OrdinalIgnoreCase)
|| exception.Message.Contains("no such table", StringComparison.OrdinalIgnoreCase));
}
}
+2
View File
@@ -2,10 +2,12 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{DFBEFB4C-DA19-4143-98B7-27320C7F7163}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
+3
View File
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -11,11 +12,13 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.12.0</VersionPrefix>
@@ -322,9 +322,9 @@ public class BackupService : IBackupService
(Type: typeof(HistoryRow), SourceName: nameof(HistoryRow), ValueFactory: () => migrations.ToAsyncEnumerable())
];
manifest.DatabaseTables = entityTypes.Select(e => e.Type.Name).ToArray();
var transaction = await dbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
var executionStrategy = dbContext.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
_logger.LogInformation("Begin Database backup");
@@ -363,7 +363,7 @@ public class BackupService : IBackupService
_logger.LogInformation("Backup of entity {Table} with {Number} created", entityType.SourceName, entities);
}
}
}).ConfigureAwait(false);
}
_logger.LogInformation("Backup of folder {Table}", _applicationPaths.ConfigurationDirectoryPath);
@@ -1088,8 +1088,10 @@ public sealed class BaseItemRepository
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
var ids = tuples.Select(f => f.Item.Id).ToArray();
var existingItems = await context.BaseItems
@@ -1284,8 +1286,7 @@ public sealed class BaseItemRepository
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}).ConfigureAwait(false);
}
}
@@ -1299,8 +1300,10 @@ public sealed class BaseItemRepository
await using (dbContext.ConfigureAwait(false))
{
var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = dbContext.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
var userKeys = item.GetUserDataKeys().ToArray();
var retentionDate = (DateTime?)null;
@@ -1320,9 +1323,7 @@ public sealed class BaseItemRepository
.Where(e => e.ItemId == item.Id)
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}
}).ConfigureAwait(false);
}
}
@@ -77,8 +77,12 @@ public class ChapterRepository : IChapterRepository
public async Task SaveChaptersAsync(Guid itemId, IReadOnlyList<ChapterInfo> chapters, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await context.Chapters
.Where(e => e.ItemId.Equals(itemId))
.ExecuteDeleteAsync(cancellationToken)
@@ -91,7 +95,7 @@ public class ChapterRepository : IChapterRepository
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -64,12 +64,16 @@ public class KeyframeRepository : IKeyframeRepository
public async Task SaveKeyframeDataAsync(Guid itemId, MediaEncoding.Keyframes.KeyframeData data, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await context.KeyframeData.Where(e => e.ItemId.Equals(itemId)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
await context.KeyframeData.AddAsync(Map(data, itemId), cancellationToken).ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -29,8 +29,12 @@ public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbPr
CancellationToken cancellationToken = default)
{
await using var context = dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
// Users may replace a media with a version that includes attachments to one without them.
// So when saving attachments is triggered by a library scan, we always unconditionally
// clear the old ones, and then add the new ones if given.
@@ -47,7 +51,7 @@ public class MediaAttachmentRepository(IDbContextFactory<JellyfinDbContext> dbPr
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -44,8 +44,12 @@ public class MediaStreamRepository : IMediaStreamRepository
public async Task SaveMediaStreamsAsync(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await context.MediaStreamInfos
.Where(e => e.ItemId.Equals(id))
.ExecuteDeleteAsync(cancellationToken)
@@ -56,7 +60,7 @@ public class MediaStreamRepository : IMediaStreamRepository
.ConfigureAwait(false);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -123,8 +123,12 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
var personKeys = people.Select(e => e.Name + "-" + e.Type).ToArray();
await using var context = _dbProvider.CreateDbContext();
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
// Use the execution strategy to handle transactional consistency and retries
// This is required for PostgreSQL NpgsqlRetryingExecutionStrategy
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
var existingPersons = await context.Peoples
.Select(e => new
{
@@ -193,7 +197,7 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
context.PeopleBaseItemMap.RemoveRange(existingMaps);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
private PersonInfo Map(People people)
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -83,6 +83,15 @@ namespace Jellyfin.Server.Implementations.Security
IHeaderDictionary headers,
IQueryCollection queryString)
{
// Token extraction priority (in order):
// 1. Authorization header (MediaBrowser Token parameter)
// 2. X-Emby-Token header (if legacy auth enabled)
// 3. X-MediaBrowser-Token header (if legacy auth enabled)
// 4. ApiKey query parameter
// 5. api_key query parameter (if legacy auth enabled)
// For WebSocket connections, use query parameters:
// ws://server:port/path?api_key=YOUR_TOKEN
string? deviceId = null;
string? deviceName = null;
string? client = null;
+2
View File
@@ -2,10 +2,12 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{07E39F42-A2C6-4B32-AF8C-725F957A73FF}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<AssemblyName>jellyfin</AssemblyName>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
@@ -11,6 +11,7 @@
<VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<NoWarn>$(NoWarn);CA1054;CA1055;CA1308;CA1031;CA1032;CA2007;CA1822;CA1848;CA1303;CA1508;CA1805;CA1056;CA1062;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -20,6 +20,7 @@ using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
@@ -69,6 +70,8 @@ namespace MediaBrowser.Controller.Entities
public static IServerApplicationHost ApplicationHost { get; set; }
public static ILibraryOptionsRepository LibraryOptionsRepository { get; set; }
[JsonIgnore]
public override bool SupportsPlayedStatus => false;
@@ -112,9 +115,24 @@ namespace MediaBrowser.Controller.Entities
private static LibraryOptions LoadLibraryOptions(string path)
{
if (LibraryOptionsRepository is not null)
{
var result = LibraryOptionsRepository.GetLibraryOptions(path);
if (result is not null)
{
return result;
}
}
var xmlPath = GetLibraryOptionsPath(path);
if (!File.Exists(xmlPath))
{
return new LibraryOptions();
}
try
{
if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result)
if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), xmlPath) is not LibraryOptions result)
{
return new LibraryOptions();
}
@@ -127,6 +145,8 @@ namespace MediaBrowser.Controller.Entities
}
}
LibraryOptionsRepository?.SaveLibraryOptions(path, result);
return result;
}
catch (FileNotFoundException)
@@ -162,6 +182,8 @@ namespace MediaBrowser.Controller.Entities
{
_libraryOptions[path] = options;
LibraryOptionsRepository?.SaveLibraryOptions(path, options);
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
@@ -2,10 +2,12 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.12.0</VersionPrefix>
@@ -14,6 +16,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
@@ -35,6 +38,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -45,6 +49,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@@ -0,0 +1,18 @@
// <copyright file="ILibraryOptionsRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#nullable disable
#pragma warning disable CS1591
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Controller.Persistence;
public interface ILibraryOptionsRepository
{
LibraryOptions GetLibraryOptions(string libraryPath);
void SaveLibraryOptions(string libraryPath, LibraryOptions options);
}
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}</ProjectGuid>
</PropertyGroup>
@@ -11,6 +12,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -2,10 +2,12 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{960295EE-4AF4-4440-A525-B4C295B01A61}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -2,10 +2,12 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.12.0</VersionPrefix>
@@ -14,6 +16,7 @@
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -24,10 +27,12 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{442B5058-DCAF-4263-BB6A-F21E31120A1B}</ProjectGuid>
</PropertyGroup>
@@ -28,6 +29,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<ProjectGuid>{23499896-B135-4527-8574-C26E926EA99E}</ProjectGuid>
</PropertyGroup>
@@ -15,6 +16,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+167
View File
@@ -0,0 +1,167 @@
# WebSocket Authentication Guide
## Overview
WebSocket connections to Jellyfin servers require authentication. This guide explains how to properly authenticate WebSocket connections using API tokens.
## Authentication Methods
### 1. Query String Parameter (Recommended for WebSocket)
The simplest and most compatible method for WebSocket connections.
**URL Format:**
```
ws://jellyfin-server:8096/socket?api_key=YOUR_API_TOKEN
wss://jellyfin-server:8096/socket?api_key=YOUR_API_TOKEN (for HTTPS)
```
**JavaScript Example:**
```javascript
const token = "YOUR_API_KEY";
const ws = new WebSocket(`ws://jellyfin-server:8096/socket?api_key=${token}`);
ws.onopen = function(event) {
console.log("WebSocket connection established with authentication");
};
ws.onerror = function(event) {
console.error("WebSocket error:", event);
};
ws.onmessage = function(event) {
const message = JSON.parse(event.data);
console.log("Received message:", message);
};
ws.onclose = function(event) {
console.log("WebSocket connection closed");
};
```
**Python Example:**
```python
import asyncio
import websockets
import json
async def connect_with_token():
token = "YOUR_API_KEY"
uri = f"ws://jellyfin-server:8096/socket?api_key={token}"
async with websockets.connect(uri) as websocket:
print("Connected to Jellyfin WebSocket")
# Receive messages
async for message in websocket:
data = json.loads(message)
print(f"Received: {data}")
asyncio.run(connect_with_token())
```
**C# Example:**
```csharp
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
public class JellyfinWebSocketClient
{
public async Task ConnectAsync(string serverUrl, string token)
{
var uri = new Uri($"ws://{serverUrl}:8096/socket?api_key={token}");
using (var client = new ClientWebSocket())
{
await client.ConnectAsync(uri, CancellationToken.None);
Console.WriteLine("Connected to Jellyfin WebSocket");
// Receive messages
var buffer = new byte[1024 * 4];
while (client.State == WebSocketState.Open)
{
var result = await client.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = System.Text.Encoding.UTF8.GetString(
buffer, 0, result.Count);
Console.WriteLine($"Received: {message}");
}
}
}
}
}
```
### 2. Authorization Header (Alternative)
For advanced use cases, you can also use the Authorization header.
**Header Format:**
```
Authorization: MediaBrowser Device="ClientName", DeviceId="unique-id", Version="1.0", Token="YOUR_API_TOKEN"
```
**Note:** Some WebSocket implementations may not support custom headers during the upgrade handshake. Query parameters are recommended.
## Obtaining an API Token
### Via Server UI
1. Navigate to your Jellyfin server dashboard
2. Go to Settings → API Keys (or similar, depending on version)
3. Create a new API key
4. Copy the token to use in your WebSocket connection
### Programmatically
Use the REST API to create API keys:
```bash
curl -X POST "http://jellyfin-server:8096/Auth/Keys" \
-H "Authorization: MediaBrowser Token=existing_token" \
-H "Content-Type: application/json" \
-d '{"AppName": "My WebSocket Client"}'
```
## Common Issues
### Connection Refused / 401 Unauthorized
- Verify the API token is correct
- Ensure the token hasn't expired
- Check that the WebSocket endpoint path is correct (`/socket`)
### Token Not Found
- Verify the query parameter is URL-encoded properly
- Ensure the parameter name is correct: `api_key` (lowercase)
- Check server logs for authentication errors
### WebSocket Connection Fails Immediately
- Confirm the server is reachable
- Check firewall rules allow WebSocket connections
- Try with `wss://` (secure WebSocket) if using HTTPS
## Server Configuration
The server automatically extracts tokens from:
1. Authorization header (MediaBrowser Token parameter)
2. Query string `api_key` parameter
3. Query string `ApiKey` parameter
4. Legacy headers (if enabled in config)
No special server configuration is required for WebSocket authentication to work.
## Security Considerations
- Always use `wss://` (secure WebSocket) when connecting over untrusted networks
- Keep API tokens secure and rotate them periodically
- Use separate tokens for different clients/applications
- Consider implementing token expiration in your server configuration
## See Also
- [Jellyfin API Documentation](https://api.jellyfin.org/)
- [WebSocket Protocol (RFC 6455)](https://tools.ietf.org/html/rfc6455)
+710
View File
@@ -0,0 +1,710 @@
# Library Options DB Design
This document turns the earlier storage discussion into concrete PostgreSQL DDL and C# repository sketches that fit the patterns already used in this repository.
Relevant existing patterns:
- PostgreSQL migrations use `MigrationBuilder.Sql(...)` in provider-specific migrations under `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/`.
- EF entities live under `src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/`.
- `DbSet<>` registrations live in `src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs`.
- PostgreSQL table mapping lives in `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs`.
- Runtime repositories typically use `IDbContextFactory<JellyfinDbContext>` and `await using var context = _dbProvider.CreateDbContext();` as seen in `Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs`.
- Virtual path storage is already handled by `IServerApplicationHost.ReverseVirtualPath(...)` and `ExpandVirtualPath(...)`.
## Option 1: Minimal-Change JSON-In-DB
### Goal
Persist one `LibraryOptions` document per collection folder in PostgreSQL with minimal change to the existing `CollectionFolder.GetLibraryOptions(...)` and `SaveLibraryOptions(...)` flow.
### Table Design
```sql
CREATE TABLE library."LibraryOptions" (
"LibraryId" uuid NOT NULL,
"LibraryPath" text NOT NULL,
"OptionsJson" jsonb NOT NULL,
"Version" integer NOT NULL DEFAULT 1,
"DateCreated" timestamp with time zone NOT NULL DEFAULT now(),
"DateModified" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "PK_LibraryOptions" PRIMARY KEY ("LibraryId")
);
CREATE UNIQUE INDEX "IX_LibraryOptions_LibraryPath"
ON library."LibraryOptions" USING btree ("LibraryPath");
CREATE INDEX "IX_LibraryOptions_DateModified"
ON library."LibraryOptions" USING btree ("DateModified" DESC);
```
### Suggested PostgreSQL Migration
```csharp
// <copyright file="20260501000000_AddLibraryOptionsJsonStore.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddLibraryOptionsJsonStore : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE TABLE IF NOT EXISTS library.""LibraryOptions"" (
""LibraryId"" uuid NOT NULL,
""LibraryPath"" text NOT NULL,
""OptionsJson"" jsonb NOT NULL,
""Version"" integer NOT NULL DEFAULT 1,
""DateCreated"" timestamp with time zone NOT NULL DEFAULT now(),
""DateModified"" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ""PK_LibraryOptions"" PRIMARY KEY (""LibraryId"")
);
");
migrationBuilder.Sql(@"
CREATE UNIQUE INDEX IF NOT EXISTS ""IX_LibraryOptions_LibraryPath""
ON library.""LibraryOptions"" (""LibraryPath"");
");
migrationBuilder.Sql(@"
CREATE INDEX IF NOT EXISTS ""IX_LibraryOptions_DateModified""
ON library.""LibraryOptions"" (""DateModified"" DESC);
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryOptions"" CASCADE;");
}
}
}
```
### Suggested Entity
```csharp
// <copyright file="LibraryOptionsEntity.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities;
using System;
public class LibraryOptionsEntity
{
public Guid LibraryId { get; set; }
public required string LibraryPath { get; set; }
public required string OptionsJson { get; set; }
public int Version { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
}
```
### DbContext / Provider Mapping
Add to `JellyfinDbContext`:
```csharp
public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();
```
Add to `PostgresDatabaseProvider.ConfigureEntities(...)`:
```csharp
modelBuilder.Entity<LibraryOptionsEntity>().ToTable("LibraryOptions", Schemas.Library);
```
Recommended extra configuration:
```csharp
modelBuilder.Entity<LibraryOptionsEntity>()
.HasIndex(e => e.LibraryPath)
.IsUnique();
```
### Repository Sketch
This sketch follows the `MediaStreamRepository` and `BaseItemRepository` conventions already used in the codebase.
```csharp
// <copyright file="LibraryOptionsRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Server.Implementations.Library;
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
public class LibraryOptionsRepository
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationHost _appHost;
public LibraryOptionsRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost appHost)
{
_dbProvider = dbProvider;
_appHost = appHost;
}
public async Task<LibraryOptions?> GetLibraryOptionsAsync(Guid libraryId, string libraryPath, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var entity = await context.LibraryOptions
.AsNoTracking()
.SingleOrDefaultAsync(e => e.LibraryId == libraryId || e.LibraryPath == libraryPath, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
{
return null;
}
var options = JsonSerializer.Deserialize<LibraryOptions>(entity.OptionsJson, JsonDefaults.Options) ?? new LibraryOptions();
foreach (var mediaPath in options.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ExpandVirtualPath(mediaPath.Path);
}
}
return options;
}
public async Task SaveLibraryOptionsAsync(Guid libraryId, string libraryPath, LibraryOptions options, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, JsonDefaults.Options), JsonDefaults.Options)
?? new LibraryOptions();
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ReverseVirtualPath(mediaPath.Path);
}
}
var json = JsonSerializer.Serialize(clone, JsonDefaults.Options);
var entity = await context.LibraryOptions
.SingleOrDefaultAsync(e => e.LibraryId == libraryId, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
{
entity = new LibraryOptionsEntity
{
LibraryId = libraryId,
LibraryPath = libraryPath,
OptionsJson = json,
Version = 1,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow
};
await context.LibraryOptions.AddAsync(entity, cancellationToken).ConfigureAwait(false);
}
else
{
entity.LibraryPath = libraryPath;
entity.OptionsJson = json;
entity.DateModified = DateTime.UtcNow;
}
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
}
```
### CollectionFolder Integration Sketch
The minimal integration strategy is:
1. Change `CollectionFolder.LoadLibraryOptions(...)` to try the repository first.
2. If no database row exists, fall back to `options.xml`.
3. After a successful XML read, persist the value into `library."LibraryOptions"`.
4. Keep the current static cache keyed by library path.
Pseudo-flow:
```csharp
var fromDb = await _libraryOptionsRepository.GetLibraryOptionsAsync(libraryId, path, cancellationToken);
if (fromDb is not null)
{
return fromDb;
}
var fromXml = LoadLibraryOptionsFromXml(path);
await _libraryOptionsRepository.SaveLibraryOptionsAsync(libraryId, path, fromXml, cancellationToken);
return fromXml;
```
### Tradeoffs
- Lowest risk.
- No relational visibility into individual media roots.
- Best fit if the goal is only to stop using `options.xml` as the persistence backend.
## Option 2: Hybrid JSON + Queryable Media Paths
### Goal
Keep the full `LibraryOptions` object as JSON for compatibility, but project `PathInfos` into a separate relational table so path-level reporting and joins are cheap and reliable.
### Table Design
```sql
CREATE TABLE library."LibraryOptions" (
"LibraryId" uuid NOT NULL,
"LibraryPath" text NOT NULL,
"OptionsJson" jsonb NOT NULL,
"Version" integer NOT NULL DEFAULT 1,
"DateCreated" timestamp with time zone NOT NULL DEFAULT now(),
"DateModified" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "PK_LibraryOptions" PRIMARY KEY ("LibraryId")
);
CREATE UNIQUE INDEX "IX_LibraryOptions_LibraryPath"
ON library."LibraryOptions" USING btree ("LibraryPath");
CREATE TABLE library."LibraryMediaPaths" (
"Id" uuid NOT NULL,
"LibraryId" uuid NOT NULL,
"Path" text NOT NULL,
"PathVirtual" text NOT NULL,
"Position" integer NOT NULL,
"DateCreated" timestamp with time zone NOT NULL DEFAULT now(),
"DateModified" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT "PK_LibraryMediaPaths" PRIMARY KEY ("Id"),
CONSTRAINT "FK_LibraryMediaPaths_LibraryOptions_LibraryId"
FOREIGN KEY ("LibraryId") REFERENCES library."LibraryOptions" ("LibraryId") ON DELETE CASCADE
);
CREATE UNIQUE INDEX "IX_LibraryMediaPaths_LibraryId_Position"
ON library."LibraryMediaPaths" USING btree ("LibraryId", "Position");
CREATE INDEX "IX_LibraryMediaPaths_Path"
ON library."LibraryMediaPaths" USING btree ("Path");
CREATE INDEX "IX_LibraryMediaPaths_PathVirtual"
ON library."LibraryMediaPaths" USING btree ("PathVirtual");
CREATE INDEX "IX_LibraryMediaPaths_LibraryId"
ON library."LibraryMediaPaths" USING btree ("LibraryId");
```
### Suggested PostgreSQL Migration
```csharp
// <copyright file="20260501001000_AddLibraryOptionsHybridStore.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddLibraryOptionsHybridStore : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE TABLE IF NOT EXISTS library.""LibraryOptions"" (
""LibraryId"" uuid NOT NULL,
""LibraryPath"" text NOT NULL,
""OptionsJson"" jsonb NOT NULL,
""Version"" integer NOT NULL DEFAULT 1,
""DateCreated"" timestamp with time zone NOT NULL DEFAULT now(),
""DateModified"" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ""PK_LibraryOptions"" PRIMARY KEY (""LibraryId"")
);
");
migrationBuilder.Sql(@"
CREATE UNIQUE INDEX IF NOT EXISTS ""IX_LibraryOptions_LibraryPath""
ON library.""LibraryOptions"" (""LibraryPath"");
");
migrationBuilder.Sql(@"
CREATE TABLE IF NOT EXISTS library.""LibraryMediaPaths"" (
""Id"" uuid NOT NULL,
""LibraryId"" uuid NOT NULL,
""Path"" text NOT NULL,
""PathVirtual"" text NOT NULL,
""Position"" integer NOT NULL,
""DateCreated"" timestamp with time zone NOT NULL DEFAULT now(),
""DateModified"" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ""PK_LibraryMediaPaths"" PRIMARY KEY (""Id""),
CONSTRAINT ""FK_LibraryMediaPaths_LibraryOptions_LibraryId""
FOREIGN KEY (""LibraryId"") REFERENCES library.""LibraryOptions"" (""LibraryId"") ON DELETE CASCADE
);
");
migrationBuilder.Sql(@"
CREATE UNIQUE INDEX IF NOT EXISTS ""IX_LibraryMediaPaths_LibraryId_Position""
ON library.""LibraryMediaPaths"" (""LibraryId"", ""Position"");
");
migrationBuilder.Sql(@"
CREATE INDEX IF NOT EXISTS ""IX_LibraryMediaPaths_Path""
ON library.""LibraryMediaPaths"" (""Path"");
");
migrationBuilder.Sql(@"
CREATE INDEX IF NOT EXISTS ""IX_LibraryMediaPaths_PathVirtual""
ON library.""LibraryMediaPaths"" (""PathVirtual"");
");
migrationBuilder.Sql(@"
CREATE INDEX IF NOT EXISTS ""IX_LibraryMediaPaths_LibraryId""
ON library.""LibraryMediaPaths"" (""LibraryId"");
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryMediaPaths"" CASCADE;");
migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryOptions"" CASCADE;");
}
}
}
```
### Suggested Entities
```csharp
// <copyright file="LibraryOptionsEntity.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities;
using System;
using System.Collections.Generic;
public class LibraryOptionsEntity
{
public Guid LibraryId { get; set; }
public required string LibraryPath { get; set; }
public required string OptionsJson { get; set; }
public int Version { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
public ICollection<LibraryMediaPathEntity> MediaPaths { get; set; } = new List<LibraryMediaPathEntity>();
}
```
```csharp
// <copyright file="LibraryMediaPathEntity.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities;
using System;
public class LibraryMediaPathEntity
{
public Guid Id { get; set; }
public Guid LibraryId { get; set; }
public required string Path { get; set; }
public required string PathVirtual { get; set; }
public int Position { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateModified { get; set; }
public LibraryOptionsEntity LibraryOptions { get; set; } = null!;
}
```
### DbContext / Provider Mapping
Add to `JellyfinDbContext`:
```csharp
public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();
public DbSet<LibraryMediaPathEntity> LibraryMediaPaths => this.Set<LibraryMediaPathEntity>();
```
Add to `PostgresDatabaseProvider.ConfigureEntities(...)`:
```csharp
modelBuilder.Entity<LibraryOptionsEntity>().ToTable("LibraryOptions", Schemas.Library);
modelBuilder.Entity<LibraryMediaPathEntity>().ToTable("LibraryMediaPaths", Schemas.Library);
modelBuilder.Entity<LibraryOptionsEntity>()
.HasMany(e => e.MediaPaths)
.WithOne(e => e.LibraryOptions)
.HasForeignKey(e => e.LibraryId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<LibraryOptionsEntity>()
.HasIndex(e => e.LibraryPath)
.IsUnique();
modelBuilder.Entity<LibraryMediaPathEntity>()
.HasIndex(e => new { e.LibraryId, e.Position })
.IsUnique();
```
### Repository Sketch
```csharp
// <copyright file="LibraryOptionsRepository.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Server.Implementations.Library;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller;
using MediaBrowser.Model.Configuration;
using Microsoft.EntityFrameworkCore;
public class LibraryOptionsRepository
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IServerApplicationHost _appHost;
public LibraryOptionsRepository(IDbContextFactory<JellyfinDbContext> dbProvider, IServerApplicationHost appHost)
{
_dbProvider = dbProvider;
_appHost = appHost;
}
public async Task<LibraryOptions?> GetLibraryOptionsAsync(Guid libraryId, string libraryPath, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var entity = await context.LibraryOptions
.AsNoTracking()
.Include(e => e.MediaPaths.OrderBy(p => p.Position))
.SingleOrDefaultAsync(e => e.LibraryId == libraryId || e.LibraryPath == libraryPath, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
{
return null;
}
var options = JsonSerializer.Deserialize<LibraryOptions>(entity.OptionsJson, JsonDefaults.Options) ?? new LibraryOptions();
foreach (var mediaPath in options.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ExpandVirtualPath(mediaPath.Path);
}
}
return options;
}
public async Task SaveLibraryOptionsAsync(Guid libraryId, string libraryPath, LibraryOptions options, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
var executionStrategy = context.Database.CreateExecutionStrategy();
await executionStrategy.ExecuteAsync(async () =>
{
await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, JsonDefaults.Options), JsonDefaults.Options)
?? new LibraryOptions();
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = _appHost.ReverseVirtualPath(mediaPath.Path);
}
}
var json = JsonSerializer.Serialize(clone, JsonDefaults.Options);
var utcNow = DateTime.UtcNow;
var entity = await context.LibraryOptions
.Include(e => e.MediaPaths)
.SingleOrDefaultAsync(e => e.LibraryId == libraryId, cancellationToken)
.ConfigureAwait(false);
if (entity is null)
{
entity = new LibraryOptionsEntity
{
LibraryId = libraryId,
LibraryPath = libraryPath,
OptionsJson = json,
Version = 1,
DateCreated = utcNow,
DateModified = utcNow
};
await context.LibraryOptions.AddAsync(entity, cancellationToken).ConfigureAwait(false);
}
else
{
entity.LibraryPath = libraryPath;
entity.OptionsJson = json;
entity.DateModified = utcNow;
}
if (entity.MediaPaths.Count > 0)
{
context.LibraryMediaPaths.RemoveRange(entity.MediaPaths);
}
entity.MediaPaths = clone.PathInfos
.Select((pathInfo, index) => new LibraryMediaPathEntity
{
Id = Guid.NewGuid(),
LibraryId = libraryId,
Path = _appHost.ExpandVirtualPath(pathInfo.Path),
PathVirtual = pathInfo.Path,
Position = index,
DateCreated = utcNow,
DateModified = utcNow
})
.ToList();
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
}
public async Task<Guid[]> GetLibrariesByMediaPathAsync(string path, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
return await context.LibraryMediaPaths
.AsNoTracking()
.Where(e => e.Path == path || e.PathVirtual == path)
.OrderBy(e => e.Position)
.Select(e => e.LibraryId)
.Distinct()
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
}
}
```
### Operational Queries Enabled By The Hybrid Model
```sql
SELECT lmp."LibraryId", lmp."Path", lmp."PathVirtual"
FROM library."LibraryMediaPaths" AS lmp
WHERE lmp."Path" = '/media/main_bridge/media/movies'
OR lmp."PathVirtual" = '/media/main_bridge/media/movies';
```
```sql
SELECT lmp."Path", count(*) AS "LibraryCount"
FROM library."LibraryMediaPaths" AS lmp
GROUP BY lmp."Path"
HAVING count(*) > 1
ORDER BY "LibraryCount" DESC, lmp."Path";
```
### Tradeoffs
- Slightly more implementation work than the JSON-only option.
- Keeps the current `LibraryOptions` object model intact.
- Makes media roots queryable, indexable, and auditable.
- Requires a transactional write path so JSON and projected rows do not drift.
## Recommended Touch Points In This Repo
Regardless of which option is chosen, the same set of integration points will need to move:
- `MediaBrowser.Controller/Entities/CollectionFolder.cs`
- Replace raw XML read/write with repository-backed read/write.
- Keep XML fallback for a transition period if rollback safety matters.
- `src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs`
- Add `DbSet<>` properties.
- `src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/`
- Add `LibraryOptionsEntity` and, for the hybrid approach, `LibraryMediaPathEntity`.
- `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs`
- Add table mapping and relationship/index configuration.
- `src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/`
- Add provider-specific migration(s).
- `Jellyfin.Server.Implementations/Library/`
- Add a repository class and register it in DI.
## Recommended Rollout
### JSON-In-DB
1. Add the table, entity, mapping, and repository.
2. Read DB first, then fall back to XML.
3. Backfill on demand when XML is encountered.
4. Keep XML writes optional for one release if rollback support is needed.
### Hybrid
1. Add both tables, entities, mapping, and repository.
2. Save JSON and `PathInfos` projection in one transaction.
3. Read DB first, then fall back to XML.
4. Add a small admin or diagnostic query surface over `LibraryMediaPaths`.
5. Remove XML writes after migration confidence is high.
## Recommendation
If the immediate goal is only to stop relying on `options.xml`, choose the JSON-only design.
If the goal includes operational visibility into library roots, duplicate path detection, path remapping support, or future query-driven tooling, choose the hybrid design. The hybrid design is the better long-term fit for PostgreSQL while still preserving the current `LibraryOptions` object graph and call sites.
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IncludeBuildOutput>false</IncludeBuildOutput>
@@ -0,0 +1,112 @@
# Database Configuration Migration Guide
## Overview
Jellyfin has transitioned from XML-based (`database.xml`) to JSON-based (`database.json`) configuration for database settings. This provides better support for interface types, cleaner syntax, and improved maintainability.
## Backward Compatibility
**The system is fully backward compatible.** If you have an existing `database.xml` file, Jellyfin will continue to work without any changes needed. The configuration loader automatically:
1. Checks for `database.json` first
2. Falls back to `database.xml` if JSON doesn't exist
3. Creates default configuration files as needed
## Migration Options
### Option 1: Automatic Migration (Recommended)
Simply rename or create a new `database.json` file in your Jellyfin configuration directory. The old `database.xml` will continue to work as a fallback.
### Option 2: Manual Migration
If you want to migrate from `database.xml` to `database.json`:
1. **Backup your existing configuration:**
```bash
cp /path/to/config/database.xml /path/to/config/database.xml.backup
```
2. **Create a new `database.json`:**
Use the provided `database.json.example` as a template and copy it to `database.json`:
```bash
cp database.json.example /path/to/config/database.json
```
3. **Edit `database.json`** with your specific settings:
- Update the `connectionString` with your database credentials
- Adjust `backupOptions` if using PostgreSQL
- Keep any custom provider settings
4. **Test the new configuration:**
Start Jellyfin and verify it connects successfully to your database in the logs.
5. **Remove the old file (optional):**
Once confirmed working, you can remove `database.xml`:
```bash
rm /path/to/config/database.xml
```
## Format Differences
### XML Format (Legacy)
```xml
<?xml version="1.0" encoding="utf-8"?>
<DatabaseConfigurationOptions>
<DatabaseType>Jellyfin-PostgreSQL</DatabaseType>
<LockingBehavior>NoLock</LockingBehavior>
<CustomProviderOptions>
<PluginName>Jellyfin-PostgreSQL</PluginName>
...
</CustomProviderOptions>
</DatabaseConfigurationOptions>
```
### JSON Format (Current)
```json
{
"databaseType": "Jellyfin-PostgreSQL",
"lockingBehavior": "NoLock",
"customProviderOptions": {
"pluginName": "Jellyfin-PostgreSQL",
...
}
}
```
## Configuration File Location
The configuration file should be placed in your Jellyfin configuration directory:
- **Linux:** `/etc/jellyfin/` or `~/.config/jellyfin/`
- **Windows:** `%APPDATA%\jellyfin\`
- **Docker:** `/config/` (mounted volume)
## Key Differences
| Feature | XML | JSON |
|---------|-----|------|
| Supports interfaces | ❌ | ✅ |
| Human-readable | ✅ | ✅ |
| Backward compatible | ✅ | ✅ (via fallback) |
| Easier parsing | ❌ | ✅ |
## Troubleshooting
### Error: "Error loading configuration file"
- Ensure the JSON file is valid JSON (use a JSON validator)
- Check file permissions (must be readable by Jellyfin process)
- Verify the configuration path is correct
### Fallback to XML
If you see logs indicating XML deserialization, this means:
- No `database.json` file was found
- The system is using the existing `database.xml` as fallback
- This is normal and expected during migration
### Performance Note
JSON parsing is faster than XML, so you may see slight improvements after migration.
## Support
For issues related to database configuration:
1. Check that your JSON syntax is valid
2. Verify database connection settings
3. Review Jellyfin logs for detailed error messages
4. Keep a backup of your working configuration
@@ -5,7 +5,8 @@
namespace Jellyfin.Database.Implementations.DbConfiguration;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
/// <summary>
/// Options to configure jellyfins managed database.
@@ -67,7 +68,7 @@ public class DatabaseConfigurationOptions
/// Gets or sets preset configurations for quick switching between database providers.
/// These are stored in the configuration file for reference but not used unless DatabaseType is set to match a preset key.
/// </summary>
[XmlArray("PresetConfigurations")]
[XmlArrayItem("PresetConfiguration")]
[SuppressMessage("Design", "CA2227:Collection properties should be read-only", Justification = "JSON deserialization requires settable property")]
[SuppressMessage("Naming", "CA1002:Do not expose generic lists", Justification = "System.Text.Json works best with List<T> for array deserialization")]
public List<PresetConfigurationItem>? PresetConfigurations { get; set; }
}
@@ -0,0 +1,54 @@
{
"databaseType": "Jellyfin-PostgreSQL",
"lockingBehavior": "NoLock",
"customProviderOptions": {
"pluginName": "Jellyfin-PostgreSQL",
"pluginAssembly": "Jellyfin.Database.Providers.Postgres",
"connectionString": "Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=your_password_here",
"options": [
{
"key": "Host",
"value": "localhost"
},
{
"key": "Port",
"value": "5432"
},
{
"key": "Database",
"value": "jellyfin"
}
]
},
"backupOptions": {
"pgDumpPath": "pg_dump",
"pgRestorePath": "pg_restore",
"backupFormat": "custom",
"includeBlobs": true,
"compressionLevel": 6,
"timeoutSeconds": 1800,
"verboseOutput": true
},
"presetConfigurations": [
{
"displayName": "Local PostgreSQL",
"description": "Default PostgreSQL configuration for local development",
"databaseType": "Jellyfin-PostgreSQL",
"customProviderOptions": {
"pluginName": "Jellyfin-PostgreSQL",
"pluginAssembly": "Jellyfin.Database.Providers.Postgres",
"connectionString": "Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=jellyfin"
}
},
{
"displayName": "SQLite",
"description": "SQLite configuration for embedded deployments",
"databaseType": "Jellyfin-Sqlite",
"customProviderOptions": {
"pluginName": "Jellyfin-Sqlite",
"pluginAssembly": "Jellyfin.Database.Providers.Sqlite",
"connectionString": "Data Source=jellyfin.db"
}
}
]
}
@@ -0,0 +1,38 @@
// <copyright file="LibraryOptionsEntity.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities;
using System;
/// <summary>
/// Stores serialized collection-folder library options for database-backed persistence.
/// </summary>
public class LibraryOptionsEntity
{
/// <summary>
/// Gets or sets the collection folder path used as the prototype key.
/// </summary>
public required string LibraryPath { get; set; }
/// <summary>
/// Gets or sets the serialized library options JSON payload.
/// </summary>
public required string OptionsJson { get; set; }
/// <summary>
/// Gets or sets the schema version for the serialized payload.
/// </summary>
public int Version { get; set; }
/// <summary>
/// Gets or sets the UTC timestamp when the row was created.
/// </summary>
public DateTime DateCreated { get; set; }
/// <summary>
/// Gets or sets the UTC timestamp when the row was last modified.
/// </summary>
public DateTime DateModified { get; set; }
}
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -11,6 +12,7 @@
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Database.Implementations</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
@@ -19,6 +21,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@@ -8,6 +8,7 @@ namespace Jellyfin.Database.Implementations;
using System;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -180,6 +181,11 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<KeyframeData> KeyframeData => this.Set<KeyframeData>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing persisted library options.
/// </summary>
public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();
/*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>();
@@ -291,12 +297,14 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
if (isConstraintViolation)
{
// Log constraint violations as warnings (expected in some concurrent scenarios)
#pragma warning disable CA1848 // Use LoggerMessage delegates - exception type is dynamic
logger.LogWarning(
ex,
"Database constraint violation: Attempted to insert or update data that violates a database constraint. " +
"This may indicate a concurrency issue or a bug in the update logic. " +
"Inner exception: {InnerExceptionType}",
ex.InnerException?.GetType().Name ?? "unknown");
#pragma warning restore CA1848
}
else
{
@@ -0,0 +1,30 @@
// <copyright file="LibraryOptionsEntityConfiguration.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.ModelConfiguration;
using System;
using Jellyfin.Database.Implementations.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
/// <summary>
/// LibraryOptionsEntity Configuration.
/// </summary>
public class LibraryOptionsEntityConfiguration : IEntityTypeConfiguration<LibraryOptionsEntity>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<LibraryOptionsEntity> builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.ToTable("LibraryOptions", "library");
builder.HasKey(e => e.LibraryPath);
builder.Property(e => e.LibraryPath).IsRequired();
builder.Property(e => e.OptionsJson).IsRequired().HasColumnType("jsonb");
builder.Property(e => e.Version).HasDefaultValue(1);
builder.Property(e => e.DateCreated).ValueGeneratedOnAdd();
builder.Property(e => e.DateModified).ValueGeneratedOnAdd();
builder.HasIndex(e => e.DateModified).IsDescending();
}
}
@@ -4,6 +4,7 @@
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CA1054;CA1055;CA1308;CA1031;CA1032;CA2007;CA1822;CA1848;CA1303;CA1508;CA1805;CA1056;CA1062;CA1861;CA1305;CA1873;CA2201;CA1034;CA1819;CA2100;CA2000;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -0,0 +1,40 @@
// <copyright file="20260501000000_AddLibraryOptionsTable.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Database.Providers.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddLibraryOptionsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE TABLE IF NOT EXISTS library.""LibraryOptions"" (
""LibraryPath"" text NOT NULL,
""OptionsJson"" jsonb NOT NULL,
""Version"" integer NOT NULL DEFAULT 1,
""DateCreated"" timestamp with time zone NOT NULL DEFAULT now(),
""DateModified"" timestamp with time zone NOT NULL DEFAULT now(),
CONSTRAINT ""PK_LibraryOptions"" PRIMARY KEY (""LibraryPath"")
);
");
migrationBuilder.Sql(@"
CREATE INDEX IF NOT EXISTS ""IX_LibraryOptions_DateModified""
ON library.""LibraryOptions"" (""DateModified"" DESC);
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryOptions"" CASCADE;");
}
}
}
@@ -808,6 +808,10 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
modelBuilder.Entity<TrickplayInfo>().ToTable("TrickplayInfos", Schemas.Library);
modelBuilder.Entity<MediaSegment>().ToTable("MediaSegments", Schemas.Library);
modelBuilder.Entity<KeyframeData>().ToTable("KeyframeData", Schemas.Library);
modelBuilder.Entity<LibraryOptionsEntity>().ToTable("LibraryOptions", Schemas.Library);
modelBuilder.Entity<LibraryOptionsEntity>().HasKey(e => e.LibraryPath);
modelBuilder.Entity<LibraryOptionsEntity>().HasIndex(e => e.DateModified);
}
/// <inheritdoc/>
@@ -4,6 +4,7 @@
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CA1062;CA1861;CA1873;CA1848;CA2253;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -0,0 +1,47 @@
// <copyright file="20260501000000_AddLibraryOptionsTable.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#nullable disable
namespace Jellyfin.Database.Providers.Sqlite.Migrations
{
using System;
using Microsoft.EntityFrameworkCore.Migrations;
/// <inheritdoc />
public partial class AddLibraryOptionsTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "LibraryOptions",
columns: table => new
{
LibraryPath = table.Column<string>(type: "TEXT", nullable: false),
OptionsJson = table.Column<string>(type: "TEXT", nullable: false),
Version = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
DateCreated = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
DateModified = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_LibraryOptions", x => x.LibraryPath);
});
migrationBuilder.CreateIndex(
name: "IX_LibraryOptions_DateModified",
table: "LibraryOptions",
column: "DateModified",
descending: new[] { true });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "LibraryOptions");
}
}
}
@@ -10,7 +10,7 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- TODO: Remove once we update SkiaSharp > 2.88.5 -->
<NoWarn>NU1903</NoWarn>
<NoWarn>$(NoWarn);NU1903;CA1062;CA1031;CA1848;CA1873;CA2000;CA1508;CA1822;CA2253;CA5394;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -9,6 +9,7 @@
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CA1062;CA1031;CA1848;CA1873;CA1822;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -9,7 +9,7 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- ICU4N.Transliterator only has prerelease versions -->
<NoWarn>NU5104</NoWarn>
<NoWarn>NU5104;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CA1062;CA1031;CA1848;CA1873;CA2253;CA1805;CA1822;CA1861;CA2234;CA1024;CA1724;CA1859;CA1308;CA1056;CA2025;CA1054;CA2000;CA5394;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.MediaEncoding.Keyframes</PackageId>
<VersionPrefix>10.11.0</VersionPrefix>
@@ -14,6 +16,7 @@
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1024;CA1031;CA1032;CA1034;CA1054;CA1055;CA1056;CA1062;CA1308;CA1303;CA1305;CA1508;CA1722;CA1724;CA1805;CA1813;CA1815;CA1816;CA1819;CA1821;CA1822;CA1823;CA1824;CA1825;CA1826;CA1830;CA1836;CA1839;CA1840;CA1841;CA1859;CA1861;CA1873;CA1911;CA2000;CA2007;CA2016;CA2025;CA2026;CA2100;CA2101;CA2102;CA2201;CA2202;CA2207;CA2208;CA2219;CA2220;CA2222;CA2225;CA2226;CA2230;CA2231;CA2234;CA2241;CA2242;CA2243;CA2244;CA2245;CA2246;CA2247;CA2248;CA2249;CA2250;CA2253;CA3001;CA3002;CA3003;CA3004;CA3005;CA3006;CA3007;CA3008;CA3009;CA3010;CA3011;CA3012;CA3061;CA3062;CA5359;CA5360;CA5361;CA5362;CA5363;CA5364;CA5365;CA5366;CA5367;CA5368;CA5369;CA5370;CA5371;CA5372;CA5373;CA5374;CA5375;CA5376;CA5377;CA5378;CA5379;CA5380;CA5381;CA5382;CA5383;CA5384;CA5385;CA5386;CA5387;CA5388;CA5389;CA5390;CA5391;CA5392;CA5393;CA5394;CA5395;CA5396;CA5397;CA5398;CA5399;CA5400;CA5401;CA5402;CA5403;NU1903;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517</NoWarn>
<TargetFramework>net11.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -0,0 +1,203 @@
// <copyright file="ConfigurationHelperTests.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Emby.Server.Implementations.Tests.AppBase;
using System;
using System.IO;
using System.Text.Json;
using Emby.Server.Implementations.AppBase;
using Jellyfin.Database.Implementations.DbConfiguration;
using Jellyfin.Extensions.Json;
using Xunit;
/// <summary>
/// Tests for the ConfigurationHelper JSON loading functionality.
/// </summary>
public class ConfigurationHelperTests
{
[Fact]
public void GetJsonConfiguration_ValidJsonFile_DeserializesCorrectly()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var jsonPath = Path.Combine(tempDir, "test-config.json");
var testConfig = new DatabaseConfigurationOptions
{
DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.NoLock
};
Directory.CreateDirectory(tempDir);
var json = JsonSerializer.Serialize(testConfig, JsonDefaults.Options);
File.WriteAllText(jsonPath, json);
try
{
// Act
var result = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Assert
Assert.NotNull(result);
Assert.Equal("Jellyfin-PostgreSQL", result.DatabaseType);
Assert.Equal(DatabaseLockingBehaviorTypes.NoLock, result.LockingBehavior);
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public void GetJsonConfiguration_MissingFile_CreatesDefaultAndReturnsIt()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var jsonPath = Path.Combine(tempDir, "test-config.json");
try
{
// Act
var result = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Assert
Assert.NotNull(result);
Assert.True(File.Exists(jsonPath), "JSON file should be created");
// Verify the file is valid JSON
var fileContent = File.ReadAllText(jsonPath);
var deserialized = JsonSerializer.Deserialize<DatabaseConfigurationOptions>(fileContent, JsonDefaults.Options);
Assert.NotNull(deserialized);
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public void GetJsonConfiguration_WithCustomProviderOptions_PreservesSettings()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var jsonPath = Path.Combine(tempDir, "test-config.json");
var testConfig = new DatabaseConfigurationOptions
{
DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.Pessimistic,
CustomProviderOptions = new CustomDatabaseOptions
{
PluginName = "Jellyfin-PostgreSQL",
PluginAssembly = "Jellyfin.Database.Providers.Postgres",
ConnectionString = "Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=secret"
}
};
Directory.CreateDirectory(tempDir);
var json = JsonSerializer.Serialize(testConfig, JsonDefaults.Options);
File.WriteAllText(jsonPath, json);
try
{
// Act
var result = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Assert
Assert.NotNull(result);
Assert.Equal("Jellyfin-PostgreSQL", result.DatabaseType);
Assert.Equal(DatabaseLockingBehaviorTypes.Pessimistic, result.LockingBehavior);
Assert.NotNull(result.CustomProviderOptions);
Assert.Equal("Jellyfin-PostgreSQL", result.CustomProviderOptions.PluginName);
Assert.Equal("Host=localhost;Port=5432;Database=jellyfin;Username=jellyfin;Password=secret",
result.CustomProviderOptions.ConnectionString);
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public void GetJsonConfiguration_InvalidJson_CreatesDefault()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var jsonPath = Path.Combine(tempDir, "test-config.json");
Directory.CreateDirectory(tempDir);
File.WriteAllText(jsonPath, "{ invalid json");
try
{
// Act
var result = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Assert - should get default instance on invalid JSON
Assert.NotNull(result);
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
[Fact]
public void GetJsonConfiguration_RoundTrip_PreservesData()
{
// Arrange
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var jsonPath = Path.Combine(tempDir, "test-config.json");
var originalConfig = new DatabaseConfigurationOptions
{
DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.Optimistic
};
Directory.CreateDirectory(tempDir);
try
{
// Act - First load should create the file
var result1 = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Modify and verify round-trip
var json = JsonSerializer.Serialize(originalConfig, JsonDefaults.Options);
File.WriteAllText(jsonPath, json);
var result2 = ConfigurationHelper.GetJsonConfiguration<DatabaseConfigurationOptions>(jsonPath);
// Assert
Assert.NotNull(result2);
Assert.Equal("Jellyfin-PostgreSQL", result2.DatabaseType);
Assert.Equal(DatabaseLockingBehaviorTypes.Optimistic, result2.LockingBehavior);
}
finally
{
// Cleanup
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, recursive: true);
}
}
}
}
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -27,3 +28,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -24,3 +25,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@@ -24,3 +25,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
#pragma warning disable CA1861 // Avoid constant arrays as arguments - required for xUnit theory test data
namespace Jellyfin.Extensions.Tests
{
using System;
@@ -75,3 +78,6 @@ namespace Jellyfin.Extensions.Tests
}
}
}
#pragma warning restore CA1707
#pragma warning restore CA1861
@@ -2,6 +2,8 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
namespace Jellyfin.Extensions.Tests;
using System.IO;
@@ -25,3 +27,5 @@ public static class FileHelperTests
File.Delete(path);
}
}
#pragma warning restore CA1707
@@ -12,16 +12,18 @@ using Xunit;
public static class FormattingStreamWriterTests
{
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
#pragma warning disable IDE0063 // Use simple using statement
[Fact]
public static void Shuffle_Valid_Correct()
#pragma warning restore CA1707
#pragma warning restore IDE0063
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE", false);
using (var ms = new MemoryStream())
using (var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture))
{
using var ms = new MemoryStream();
using var txt = new FormattingStreamWriter(ms, CultureInfo.InvariantCulture);
txt.Write("{0}", 3.14159);
txt.Close();
Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray()));
}
}
}
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;CS1570;CS8600;SA1600;SA1309;SA1200</NoWarn>
<NoWarn>$(NoWarn);CS1591;CS1570;CS8600;SA1600;SA1309;SA1200;CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
#pragma warning disable CA1062 // Validate arguments of public methods - FsCheck generates valid inputs
namespace Jellyfin.Extensions.Tests.Json.Converters
{
using System.Text.Json;
@@ -67,3 +70,6 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
}
}
}
#pragma warning restore CA1707
#pragma warning restore CA1062
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
#pragma warning disable CA1861 // Avoid constant arrays as arguments - required for xUnit theory test data
namespace Jellyfin.Extensions.Tests.Json.Converters
{
using System;
@@ -260,3 +263,6 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
}
}
}
#pragma warning restore CA1707
#pragma warning restore CA1861
@@ -2,6 +2,8 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
namespace Jellyfin.Extensions.Tests.Json.Converters
{
using System.Text.Json;
@@ -129,3 +131,5 @@ namespace Jellyfin.Extensions.Tests.Json.Converters
}
}
}
#pragma warning restore CA1707
@@ -1,5 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -28,3 +29,4 @@
<ProjectReference Include="..\..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" />
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -24,3 +25,4 @@
</ItemGroup>
</Project>
@@ -2,10 +2,11 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
namespace Jellyfin.MediaEncoding.Keyframes.Tests.FfProbe
{
using System.IO;
using System.Text.Json;
using Jellyfin.MediaEncoding.Keyframes.FfProbe;
using Xunit;
public class FfProbeKeyframeExtractorTests
@@ -13,7 +14,9 @@ namespace Jellyfin.MediaEncoding.Keyframes.FfProbe
[Theory]
[InlineData("keyframes.txt", "keyframes_result.json")]
[InlineData("keyframes_streamduration.txt", "keyframes_streamduration_result.json")]
#pragma warning disable CA1707 // Identifiers should not contain underscores - xUnit test naming convention
public void ParseStream_Valid_Success(string testDataFileName, string resultFileName)
#pragma warning restore CA1707
{
var testDataPath = Path.Combine("FfProbe/Test Data", testDataFileName);
var resultPath = Path.Combine("FfProbe/Test Data", resultFileName);
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -38,3 +39,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -32,3 +33,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1055;CA1054;CA1308;CA2007;CA1822;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1822;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007</NoWarn>
<ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -23,3 +24,4 @@
</ItemGroup>
</Project>
@@ -2,6 +2,7 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -24,3 +25,4 @@
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -33,3 +34,4 @@
</ItemGroup>
</Project>
@@ -8,6 +8,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Microsoft.EntityFrameworkCore;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Locking;
using Jellyfin.Server.Implementations.Item;
@@ -2,11 +2,13 @@
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<JELLYFIN_WEB_DIR>E:/Projects/jellyfin-web</JELLYFIN_WEB_DIR>
</PropertyGroup>
@@ -43,3 +45,4 @@
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -43,3 +44,4 @@
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -25,3 +26,4 @@
</ItemGroup>
</Project>
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000</NoWarn>
<TargetFramework>net11.0</TargetFramework>
</PropertyGroup>
@@ -28,3 +29,4 @@
</ItemGroup>
</Project>