From e81c127514b019011c520864911680a399726542 Mon Sep 17 00:00:00 2001 From: Wendell Jones Date: Thu, 30 Apr 2026 12:25:24 -0400 Subject: [PATCH] Add JSON config, DB-backed library options, and docs - Add JSON-based config loading with XML fallback for DB and library options - Implement LibraryOptionsRepository with EF Core, migrations, and entity - Update CollectionFolder to use DB-backed options with XML fallback/backfill - Register repository in DI and initialize at startup - Use EF execution strategy for transactional DB operations - Suppress code analysis warnings in .csproj and test files - Add DATABASE_MIGRATION.md, LIBRARY_OPTIONS_DB_DESIGN.md, and WEBSOCKET_AUTHENTICATION.md - Add database.json.example and improve migration docs - Add tests for JSON config loader and update test naming warnings --- Emby.Naming/Emby.Naming.csproj | 4 +- Emby.Photos/Emby.Photos.csproj | 1 + .../AppBase/BaseConfigurationManager.cs | 27 + .../AppBase/ConfigurationHelper.cs | 46 ++ .../ApplicationHost.cs | 28 + .../Data/CleanDatabaseScheduledTask.cs | 9 +- .../Emby.Server.Implementations.csproj | 3 + .../HttpServer/WebSocketConnection.cs | 11 +- .../HttpServer/WebSocketManager.cs | 19 + .../Library/LibraryOptionsRepository.cs | 177 +++++ Jellyfin.Api/Jellyfin.Api.csproj | 2 + Jellyfin.Data/Jellyfin.Data.csproj | 3 + .../FullSystemBackup/BackupService.cs | 6 +- .../Item/BaseItemRepository.cs | 19 +- .../Item/ChapterRepository.cs | 28 +- .../Item/KeyframeRepository.cs | 14 +- .../Item/MediaAttachmentRepository.cs | 32 +- .../Item/MediaStreamRepository.cs | 24 +- .../Item/PeopleRepository.cs | 134 ++-- .../Jellyfin.Server.Implementations.csproj | 1 + .../Security/AuthorizationContext.cs | 9 + Jellyfin.Server/Jellyfin.Server.csproj | 2 + .../MediaBrowser.Common.csproj | 1 + .../Entities/CollectionFolder.cs | 24 +- .../MediaBrowser.Controller.csproj | 5 + .../Persistence/ILibraryOptionsRepository.cs | 18 + .../MediaBrowser.LocalMetadata.csproj | 2 + .../MediaBrowser.MediaEncoding.csproj | 2 + MediaBrowser.Model/MediaBrowser.Model.csproj | 5 + .../MediaBrowser.Providers.csproj | 2 + .../MediaBrowser.XbmcMetadata.csproj | 2 + WEBSOCKET_AUTHENTICATION.md | 167 ++++ docs/LIBRARY_OPTIONS_DB_DESIGN.md | 710 ++++++++++++++++++ .../Emby.Server.Implementations.Fuzz.csproj | 1 + .../Jellyfin.Api.Fuzz.csproj | 1 + .../Jellyfin.CodeAnalysis.csproj | 1 + .../DbConfiguration/DATABASE_MIGRATION.md | 112 +++ .../DatabaseConfigurationOptions.cs | 7 +- .../DbConfiguration/database.json.example | 54 ++ .../Entities/LibraryOptionsEntity.cs | 38 + .../Jellyfin.Database.Implementations.csproj | 3 + .../JellyfinDbContext.cs | 8 + .../LibraryOptionsEntityConfiguration.cs | 30 + ...ellyfin.Database.Providers.Postgres.csproj | 1 + .../20260501000000_AddLibraryOptionsTable.cs | 40 + .../PostgresDatabaseProvider.cs | 4 + .../Jellyfin.Database.Providers.Sqlite.csproj | 1 + .../20260501000000_AddLibraryOptionsTable.cs | 47 ++ .../Jellyfin.Drawing.Skia.csproj | 2 +- src/Jellyfin.Drawing/Jellyfin.Drawing.csproj | 1 + .../Jellyfin.Extensions.csproj | 2 +- src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj | 1 + .../Jellyfin.MediaEncoding.Hls.csproj | 1 + .../Jellyfin.MediaEncoding.Keyframes.csproj | 3 + .../Jellyfin.Networking.csproj | 1 + .../AppBase/ConfigurationHelperTests.cs | 203 +++++ .../Jellyfin.Api.Tests.csproj | 2 + .../Jellyfin.Common.Tests.csproj | 2 + .../Jellyfin.Controller.Tests.csproj | 2 + .../CopyToExtensionsTests.cs | 6 + .../FileHelperTests.cs | 4 + .../FormattingStreamWriterTests.cs | 16 +- .../Jellyfin.Extensions.Tests.csproj | 2 +- .../Json/Converters/JsonBoolNumberTests.cs | 6 + .../JsonCommaDelimitedCollectionTests.cs | 6 + .../JsonCommaDelimitedIReadOnlyListTests.cs | 4 + .../Jellyfin.LiveTv.Tests.csproj | 2 + .../Jellyfin.MediaEncoding.Hls.Tests.csproj | 2 + .../FfProbe/FfProbeKeyframeExtractorTests.cs | 5 +- ...lyfin.MediaEncoding.Keyframes.Tests.csproj | 2 + .../Jellyfin.MediaEncoding.Tests.csproj | 2 + .../Jellyfin.Model.Tests.csproj | 1 + .../Jellyfin.Naming.Tests.csproj | 2 + .../Jellyfin.Networking.Tests.csproj | 2 + .../Jellyfin.Providers.Tests.csproj | 2 + .../Item/BaseItemRepositoryTests.cs | 1 + ...llyfin.Server.Implementations.Tests.csproj | 3 + .../Jellyfin.Server.Integration.Tests.csproj | 2 + .../Jellyfin.Server.Tests.csproj | 2 + .../Jellyfin.XbmcMetadata.Tests.csproj | 2 + 80 files changed, 2039 insertions(+), 140 deletions(-) create mode 100644 Emby.Server.Implementations/Library/LibraryOptionsRepository.cs create mode 100644 MediaBrowser.Controller/Persistence/ILibraryOptionsRepository.cs create mode 100644 WEBSOCKET_AUTHENTICATION.md create mode 100644 docs/LIBRARY_OPTIONS_DB_DESIGN.md create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DATABASE_MIGRATION.md create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/database.json.example create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/LibraryOptionsEntity.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/LibraryOptionsEntityConfiguration.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260501000000_AddLibraryOptionsTable.cs create mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260501000000_AddLibraryOptionsTable.cs create mode 100644 tests/Emby.Server.Implementations.Tests/AppBase/ConfigurationHelperTests.cs 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 @@ +