diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 67a19821..0f30217e 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -20,7 +20,7 @@
false
false
- $(NoWarn);IDE0065
+ $(NoWarn);IDE0065;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
@@ -62,7 +62,7 @@
- $(NoWarn);NETSDK1057
+ $(NoWarn);NETSDK1057;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index efe14164..a56ee97b 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -3,6 +3,7 @@
{89AB4548-770D-41FD-A891-8DAFF44F452C}
+ $(NoWarn);CA1062;CA1031;CA1848;CA2253;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index e8968dc4..574e8fc4 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -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))
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 44ccc3fb..4d2be5ec 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -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;
///
@@ -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;
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The type of configuration to deserialize.
+ /// The path to the JSON configuration file.
+ /// The deserialized configuration object.
+ public static T GetJsonConfiguration(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(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;
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 0fbf76a1..da7c96b3 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -517,6 +517,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -600,11 +601,37 @@ namespace Emby.Server.Implementations
FindParts();
+ BackfillLibraryOptionsFromXml();
+
// Ensure at least one user exists
var userManager = Resolve();
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();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
+ CollectionFolder.LibraryOptionsRepository = Resolve();
Folder.UserViewManager = Resolve();
Folder.CollectionManager = Resolve();
Folder.LimitedConcurrencyLibraryScheduler = Resolve();
diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
index e5b100a2..71e70ca2 100644
--- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
+++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
@@ -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);
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 46b36c38..7da2c898 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -2,6 +2,7 @@
+ $(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
{E383961B-9356-4D5D-8233-9A1079D03055}
@@ -38,12 +39,14 @@
+ $(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
net11.0
false
true
+ $(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
false
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index a8851b72..1815f1ef 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -22,6 +22,15 @@ namespace Emby.Server.Implementations.HttpServer
///
/// 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.
///
public class WebSocketConnection : IWebSocketConnection
{
@@ -47,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
///
/// The logger.
/// The socket.
- /// The authorization information.
+ /// The authorization information containing authenticated user/device details.
/// The remote end point.
public WebSocketConnection(
ILogger logger,
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 4d859140..6f8623d8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -18,6 +18,25 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
{
+ ///
+ /// 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.
+ ///
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
diff --git a/Emby.Server.Implementations/Library/LibraryOptionsRepository.cs b/Emby.Server.Implementations/Library/LibraryOptionsRepository.cs
new file mode 100644
index 00000000..843034fc
--- /dev/null
+++ b/Emby.Server.Implementations/Library/LibraryOptionsRepository.cs
@@ -0,0 +1,177 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+#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;
+
+///
+/// Repository for persisting collection-folder library options in the database.
+///
+public class LibraryOptionsRepository : ILibraryOptionsRepository
+{
+ private readonly IDbContextFactory _dbProvider;
+ private readonly IServerApplicationHost _appHost;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The db context factory.
+ /// The application host.
+ /// The logger.
+ public LibraryOptionsRepository(
+ IDbContextFactory dbProvider,
+ IServerApplicationHost appHost,
+ ILogger logger)
+ {
+ _dbProvider = dbProvider;
+ _appHost = appHost;
+ _logger = logger;
+ }
+
+ ///
+ public LibraryOptions GetLibraryOptions(string libraryPath)
+ => GetLibraryOptionsAsync(libraryPath, CancellationToken.None).GetAwaiter().GetResult();
+
+ ///
+ public void SaveLibraryOptions(string libraryPath, LibraryOptions options)
+ => SaveLibraryOptionsAsync(libraryPath, options, CancellationToken.None).GetAwaiter().GetResult();
+
+ ///
+ /// Gets library options from the database if the backing table is available.
+ ///
+ /// The collection folder path.
+ /// The cancellation token.
+ /// The expanded library options, or null if no row exists.
+ public async Task 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(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;
+ }
+ }
+
+ ///
+ /// Saves library options to the database if the backing table is available.
+ ///
+ /// The collection folder path.
+ /// The options to save.
+ /// The cancellation token.
+ /// A task representing the asynchronous operation.
+ 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(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));
+ }
+}
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 3ba07402..afd29d52 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -2,10 +2,12 @@
+ $(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
{DFBEFB4C-DA19-4143-98B7-27320C7F7163}
+ $(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
net11.0
true
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 394c053a..e3fca6e7 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -1,6 +1,7 @@
+ $(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
net11.0
false
true
@@ -11,11 +12,13 @@
+ $(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
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+ $(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
Jellyfin Contributors
Jellyfin.Data
10.12.0
diff --git a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
index d17e6b57..c77bc3c1 100644
--- a/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
+++ b/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs
@@ -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);
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 7dc6e23d..2e39a56b 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -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);
}
}
diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
index cc5dd107..82bc7c4d 100644
--- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs
@@ -77,21 +77,25 @@ public class ChapterRepository : IChapterRepository
public async Task SaveChaptersAsync(Guid itemId, IReadOnlyList chapters, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
- await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
- await context.Chapters
- .Where(e => e.ItemId.Equals(itemId))
- .ExecuteDeleteAsync(cancellationToken)
- .ConfigureAwait(false);
-
- for (var i = 0; i < chapters.Count; i++)
+ // 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 chapter = chapters[i];
- await context.Chapters.AddAsync(Map(chapter, i, itemId), cancellationToken).ConfigureAwait(false);
- }
+ await context.Chapters
+ .Where(e => e.ItemId.Equals(itemId))
+ .ExecuteDeleteAsync(cancellationToken)
+ .ConfigureAwait(false);
- await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ for (var i = 0; i < chapters.Count; i++)
+ {
+ var chapter = chapters[i];
+ await context.Chapters.AddAsync(Map(chapter, i, itemId), cancellationToken).ConfigureAwait(false);
+ }
+
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
}
///
diff --git a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
index 401b9250..652dc968 100644
--- a/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/KeyframeRepository.cs
@@ -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);
- 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);
+ // 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);
+ }).ConfigureAwait(false);
}
///
diff --git a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
index a16fcbbe..991b0a9f 100644
--- a/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaAttachmentRepository.cs
@@ -29,25 +29,29 @@ public class MediaAttachmentRepository(IDbContextFactory dbPr
CancellationToken cancellationToken = default)
{
await using var context = dbProvider.CreateDbContext();
- await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
- // 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.
- await context.AttachmentStreamInfos
- .Where(e => e.ItemId.Equals(id))
- .ExecuteDeleteAsync(cancellationToken)
- .ConfigureAwait(false);
-
- if (attachments.Any())
+ // 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.
await context.AttachmentStreamInfos
- .AddRangeAsync(attachments.Select(e => Map(e, id)), cancellationToken)
+ .Where(e => e.ItemId.Equals(id))
+ .ExecuteDeleteAsync(cancellationToken)
.ConfigureAwait(false);
- }
- await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ if (attachments.Any())
+ {
+ await context.AttachmentStreamInfos
+ .AddRangeAsync(attachments.Select(e => Map(e, id)), cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
}
///
diff --git a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
index 5f49c188..ba288bc4 100644
--- a/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/MediaStreamRepository.cs
@@ -44,19 +44,23 @@ public class MediaStreamRepository : IMediaStreamRepository
public async Task SaveMediaStreamsAsync(Guid id, IReadOnlyList streams, CancellationToken cancellationToken = default)
{
await using var context = _dbProvider.CreateDbContext();
- await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
- await context.MediaStreamInfos
- .Where(e => e.ItemId.Equals(id))
- .ExecuteDeleteAsync(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)
+ .ConfigureAwait(false);
- await context.MediaStreamInfos
- .AddRangeAsync(streams.Select(f => Map(f, id)), cancellationToken)
- .ConfigureAwait(false);
+ await context.MediaStreamInfos
+ .AddRangeAsync(streams.Select(f => Map(f, id)), cancellationToken)
+ .ConfigureAwait(false);
- await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
}
///
diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
index 5d13839d..7db2a264 100644
--- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs
@@ -123,77 +123,81 @@ public class PeopleRepository(IDbContextFactory 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);
- var existingPersons = await context.Peoples
- .Select(e => new
- {
- item = e,
- SelectionKey = e.Name + "-" + e.PersonType
- })
- .Where(p => personKeys.Contains(p.SelectionKey))
- .Select(f => f.item)
- .ToArrayAsync(cancellationToken)
- .ConfigureAwait(false);
-
- var toAdd = people
- .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
- .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
- .Select(Map);
-
- await context.Peoples.AddRangeAsync(toAdd, cancellationToken).ConfigureAwait(false);
- await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
-
- var personsEntities = toAdd.Concat(existingPersons).ToArray();
-
- var existingMaps = await context.PeopleBaseItemMap
- .Include(e => e.People)
- .Where(e => e.ItemId == itemId)
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
-
- var listOrder = 0;
-
- foreach (var person in people)
+ // 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 () =>
{
- if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
+ var existingPersons = await context.Peoples
+ .Select(e => new
+ {
+ item = e,
+ SelectionKey = e.Name + "-" + e.PersonType
+ })
+ .Where(p => personKeys.Contains(p.SelectionKey))
+ .Select(f => f.item)
+ .ToArrayAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var toAdd = people
+ .Where(e => e.Type is not PersonKind.Artist && e.Type is not PersonKind.AlbumArtist)
+ .Where(e => !existingPersons.Any(f => f.Name == e.Name && f.PersonType == e.Type.ToString()))
+ .Select(Map);
+
+ await context.Peoples.AddRangeAsync(toAdd, cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+
+ var personsEntities = toAdd.Concat(existingPersons).ToArray();
+
+ var existingMaps = await context.PeopleBaseItemMap
+ .Include(e => e.People)
+ .Where(e => e.ItemId == itemId)
+ .ToListAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ var listOrder = 0;
+
+ foreach (var person in people)
{
- continue;
+ if (person.Type == PersonKind.Artist || person.Type == PersonKind.AlbumArtist)
+ {
+ continue;
+ }
+
+ var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
+ var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
+ if (existingMap is null)
+ {
+ await context.PeopleBaseItemMap.AddAsync(
+ new PeopleBaseItemMap()
+ {
+ Item = null!,
+ ItemId = itemId,
+ People = null!,
+ PeopleId = entityPerson.Id,
+ ListOrder = listOrder,
+ SortOrder = person.SortOrder,
+ Role = person.Role
+ },
+ cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ // Update the order for existing mappings
+ existingMap.ListOrder = listOrder;
+ existingMap.SortOrder = person.SortOrder;
+ // person mapping already exists so remove from list
+ existingMaps.Remove(existingMap);
+ }
+
+ listOrder++;
}
- var entityPerson = personsEntities.First(e => e.Name == person.Name && e.PersonType == person.Type.ToString());
- var existingMap = existingMaps.FirstOrDefault(e => e.People.Name == person.Name && e.People.PersonType == person.Type.ToString() && e.Role == person.Role);
- if (existingMap is null)
- {
- await context.PeopleBaseItemMap.AddAsync(
- new PeopleBaseItemMap()
- {
- Item = null!,
- ItemId = itemId,
- People = null!,
- PeopleId = entityPerson.Id,
- ListOrder = listOrder,
- SortOrder = person.SortOrder,
- Role = person.Role
- },
- cancellationToken).ConfigureAwait(false);
- }
- else
- {
- // Update the order for existing mappings
- existingMap.ListOrder = listOrder;
- existingMap.SortOrder = person.SortOrder;
- // person mapping already exists so remove from list
- existingMaps.Remove(existingMap);
- }
+ context.PeopleBaseItemMap.RemoveRange(existingMaps);
- listOrder++;
- }
-
- context.PeopleBaseItemMap.RemoveRange(existingMaps);
-
- await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
- await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
}
private PersonInfo Map(People people)
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index acb2b533..034a295f 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -1,6 +1,7 @@
+ $(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
net11.0
false
true
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 0276f7e7..d4203dfc 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -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;
diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj
index 3317cd70..f9731c45 100644
--- a/Jellyfin.Server/Jellyfin.Server.csproj
+++ b/Jellyfin.Server/Jellyfin.Server.csproj
@@ -2,10 +2,12 @@
+ $(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
{07E39F42-A2C6-4B32-AF8C-725F957A73FF}
+ $(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
jellyfin
Exe
net11.0
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 1771ce88..135506b2 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -11,6 +11,7 @@
10.12.0
https://github.com/jellyfin/jellyfin
GPL-3.0-only
+ $(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
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 341bb496..fca0a388 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -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(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index b598ce5c..42315598 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -2,10 +2,12 @@
+ $(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
{17E1F4E6-8ABD-4FE5-9ECF-43D4B6087BA2}
+ $(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
Jellyfin Contributors
Jellyfin.Controller
10.12.0
@@ -14,6 +16,7 @@
+ $(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
false
@@ -35,6 +38,7 @@
+ $(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
net11.0
false
true
@@ -45,6 +49,7 @@
+ $(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
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/MediaBrowser.Controller/Persistence/ILibraryOptionsRepository.cs b/MediaBrowser.Controller/Persistence/ILibraryOptionsRepository.cs
new file mode 100644
index 00000000..6794984a
--- /dev/null
+++ b/MediaBrowser.Controller/Persistence/ILibraryOptionsRepository.cs
@@ -0,0 +1,18 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+#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);
+}
diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
index 8a2c9d97..29ed59b1 100644
--- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
+++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj
@@ -2,6 +2,7 @@
+ $(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
{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}
@@ -11,6 +12,7 @@
+ $(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
net11.0
false
true
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index 9bf893a2..a0a14488 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -2,10 +2,12 @@
+ $(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
{960295EE-4AF4-4440-A525-B4C295B01A61}
+ $(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
net11.0
false
true
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 61a9d262..bd9cf04b 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -2,10 +2,12 @@
+ $(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
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}
+ $(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
Jellyfin Contributors
Jellyfin.Model
10.12.0
@@ -14,6 +16,7 @@
+ $(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
net11.0
false
true
@@ -24,10 +27,12 @@
+ $(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
false
+ $(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
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index e5059aa7..194ddd1d 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -2,6 +2,7 @@
+ $(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
{442B5058-DCAF-4263-BB6A-F21E31120A1B}
@@ -28,6 +29,7 @@
+ $(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
net11.0
false
true
diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
index a513ef79..0d705c1b 100644
--- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
+++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj
@@ -2,6 +2,7 @@
+ $(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
{23499896-B135-4527-8574-C26E926EA99E}
@@ -15,6 +16,7 @@
+ $(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
net11.0
false
true
diff --git a/WEBSOCKET_AUTHENTICATION.md b/WEBSOCKET_AUTHENTICATION.md
new file mode 100644
index 00000000..8689ff95
--- /dev/null
+++ b/WEBSOCKET_AUTHENTICATION.md
@@ -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(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)
diff --git a/docs/LIBRARY_OPTIONS_DB_DESIGN.md b/docs/LIBRARY_OPTIONS_DB_DESIGN.md
new file mode 100644
index 00000000..3683e9d3
--- /dev/null
+++ b/docs/LIBRARY_OPTIONS_DB_DESIGN.md
@@ -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` 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 (c) PlaceholderCompany. All rights reserved.
+//
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Postgres.Migrations
+{
+ ///
+ public partial class AddLibraryOptionsJsonStore : Migration
+ {
+ ///
+ 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);
+ ");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryOptions"" CASCADE;");
+ }
+ }
+}
+```
+
+### Suggested Entity
+
+```csharp
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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 LibraryOptions => this.Set();
+```
+
+Add to `PostgresDatabaseProvider.ConfigureEntities(...)`:
+
+```csharp
+modelBuilder.Entity().ToTable("LibraryOptions", Schemas.Library);
+```
+
+Recommended extra configuration:
+
+```csharp
+modelBuilder.Entity()
+ .HasIndex(e => e.LibraryPath)
+ .IsUnique();
+```
+
+### Repository Sketch
+
+This sketch follows the `MediaStreamRepository` and `BaseItemRepository` conventions already used in the codebase.
+
+```csharp
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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 _dbProvider;
+ private readonly IServerApplicationHost _appHost;
+
+ public LibraryOptionsRepository(IDbContextFactory dbProvider, IServerApplicationHost appHost)
+ {
+ _dbProvider = dbProvider;
+ _appHost = appHost;
+ }
+
+ public async Task 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(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(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 (c) PlaceholderCompany. All rights reserved.
+//
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Postgres.Migrations
+{
+ ///
+ public partial class AddLibraryOptionsHybridStore : Migration
+ {
+ ///
+ 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"");
+ ");
+ }
+
+ ///
+ 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 (c) PlaceholderCompany. All rights reserved.
+//
+
+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 MediaPaths { get; set; } = new List();
+}
+```
+
+```csharp
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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 LibraryOptions => this.Set();
+
+public DbSet LibraryMediaPaths => this.Set();
+```
+
+Add to `PostgresDatabaseProvider.ConfigureEntities(...)`:
+
+```csharp
+modelBuilder.Entity().ToTable("LibraryOptions", Schemas.Library);
+modelBuilder.Entity().ToTable("LibraryMediaPaths", Schemas.Library);
+
+modelBuilder.Entity()
+ .HasMany(e => e.MediaPaths)
+ .WithOne(e => e.LibraryOptions)
+ .HasForeignKey(e => e.LibraryId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+modelBuilder.Entity()
+ .HasIndex(e => e.LibraryPath)
+ .IsUnique();
+
+modelBuilder.Entity()
+ .HasIndex(e => new { e.LibraryId, e.Position })
+ .IsUnique();
+```
+
+### Repository Sketch
+
+```csharp
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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 _dbProvider;
+ private readonly IServerApplicationHost _appHost;
+
+ public LibraryOptionsRepository(IDbContextFactory dbProvider, IServerApplicationHost appHost)
+ {
+ _dbProvider = dbProvider;
+ _appHost = appHost;
+ }
+
+ public async Task 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(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(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 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.
\ No newline at end of file
diff --git a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
index 173be157..c78dfa0c 100644
--- a/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
+++ b/fuzz/Emby.Server.Implementations.Fuzz/Emby.Server.Implementations.Fuzz.csproj
@@ -1,6 +1,7 @@
+ $(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
Exe
net11.0
diff --git a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
index 59e1346a..8edf5f11 100644
--- a/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
+++ b/fuzz/Jellyfin.Api.Fuzz/Jellyfin.Api.Fuzz.csproj
@@ -1,6 +1,7 @@
+ $(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
Exe
net11.0
diff --git a/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj
index 5f6c9643..2da59180 100644
--- a/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj
+++ b/src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj
@@ -1,6 +1,7 @@
+ $(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
netstandard2.0
latest
false
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DATABASE_MIGRATION.md b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DATABASE_MIGRATION.md
new file mode 100644
index 00000000..768c86fa
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DATABASE_MIGRATION.md
@@ -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
+
+
+ Jellyfin-PostgreSQL
+ NoLock
+
+ Jellyfin-PostgreSQL
+ ...
+
+
+```
+
+### 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
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs
index 339b88b1..9889ed6e 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs
@@ -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;
///
/// 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.
///
- [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 for array deserialization")]
public List? PresetConfigurations { get; set; }
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.json.example b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.json.example
new file mode 100644
index 00000000..77baf7ea
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.json.example
@@ -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"
+ }
+ }
+ ]
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LibraryOptionsEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LibraryOptionsEntity.cs
new file mode 100644
index 00000000..08cc6415
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LibraryOptionsEntity.cs
@@ -0,0 +1,38 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+using System;
+
+///
+/// Stores serialized collection-folder library options for database-backed persistence.
+///
+public class LibraryOptionsEntity
+{
+ ///
+ /// Gets or sets the collection folder path used as the prototype key.
+ ///
+ public required string LibraryPath { get; set; }
+
+ ///
+ /// Gets or sets the serialized library options JSON payload.
+ ///
+ public required string OptionsJson { get; set; }
+
+ ///
+ /// Gets or sets the schema version for the serialized payload.
+ ///
+ public int Version { get; set; }
+
+ ///
+ /// Gets or sets the UTC timestamp when the row was created.
+ ///
+ public DateTime DateCreated { get; set; }
+
+ ///
+ /// Gets or sets the UTC timestamp when the row was last modified.
+ ///
+ public DateTime DateModified { get; set; }
+}
\ No newline at end of file
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
index 04faed14..fd664c3c 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
@@ -1,6 +1,7 @@
+ $(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
net11.0
false
true
@@ -11,6 +12,7 @@
+ $(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
Jellyfin Contributors
Jellyfin.Database.Implementations
10.11.0
@@ -19,6 +21,7 @@
+ $(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
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
index 13a6b002..4ef3bf02 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
@@ -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 options, ILog
///
public DbSet KeyframeData => this.Set();
+ ///
+ /// Gets the containing persisted library options.
+ ///
+ public DbSet LibraryOptions => this.Set();
+
/*public DbSet Artwork => Set();
public DbSet Books => Set();
@@ -291,12 +297,14 @@ public class JellyfinDbContext(DbContextOptions 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
{
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LibraryOptionsEntityConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LibraryOptionsEntityConfiguration.cs
new file mode 100644
index 00000000..bace6296
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LibraryOptionsEntityConfiguration.cs
@@ -0,0 +1,30 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+using System;
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+///
+/// LibraryOptionsEntity Configuration.
+///
+public class LibraryOptionsEntityConfiguration : IEntityTypeConfiguration
+{
+ ///
+ public void Configure(EntityTypeBuilder 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();
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj
index 8f508f16..a6c95114 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj
@@ -4,6 +4,7 @@
net11.0
false
true
+ $(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
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260501000000_AddLibraryOptionsTable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260501000000_AddLibraryOptionsTable.cs
new file mode 100644
index 00000000..dfb1f3f9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260501000000_AddLibraryOptionsTable.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Postgres.Migrations
+{
+ ///
+ public partial class AddLibraryOptionsTable : Migration
+ {
+ ///
+ 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);
+ ");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql(@"DROP TABLE IF EXISTS library.""LibraryOptions"" CASCADE;");
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
index f98bbcd8..e560e160 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
@@ -808,6 +808,10 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
modelBuilder.Entity().ToTable("TrickplayInfos", Schemas.Library);
modelBuilder.Entity().ToTable("MediaSegments", Schemas.Library);
modelBuilder.Entity().ToTable("KeyframeData", Schemas.Library);
+ modelBuilder.Entity().ToTable("LibraryOptions", Schemas.Library);
+
+ modelBuilder.Entity().HasKey(e => e.LibraryPath);
+ modelBuilder.Entity().HasIndex(e => e.DateModified);
}
///
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj
index ccc846b4..5e85cc11 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Jellyfin.Database.Providers.Sqlite.csproj
@@ -4,6 +4,7 @@
net11.0
false
true
+ $(NoWarn);CA1062;CA1861;CA1873;CA1848;CA2253;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260501000000_AddLibraryOptionsTable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260501000000_AddLibraryOptionsTable.cs
new file mode 100644
index 00000000..a89cd37f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260501000000_AddLibraryOptionsTable.cs
@@ -0,0 +1,47 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.Sqlite.Migrations
+{
+ using System;
+ using Microsoft.EntityFrameworkCore.Migrations;
+
+ ///
+ public partial class AddLibraryOptionsTable : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "LibraryOptions",
+ columns: table => new
+ {
+ LibraryPath = table.Column(type: "TEXT", nullable: false),
+ OptionsJson = table.Column(type: "TEXT", nullable: false),
+ Version = table.Column(type: "INTEGER", nullable: false, defaultValue: 1),
+ DateCreated = table.Column(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"),
+ DateModified = table.Column(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 });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "LibraryOptions");
+ }
+ }
+}
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index d3227a12..f96053ce 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -10,7 +10,7 @@
false
true
- NU1903
+ $(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
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index 78d5775f..b6c566f8 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -9,6 +9,7 @@
net11.0
false
true
+ $(NoWarn);CA1062;CA1031;CA1848;CA1873;CA1822;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 15c98cbb..f5a8d45b 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -9,7 +9,7 @@
true
snupkg
- NU5104
+ NU5104;CA1848;CA1515;CA1812;CA1849;CA1860;CA1867;CA1517;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
index bfc07101..f274df54 100644
--- a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
+++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj
@@ -2,6 +2,7 @@
net11.0
true
+ $(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
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index 8980c68a..2d63d926 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -1,6 +1,7 @@
+ $(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
net11.0
true
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 093af2b4..d5dc105b 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -1,11 +1,13 @@
+ $(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
net11.0
true
+ $(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
Jellyfin Contributors
Jellyfin.MediaEncoding.Keyframes
10.11.0
@@ -14,6 +16,7 @@
+ $(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
$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
diff --git a/src/Jellyfin.Networking/Jellyfin.Networking.csproj b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
index ee159829..a8631fa0 100644
--- a/src/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/src/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -1,5 +1,6 @@
+ $(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
net11.0
false
true
diff --git a/tests/Emby.Server.Implementations.Tests/AppBase/ConfigurationHelperTests.cs b/tests/Emby.Server.Implementations.Tests/AppBase/ConfigurationHelperTests.cs
new file mode 100644
index 00000000..c8a5e61d
--- /dev/null
+++ b/tests/Emby.Server.Implementations.Tests/AppBase/ConfigurationHelperTests.cs
@@ -0,0 +1,203 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+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;
+
+///
+/// Tests for the ConfigurationHelper JSON loading functionality.
+///
+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(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(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(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(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(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(jsonPath);
+
+ // Modify and verify round-trip
+ var json = JsonSerializer.Serialize(originalConfig, JsonDefaults.Options);
+ File.WriteAllText(jsonPath, json);
+
+ var result2 = ConfigurationHelper.GetJsonConfiguration(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);
+ }
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index deb81648..5c6ae5d0 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}
net11.0
@@ -27,3 +28,4 @@
+
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 0939b08a..7b2c2b00 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{DF194677-DFD3-42AF-9F75-D44D5A416478}
net11.0
@@ -24,3 +25,4 @@
+
diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
index ee30ed70..a4974667 100644
--- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
+++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{462584F7-5023-4019-9EAC-B98CA458C0A0}
net11.0
true
@@ -24,3 +25,4 @@
+
diff --git a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
index ba098f84..4c92bd7b 100644
--- a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
+#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
diff --git a/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs b/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs
index e9fa456a..00f8b42f 100644
--- a/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/FileHelperTests.cs
@@ -2,6 +2,8 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
+#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
diff --git a/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs
index 916b8213..7416af8d 100644
--- a/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/FormattingStreamWriterTests.cs
@@ -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))
- {
- txt.Write("{0}", 3.14159);
- txt.Close();
- Assert.Equal("3.14159", Encoding.UTF8.GetString(ms.ToArray()));
- }
+ 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()));
}
}
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index f41cfe03..8c007377 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -3,7 +3,7 @@
net11.0
true
- $(NoWarn);CS1591;CS1570;CS8600;SA1600;SA1309;SA1200
+ $(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
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
index ce9a2c84..d8765e26 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonBoolNumberTests.cs
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
+#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
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs
index c8a43402..d36f222f 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedCollectionTests.cs
@@ -2,6 +2,9 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
+#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
diff --git a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
index 6e32f867..2474cccf 100644
--- a/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
+++ b/tests/Jellyfin.Extensions.Tests/Json/Converters/JsonCommaDelimitedIReadOnlyListTests.cs
@@ -2,6 +2,8 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
+#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
diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
index b13da9ee..15211010 100644
--- a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
+++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj
@@ -1,5 +1,6 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -28,3 +29,4 @@
+
diff --git a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
index 0e57a491..981d9114 100644
--- a/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Hls.Tests/Jellyfin.MediaEncoding.Hls.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -24,3 +25,4 @@
+
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/FfProbe/FfProbeKeyframeExtractorTests.cs b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/FfProbe/FfProbeKeyframeExtractorTests.cs
index 8f1188b8..42cfd432 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/FfProbe/FfProbeKeyframeExtractorTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/FfProbe/FfProbeKeyframeExtractorTests.cs
@@ -2,10 +2,11 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
//
-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);
diff --git a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
index 476b042c..d4679fe6 100644
--- a/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Keyframes.Tests/Jellyfin.MediaEncoding.Keyframes.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -38,3 +39,4 @@
+
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
index 13f40546..3dd97b83 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
+++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{28464062-0939-4AA7-9F7B-24DDDA61A7C0}
net11.0
@@ -32,3 +33,4 @@
+
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index 9db23036..09cfc9ce 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -2,6 +2,7 @@
net11.0
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1055;CA1054;CA1308;CA2007;CA1822;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127
diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
index 3c9cc46a..991cada5 100644
--- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
+++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1822;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007
{3998657B-1CCC-49DD-A19F-275DC8495F57}
net11.0
@@ -23,3 +24,4 @@
+
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index a8aa9d8c..f69c052e 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -2,6 +2,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}
net11.0
@@ -24,3 +25,4 @@
+
diff --git a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
index 1c56625a..2913956c 100644
--- a/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
+++ b/tests/Jellyfin.Providers.Tests/Jellyfin.Providers.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -33,3 +34,4 @@
+
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs
index 74c60aec..d5ff2a8e 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Item/BaseItemRepositoryTests.cs
@@ -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;
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index 4a20996f..269daaa1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -2,11 +2,13 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}
net11.0
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
E:/Projects/jellyfin-web
@@ -43,3 +45,4 @@
+
diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
index c087daee..dd2e4f34 100644
--- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
+++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -43,3 +44,4 @@
+
diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
index f8ff5c05..fa802c37 100644
--- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
+++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -25,3 +26,4 @@
+
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
index a4e38f17..51a614c8 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Jellyfin.XbmcMetadata.Tests.csproj
@@ -1,6 +1,7 @@
+ $(NoWarn);CA1707;CA1861;CA1515;CA1062;CA5394;CA1848;CA1865;CA1869;CA1716;CA2101;SA1127;CA1845;CA1859;CA2007;CA2234;CA1812;CA1822;CA2000
net11.0
@@ -28,3 +29,4 @@
+