From 5165e4e2d421b2358582f691090ceb4bb220745e Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 13 May 2026 13:46:46 +0200 Subject: [PATCH 01/63] Order chapter response by start time --- .../Item/ChapterRepository.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs index 98700f3224..f7d76517e1 100644 --- a/Jellyfin.Server.Implementations/Item/ChapterRepository.cs +++ b/Jellyfin.Server.Implementations/Item/ChapterRepository.cs @@ -55,6 +55,7 @@ public class ChapterRepository : IChapterRepository { using var context = _dbProvider.CreateDbContext(); return context.Chapters.AsNoTracking().Where(e => e.ItemId.Equals(baseItemId)) + .OrderBy(e => e.StartPositionTicks) .Select(e => new { chapter = e, @@ -69,18 +70,16 @@ public class ChapterRepository : IChapterRepository public void SaveChapters(Guid itemId, IReadOnlyList chapters) { using var context = _dbProvider.CreateDbContext(); - using (var transaction = context.Database.BeginTransaction()) + using var transaction = context.Database.BeginTransaction(); + context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); + for (var i = 0; i < chapters.Count; i++) { - context.Chapters.Where(e => e.ItemId.Equals(itemId)).ExecuteDelete(); - for (var i = 0; i < chapters.Count; i++) - { - var chapter = chapters[i]; - context.Chapters.Add(Map(chapter, i, itemId)); - } - - context.SaveChanges(); - transaction.Commit(); + var chapter = chapters[i]; + context.Chapters.Add(Map(chapter, i, itemId)); } + + context.SaveChanges(); + transaction.Commit(); } /// From 38bda65ca9a5994d8e8feb0cd239d88e080a90b8 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 13 May 2026 13:47:07 +0200 Subject: [PATCH 02/63] Properly handle cancellation in MediaSegmentManager --- .../MediaSegments/MediaSegmentManager.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 249df476a0..61c028b685 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -81,6 +81,11 @@ public class MediaSegmentManager : IMediaSegmentManager foreach (var provider in providers) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + if (!await provider.Supports(baseItem).ConfigureAwait(false)) { _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {MediaPath}", provider.Name, baseItem.Path); @@ -146,6 +151,15 @@ public class MediaSegmentManager : IMediaSegmentManager await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); } } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) when (cancellationToken.IsCancellationRequested) + { + _logger.LogDebug(ex, "Provider {ProviderName} aborted segment extraction for {MediaPath} due to shutdown", provider.Name, baseItem.Path); + break; + } catch (Exception ex) { _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); From e811cd7caf434acd84645af8bd05248b54c65c39 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 13 May 2026 13:47:56 +0200 Subject: [PATCH 03/63] Prevent unecessary log spam in NetworkUtils --- MediaBrowser.Common/Net/NetworkUtils.cs | 9 ++- .../NetworkParseTests.cs | 59 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 71539b8b78..25a1022d4e 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -180,9 +180,16 @@ public static partial class NetworkUtils List? tmpResult = null; for (int a = 0; a < values.Length; a++) { + // Skip entries whose '!' polarity doesn't match this pass + var trimmed = values[a].AsSpan().Trim(); + if (trimmed.StartsWith('!') != negated) + { + continue; + } + if (TryParseToSubnet(values[a], out var innerResult, negated)) { - (tmpResult ??= new()).Add(innerResult); + (tmpResult ??= []).Add(innerResult); } else { diff --git a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs index 66eec077dc..1f523f7f21 100644 --- a/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs +++ b/tests/Jellyfin.Networking.Tests/NetworkParseTests.cs @@ -135,6 +135,65 @@ namespace Jellyfin.Networking.Tests Times.Once); } + /// + /// Verifies that IPv4 entries whose '!' polarity doesn't match the requested pass are skipped silently, + /// not logged as invalid. Callers parse the same list twice (LAN and excluded) so the off-polarity + /// entries are expected, not erroneous. + /// + [Fact] + public static void TryParseToSubnets_PolarityMismatchIPv4_DoesNotWarn() + { + var logger = new Mock(); + var values = new[] { "127.0.0.0/8", "192.168.178.0/24", "!10.0.0.0/8" }; + + // Non-negated pass picks up the two non-'!' entries and ignores '!10.0.0.0/8' silently. + Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object)); + Assert.NotNull(lanResult); + Assert.Equal(2, lanResult.Count); + + // Negated pass picks up the single '!' entry and ignores the others silently. + Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object)); + Assert.NotNull(excludedResult); + Assert.Single(excludedResult); + + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + + /// + /// Same as the IPv4 case but for IPv6 entries — makes sure the polarity pre-check works + /// for IPv6 CIDR notation (with '::') as well. + /// + [Fact] + public static void TryParseToSubnets_PolarityMismatchIPv6_DoesNotWarn() + { + var logger = new Mock(); + var values = new[] { "fd00::/8", "fe80::/10", "!fd12:3456:789a::/48" }; + + Assert.True(NetworkUtils.TryParseToSubnets(values, out var lanResult, false, logger.Object)); + Assert.NotNull(lanResult); + Assert.Equal(2, lanResult.Count); + + Assert.True(NetworkUtils.TryParseToSubnets(values, out var excludedResult, true, logger.Object)); + Assert.NotNull(excludedResult); + Assert.Single(excludedResult); + + logger.Verify( + l => l.Log( + LogLevel.Warning, + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>()), + Times.Never); + } + /// /// Checks if IPv4 address is within a defined subnet. /// From 1751c5b45d7022f662b04b565c4f70a7ce2329d1 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 14 May 2026 07:42:50 +0200 Subject: [PATCH 04/63] Fixup --- .../MediaSegments/MediaSegmentManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 61c028b685..be98f93dab 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -81,10 +81,7 @@ public class MediaSegmentManager : IMediaSegmentManager foreach (var provider in providers) { - if (cancellationToken.IsCancellationRequested) - { - break; - } + cancellationToken.ThrowIfCancellationRequested(); if (!await provider.Supports(baseItem).ConfigureAwait(false)) { From 09e607db9948a3563fe7ed0f85e0cdd9497be379 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 15 May 2026 11:55:49 +0200 Subject: [PATCH 05/63] Fix integrated provider images --- .gitignore | 1 + .../Plugins/PluginManager.cs | 3 +- Jellyfin.Api/Controllers/PluginsController.cs | 28 ++++++++--- MediaBrowser.Common/Plugins/LocalPlugin.cs | 2 +- MediaBrowser.Common/Plugins/PluginManifest.cs | 9 ++++ .../Plugins/IHasEmbeddedImage.cs | 17 +++++++ .../MediaBrowser.Providers.csproj | 47 +++++++++++++++++- .../Plugins/AudioDb/Plugin.cs | 5 +- .../Plugins/AudioDb/jellyfin-plugin-tadb.svg | 1 + .../Configuration/PluginConfiguration.cs | 10 ++++ .../Plugins/ComicVine/Plugin.cs | 45 +++++++++++++++++ .../ComicVine/jellyfin-plugin-comicvine.svg | 16 ++++++ .../Configuration/PluginConfiguration.cs | 10 ++++ .../Plugins/GoogleBooks/Plugin.cs | 45 +++++++++++++++++ .../jellyfin-plugin-googlebooks.svg | 18 +++++++ .../ListenBrainz/ListenBrainzPlugin.cs | 11 ++-- .../Plugins/MusicBrainz/Plugin.cs | 6 ++- .../jellyfin-plugin-musicbrainz.svg | 36 ++++++++++++++ MediaBrowser.Providers/Plugins/Omdb/Plugin.cs | 5 +- .../Plugins/Omdb/jellyfin-plugin-omdb.png | Bin 0 -> 67173 bytes .../Plugins/StudioImages/Plugin.cs | 6 ++- .../jellyfin-plugin-studioimages.svg | 1 + MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs | 6 ++- .../Plugins/Tmdb/jellyfin-plugin-tmdb.svg | 16 ++++++ 24 files changed, 323 insertions(+), 21 deletions(-) create mode 100644 MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs create mode 100644 MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs create mode 100644 MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg create mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs create mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs create mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg create mode 100644 MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png create mode 100644 MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg create mode 100644 MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg diff --git a/.gitignore b/.gitignore index d5a0367bff..e399f1fc47 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ local.properties .classpath .settings/ .loadpath +*.lscache # External tool builders .externalToolBuilders/ diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index 91ccb16ef9..f699c99d85 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -564,7 +564,8 @@ namespace Emby.Server.Implementations.Plugins Id = instance.Id, Status = PluginStatus.Active, Name = instance.Name, - Version = instance.Version.ToString() + Version = instance.Version.ToString(), + ImageResourceName = (instance as IHasEmbeddedImage)?.ImageResourceName }) { Instance = instance diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs index 79e6536fb6..0105ecf7a7 100644 --- a/Jellyfin.Api/Controllers/PluginsController.cs +++ b/Jellyfin.Api/Controllers/PluginsController.cs @@ -226,16 +226,32 @@ public class PluginsController : BaseJellyfinApiController return NotFound(); } - var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty); - if (plugin.Manifest.ImagePath is null || !System.IO.File.Exists(imagePath)) + if (!string.IsNullOrEmpty(plugin.Manifest.ImagePath)) { - return NotFound(); + var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); + if (!System.IO.File.Exists(imagePath)) + { + return NotFound(); + } + + Response.Headers.ContentDisposition = "attachment"; + return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); } - Response.Headers.ContentDisposition = "attachment"; + var resourceName = plugin.Manifest.ImageResourceName; + if (!string.IsNullOrEmpty(resourceName) && plugin.Instance is not null) + { + var stream = plugin.Instance.GetType().Assembly.GetManifestResourceStream(resourceName); + if (stream is null) + { + return NotFound(); + } - imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath); - return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath)); + Response.Headers.ContentDisposition = "attachment"; + return File(stream, MimeTypes.GetMimeType(resourceName)); + } + + return NotFound(); } /// diff --git a/MediaBrowser.Common/Plugins/LocalPlugin.cs b/MediaBrowser.Common/Plugins/LocalPlugin.cs index 96af423cc3..4723be1001 100644 --- a/MediaBrowser.Common/Plugins/LocalPlugin.cs +++ b/MediaBrowser.Common/Plugins/LocalPlugin.cs @@ -109,7 +109,7 @@ namespace MediaBrowser.Common.Plugins { var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true); inst.Status = Manifest.Status; - inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath); + inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath) || !string.IsNullOrEmpty(Manifest.ImageResourceName); return inst; } diff --git a/MediaBrowser.Common/Plugins/PluginManifest.cs b/MediaBrowser.Common/Plugins/PluginManifest.cs index e0847ccea4..e749e85899 100644 --- a/MediaBrowser.Common/Plugins/PluginManifest.cs +++ b/MediaBrowser.Common/Plugins/PluginManifest.cs @@ -107,6 +107,15 @@ namespace MediaBrowser.Common.Plugins [JsonPropertyName("imagePath")] public string? ImagePath { get; set; } + /// + /// Gets or sets the name of an embedded resource in the plugin's assembly + /// that should be served as the plugin image. + /// Used by bundled/integrated plugins whose images are shipped inside the assembly + /// rather than on disk. Ignored when is set. + /// + [JsonIgnore] + public string? ImageResourceName { get; set; } + /// /// Gets or sets the collection of assemblies that should be loaded. /// Paths are considered relative to the plugin folder. diff --git a/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs new file mode 100644 index 0000000000..4196cd9f24 --- /dev/null +++ b/MediaBrowser.Controller/Plugins/IHasEmbeddedImage.cs @@ -0,0 +1,17 @@ +namespace MediaBrowser.Controller.Plugins; + +/// +/// Marker interface for integrated/bundled plugins that ship their plugin image as an embedded +/// resource inside the plugin assembly rather than as a file on disk. +/// +/// +/// This interface is intended for plugins compiled into the server. External plugins should +/// continue to declare their image via the imagePath field in meta.json. +/// +public interface IHasEmbeddedImage +{ + /// + /// Gets the name of the embedded resource in this plugin's assembly to serve as the plugin image. + /// + string ImageResourceName { get; } +} diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 8b727a8cac..181cda3288 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -47,22 +47,65 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs index 6c2ad0573e..bb6b67286d 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.AudioDb { - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) @@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml"; + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tadb.svg"; + public IEnumerable GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg new file mode 100644 index 0000000000..94fa55cc9c --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/jellyfin-plugin-tadb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..d35639d7e8 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs @@ -0,0 +1,10 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.ComicVine; + +/// +/// Plugin configuration for the Comic Vine provider. +/// +public class PluginConfiguration : BasePluginConfiguration +{ +} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs b/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs new file mode 100644 index 0000000000..101fa103b0 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs @@ -0,0 +1,45 @@ +using System; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.ComicVine; + +/// +/// ComicVine plugin instance. +/// +public class Plugin : BasePlugin, IHasEmbeddedImage +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + /// + /// Gets the current plugin instance. + /// + public static Plugin? Instance { get; private set; } + + /// + public override Guid Id => new("3ade6fd1-c76c-4560-b2df-f6bca4c2332f"); + + /// + public override string Name => "Comic Vine"; + + /// + public override string Description => "Get external links for comic books from Comic Vine."; + + /// + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.ComicVine.xml"; + + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-comicvine.svg"; +} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg b/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg new file mode 100644 index 0000000000..81bde53a51 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs new file mode 100644 index 0000000000..c32f764810 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs @@ -0,0 +1,10 @@ +using MediaBrowser.Model.Plugins; + +namespace MediaBrowser.Providers.Plugins.GoogleBooks; + +/// +/// Plugin configuration for the Google Books provider. +/// +public class PluginConfiguration : BasePluginConfiguration +{ +} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs new file mode 100644 index 0000000000..645e27f5f9 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs @@ -0,0 +1,45 @@ +using System; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Serialization; + +namespace MediaBrowser.Providers.Plugins.GoogleBooks; + +/// +/// Google Books plugin instance. +/// +public class Plugin : BasePlugin, IHasEmbeddedImage +{ + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) + : base(applicationPaths, xmlSerializer) + { + Instance = this; + } + + /// + /// Gets the current plugin instance. + /// + public static Plugin? Instance { get; private set; } + + /// + public override Guid Id => new("9f97232d-e7f4-432d-92d3-c709ce47e30b"); + + /// + public override string Name => "Google Books"; + + /// + public override string Description => "Get external links for books from Google Books."; + + /// + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.GoogleBooks.xml"; + + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-googlebooks.svg"; +} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg b/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg new file mode 100644 index 0000000000..f93ca4a18e --- /dev/null +++ b/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs index efac93f94e..1681f0334d 100644 --- a/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs +++ b/MediaBrowser.Providers/Plugins/ListenBrainz/ListenBrainzPlugin.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.ListenBrainz.Configuration; @@ -11,7 +12,7 @@ namespace MediaBrowser.Providers.Plugins.ListenBrainz; /// /// ListenBrainz plugin instance. /// -public class ListenBrainzPlugin : BasePlugin, IHasWebPages +public class ListenBrainzPlugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Initializes a new instance of the class. @@ -41,6 +42,9 @@ public class ListenBrainzPlugin : BasePlugin, IHasWebPages /// public override string ConfigurationFileName => "Jellyfin.Plugin.ListenBrainz.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".Configuration.ListenBrainz_logo.svg"; + /// public IEnumerable GetPages() { @@ -51,11 +55,6 @@ public class ListenBrainzPlugin : BasePlugin, IHasWebPages EmbeddedResourcePath = resourcePrefix + "config.html" }; yield return new PluginPageInfo - { - Name = Name + "Logo", - EmbeddedResourcePath = resourcePrefix + "ListenBrainz_logo.svg" - }; - yield return new PluginPageInfo { Name = Name + "Notice", EmbeddedResourcePath = resourcePrefix + "NOTICE.md" diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index 69225d0b95..f448e6b20c 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -6,6 +6,7 @@ using System.Threading; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.MusicBrainz.Configuration; @@ -17,7 +18,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz; /// /// Plugin instance. /// -public class Plugin : BasePlugin, IHasWebPages, IDisposable +public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage, IDisposable { private readonly ILogger _logger; private readonly Lock _queryLock = new(); @@ -66,6 +67,9 @@ public class Plugin : BasePlugin, IHasWebPages, IDisposable // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-musicbrainz.svg"; + /// /// Gets the current MusicBrainz query client. /// diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg new file mode 100644 index 0000000000..8074d59d7b --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/jellyfin-plugin-musicbrainz.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs index a0fba48f05..9066ff8523 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Providers.Plugins.Omdb { - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) @@ -29,6 +30,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb // TODO remove when plugin removed from server. public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml"; + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-omdb.png"; + public IEnumerable GetPages() { yield return new PluginPageInfo diff --git a/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png b/MediaBrowser.Providers/Plugins/Omdb/jellyfin-plugin-omdb.png new file mode 100644 index 0000000000000000000000000000000000000000..a4bb7f7d5f071117a2e6c41213f7c0f0540e1ddb GIT binary patch literal 67173 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%0A1@3-wipn(u4h?TmljDC!NF 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

pZ0QoWFq>jvkOQrJpWMWt9f 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 literal 0 HcmV?d00001 diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs index 28f8c0c617..167959967d 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Plugins.StudioImages.Configuration; @@ -13,7 +14,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages ///

/// Artwork Plugin class. /// - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Artwork repository URL. @@ -50,6 +51,9 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-studioimages.svg"; + /// public IEnumerable GetPages() { diff --git a/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg new file mode 100644 index 0000000000..e9da69c571 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/StudioImages/jellyfin-plugin-studioimages.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs index 4adde8366a..04f1fd04a3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; +using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; @@ -12,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// /// Plugin class for the TMDb library. /// - public class Plugin : BasePlugin, IHasWebPages + public class Plugin : BasePlugin, IHasWebPages, IHasEmbeddedImage { /// /// Initializes a new instance of the class. @@ -44,6 +45,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml"; + /// + public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-tmdb.svg"; + /// /// Return the plugin configuration page. /// diff --git a/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg new file mode 100644 index 0000000000..fbebb32b60 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/jellyfin-plugin-tmdb.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + From f73fc1feb2f2204ac8c27b1162ec0724529f2d7c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 15 May 2026 19:03:08 +0000 Subject: [PATCH 06/63] Update filenaming scheme to match EFCore one --- ...20250420050000_DisableTranscodingThrottling.cs} | 0 ... 20250420060000_CreateUserLoggingConfigFile.cs} | 0 ...b.cs => 20250420070000_MigrateActivityLogDb.cs} | 0 ....cs => 20250420080000_RemoveDuplicateExtras.cs} | 0 ...> 20250420090000_AddDefaultPluginRepository.cs} | 0 ...teUserDb.cs => 20250420100000_MigrateUserDb.cs} | 0 ...20250420110000_ReaddDefaultPluginRepository.cs} | 0 ... 20250420120000_MigrateDisplayPreferencesDb.cs} | 0 ...0250420130000_RemoveDownloadImagesInAdvance.cs} | 0 ...s => 20250420140000_MigrateAuthenticationDb.cs} | 0 ...Owner.cs => 20250420150000_FixPlaylistOwner.cs} | 0 ...s => 20250420160000_AddDefaultCastReceivers.cs} | 0 ...0250420170000_UpdateDefaultPluginRepository.cs} | 0 ...AudioData.cs => 20250420180000_FixAudioData.cs} | 0 ...50420190000_RemoveDuplicatePlaylistChildren.cs} | 0 ...20193000_MigrateLibraryDbCompatibilityCheck.cs} | 0 ...aryDb.cs => 20250420200000_MigrateLibraryDb.cs} | 0 ...les.cs => 20250420210000_MoveExtractedFiles.cs} | 0 ...les.cs => 20250420230000_MoveTrickplayFiles.cs} | 0 ... 20250420230000_RefreshInternalDateModified.cs} | 0 ...ta.cs => 20250421000000_MigrateKeyframeData.cs} | 0 ...cs => 20250618010000_MigrateLibraryUserData.cs} | 0 .../{FixDates.cs => 20250620180000_FixDates.cs} | 0 ...rFlag.cs => 20250730215000_ReseedFolderFlag.cs} | 0 ...ames.cs => 20251008120000_RefreshCleanNames.cs} | 0 ...rtist.cs => 20251009200000_CleanMusicArtist.cs} | 0 ....cs => 20260113120000_MigrateLinkedChildren.cs} | 0 ....cs => 20260113230000_CleanupOrphanedExtras.cs} | 0 ...0115120000_FixIncorrectOwnerIdRelationships.cs} | 0 ...6200000_FixLibrarySubtitleDownloadLanguages.cs} | 0 ...ls.cs => 20260302090000_MigrateRatingLevels.cs} | 0 ...> 20260508120000_MergeDuplicateMusicArtists.cs} | 0 ...e.cs => 20260508130000_MergeDuplicatePeople.cs} | 14 ++++++++++---- 33 files changed, 10 insertions(+), 4 deletions(-) rename Jellyfin.Server/Migrations/Routines/{DisableTranscodingThrottling.cs => 20250420050000_DisableTranscodingThrottling.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{CreateUserLoggingConfigFile.cs => 20250420060000_CreateUserLoggingConfigFile.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateActivityLogDb.cs => 20250420070000_MigrateActivityLogDb.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{RemoveDuplicateExtras.cs => 20250420080000_RemoveDuplicateExtras.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{AddDefaultPluginRepository.cs => 20250420090000_AddDefaultPluginRepository.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateUserDb.cs => 20250420100000_MigrateUserDb.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{ReaddDefaultPluginRepository.cs => 20250420110000_ReaddDefaultPluginRepository.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateDisplayPreferencesDb.cs => 20250420120000_MigrateDisplayPreferencesDb.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{RemoveDownloadImagesInAdvance.cs => 20250420130000_RemoveDownloadImagesInAdvance.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateAuthenticationDb.cs => 20250420140000_MigrateAuthenticationDb.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{FixPlaylistOwner.cs => 20250420150000_FixPlaylistOwner.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{AddDefaultCastReceivers.cs => 20250420160000_AddDefaultCastReceivers.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{UpdateDefaultPluginRepository.cs => 20250420170000_UpdateDefaultPluginRepository.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{FixAudioData.cs => 20250420180000_FixAudioData.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{RemoveDuplicatePlaylistChildren.cs => 20250420190000_RemoveDuplicatePlaylistChildren.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateLibraryDbCompatibilityCheck.cs => 20250420193000_MigrateLibraryDbCompatibilityCheck.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateLibraryDb.cs => 20250420200000_MigrateLibraryDb.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MoveExtractedFiles.cs => 20250420210000_MoveExtractedFiles.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MoveTrickplayFiles.cs => 20250420230000_MoveTrickplayFiles.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{RefreshInternalDateModified.cs => 20250420230000_RefreshInternalDateModified.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateKeyframeData.cs => 20250421000000_MigrateKeyframeData.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateLibraryUserData.cs => 20250618010000_MigrateLibraryUserData.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{FixDates.cs => 20250620180000_FixDates.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{ReseedFolderFlag.cs => 20250730215000_ReseedFolderFlag.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{RefreshCleanNames.cs => 20251008120000_RefreshCleanNames.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{CleanMusicArtist.cs => 20251009200000_CleanMusicArtist.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateLinkedChildren.cs => 20260113120000_MigrateLinkedChildren.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{CleanupOrphanedExtras.cs => 20260113230000_CleanupOrphanedExtras.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{FixIncorrectOwnerIdRelationships.cs => 20260115120000_FixIncorrectOwnerIdRelationships.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{FixLibrarySubtitleDownloadLanguages.cs => 20260206200000_FixLibrarySubtitleDownloadLanguages.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MigrateRatingLevels.cs => 20260302090000_MigrateRatingLevels.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MergeDuplicateMusicArtists.cs => 20260508120000_MergeDuplicateMusicArtists.cs} (100%) rename Jellyfin.Server/Migrations/Routines/{MergeDuplicatePeople.cs => 20260508130000_MergeDuplicatePeople.cs} (96%) diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs rename to Jellyfin.Server/Migrations/Routines/20250420050000_DisableTranscodingThrottling.cs diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs rename to Jellyfin.Server/Migrations/Routines/20250420060000_CreateUserLoggingConfigFile.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420070000_MigrateActivityLogDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs rename to Jellyfin.Server/Migrations/Routines/20250420080000_RemoveDuplicateExtras.cs diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420090000_AddDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420100000_MigrateUserDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420110000_ReaddDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420120000_MigrateDisplayPreferencesDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs b/Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDownloadImagesInAdvance.cs rename to Jellyfin.Server/Migrations/Routines/20250420130000_RemoveDownloadImagesInAdvance.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs b/Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateAuthenticationDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420140000_MigrateAuthenticationDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs b/Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixPlaylistOwner.cs rename to Jellyfin.Server/Migrations/Routines/20250420150000_FixPlaylistOwner.cs diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs b/Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs rename to Jellyfin.Server/Migrations/Routines/20250420160000_AddDefaultCastReceivers.cs diff --git a/Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/UpdateDefaultPluginRepository.cs rename to Jellyfin.Server/Migrations/Routines/20250420170000_UpdateDefaultPluginRepository.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixAudioData.cs b/Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixAudioData.cs rename to Jellyfin.Server/Migrations/Routines/20250420180000_FixAudioData.cs diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs b/Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RemoveDuplicatePlaylistChildren.cs rename to Jellyfin.Server/Migrations/Routines/20250420190000_RemoveDuplicatePlaylistChildren.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs b/Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryDbCompatibilityCheck.cs rename to Jellyfin.Server/Migrations/Routines/20250420193000_MigrateLibraryDbCompatibilityCheck.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs rename to Jellyfin.Server/Migrations/Routines/20250420200000_MigrateLibraryDb.cs diff --git a/Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MoveExtractedFiles.cs rename to Jellyfin.Server/Migrations/Routines/20250420210000_MoveExtractedFiles.cs diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs rename to Jellyfin.Server/Migrations/Routines/20250420230000_MoveTrickplayFiles.cs diff --git a/Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs b/Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RefreshInternalDateModified.cs rename to Jellyfin.Server/Migrations/Routines/20250420230000_RefreshInternalDateModified.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs b/Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateKeyframeData.cs rename to Jellyfin.Server/Migrations/Routines/20250421000000_MigrateKeyframeData.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs b/Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLibraryUserData.cs rename to Jellyfin.Server/Migrations/Routines/20250618010000_MigrateLibraryUserData.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixDates.cs b/Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixDates.cs rename to Jellyfin.Server/Migrations/Routines/20250620180000_FixDates.cs diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs rename to Jellyfin.Server/Migrations/Routines/20250730215000_ReseedFolderFlag.cs diff --git a/Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs b/Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/RefreshCleanNames.cs rename to Jellyfin.Server/Migrations/Routines/20251008120000_RefreshCleanNames.cs diff --git a/Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs b/Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/CleanMusicArtist.cs rename to Jellyfin.Server/Migrations/Routines/20251009200000_CleanMusicArtist.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs rename to Jellyfin.Server/Migrations/Routines/20260113120000_MigrateLinkedChildren.cs diff --git a/Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/CleanupOrphanedExtras.cs rename to Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs b/Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs rename to Jellyfin.Server/Migrations/Routines/20260115120000_FixIncorrectOwnerIdRelationships.cs diff --git a/Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs b/Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/FixLibrarySubtitleDownloadLanguages.cs rename to Jellyfin.Server/Migrations/Routines/20260206200000_FixLibrarySubtitleDownloadLanguages.cs diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs rename to Jellyfin.Server/Migrations/Routines/20260302090000_MigrateRatingLevels.cs diff --git a/Jellyfin.Server/Migrations/Routines/MergeDuplicateMusicArtists.cs b/Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs similarity index 100% rename from Jellyfin.Server/Migrations/Routines/MergeDuplicateMusicArtists.cs rename to Jellyfin.Server/Migrations/Routines/20260508120000_MergeDuplicateMusicArtists.cs diff --git a/Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs similarity index 96% rename from Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs rename to Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs index d092555139..10433599fa 100644 --- a/Jellyfin.Server/Migrations/Routines/MergeDuplicatePeople.cs +++ b/Jellyfin.Server/Migrations/Routines/20260508130000_MergeDuplicatePeople.cs @@ -284,10 +284,16 @@ public class MergeDuplicatePeople : IAsyncMigrationRoutine return; } - await context.Peoples - .Where(p => idsToDelete.Contains(p.Id)) - .ExecuteDeleteAsync(cancellationToken) - .ConfigureAwait(false); + var idx = 0; + foreach (var item in idsToDelete.Chunk(200)) + { + idx++; // humans count at one + _logger.LogInformation("Remove batch {BatchNo}/{MaxBatches} duplicate Peoples.", idx, idsToDelete.Count / 200); + await context.Peoples + .Where(p => item.Contains(p.Id)) + .ExecuteDeleteAsync(cancellationToken) + .ConfigureAwait(false); + } _logger.LogInformation("Removed {Count} duplicate Peoples rows.", idsToDelete.Count); } From 9d20aefd89f5b7990f3d6809e4f49f10fad4d77f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 15 May 2026 20:10:33 +0000 Subject: [PATCH 07/63] Reorder migration handling for extra column --- .../Item/BaseItemMapper.cs | 2 +- Jellyfin.Server/GlobalSuppressions.cs | 8 + .../20260113230000_CleanupOrphanedExtras.cs | 58 +- ...3233000_AddForeignKeyToOwnerId.Designer.cs | 3 + .../20260113233000_AddForeignKeyToOwnerId.cs | 36 + ...60113233500_DropExtraIdsColumn.Designer.cs | 3 + ...dLatestItemsDateCreatedIndexes.Designer.cs | 3 + ...18182305_AddIndicesToImageInfo.Designer.cs | 3 + ...130232147_AddBaseItemNameIndex.Designer.cs | 3 + ...60206224832_IndexOptimizations.Designer.cs | 3 + ...4_ChangePrimaryVersionIdToGuid.Designer.cs | 3 + ...08123920_AddTypeCleanNameIndex.Designer.cs | 3 + ...5_AddPartialIndexForItemCounts.Designer.cs | 3 + ...0504180809_AddOriginalLanguage.Designer.cs | 1802 ----------------- .../20260504180809_AddOriginalLanguage.cs | 47 - 15 files changed, 95 insertions(+), 1885 deletions(-) create mode 100644 Jellyfin.Server/GlobalSuppressions.cs delete mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs delete mode 100644 src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs diff --git a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs index 736388e9eb..c64e6ac068 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemMapper.cs @@ -26,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Item; /// /// Handles mapping between BaseItemEntity (database) and BaseItemDto (domain) objects. /// -internal static class BaseItemMapper +public static class BaseItemMapper { /// /// This holds all the types in the running assemblies diff --git a/Jellyfin.Server/GlobalSuppressions.cs b/Jellyfin.Server/GlobalSuppressions.cs new file mode 100644 index 0000000000..676747e29f --- /dev/null +++ b/Jellyfin.Server/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Migration files should follow the EFCore standard in regards to naming.", Scope = "namespaceanddescendants", Target = "~N:Jellyfin.Server.Migrations.Routines")] diff --git a/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs index 14abaa7317..f4dfa49068 100644 --- a/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/20260113230000_CleanupOrphanedExtras.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; +using Jellyfin.Server.Implementations.Item; using Jellyfin.Server.Migrations.Stages; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Controller.Channels; @@ -23,7 +24,7 @@ namespace Jellyfin.Server.Migrations.Routines; /// Removes orphaned extras (items with OwnerId pointing to non-existent items). /// Must run before EF migrations that add FK constraints on OwnerId. /// -[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.CoreInitialisation)] +[JellyfinMigration("2026-01-13T23:00:00", nameof(CleanupOrphanedExtras), Stage = JellyfinMigrationStageTypes.AppInitialisation)] [JellyfinMigrationBackup(JellyfinDb = true)] public class CleanupOrphanedExtras : IAsyncMigrationRoutine { @@ -37,39 +38,14 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine /// The startup logger. /// The database context factory. /// The library manager. - /// The item repository. - /// The item count service. - /// The channel manager. - /// The recordings manager. - /// The media source manager. - /// The media segments manager. - /// The configuration manager. - /// The file system. public CleanupOrphanedExtras( IStartupLogger logger, IDbContextFactory dbContextFactory, - ILibraryManager libraryManager, - IItemRepository itemRepository, - IItemCountService itemCountService, - IChannelManager channelManager, - IRecordingsManager recordingsManager, - IMediaSourceManager mediaSourceManager, - IMediaSegmentManager mediaSegmentManager, - IServerConfigurationManager configurationManager, - IFileSystem fileSystem) + ILibraryManager libraryManager) { _logger = logger; _dbContextFactory = dbContextFactory; _libraryManager = libraryManager; - BaseItem.LibraryManager ??= libraryManager; - BaseItem.ItemRepository ??= itemRepository; - BaseItem.ItemCountService ??= itemCountService; - BaseItem.ChannelManager ??= channelManager; - BaseItem.MediaSourceManager ??= mediaSourceManager; - BaseItem.MediaSegmentManager ??= mediaSegmentManager; - BaseItem.ConfigurationManager ??= configurationManager; - BaseItem.FileSystem ??= fileSystem; - Video.RecordingsManager ??= recordingsManager; } /// @@ -78,12 +54,19 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); await using (context.ConfigureAwait(false)) { + var placeholderOwner = Guid.Parse("00000000-0000-0000-0000-000000000001"); +#pragma warning disable RS0030 // Do not use banned APIs var orphanedItemIds = await context.BaseItems - .Where(b => b.OwnerId.HasValue && !b.OwnerId.Value.Equals(Guid.Empty)) - .Where(b => !context.BaseItems.Any(parent => parent.Id.Equals(b.OwnerId!.Value))) - .Select(b => b.Id) + .Where(b => b.OwnerId.HasValue && b.OwnerId == placeholderOwner) + .Select(b => new + { + b.Id, + b.Path, + b.Type + }) .ToListAsync(cancellationToken) .ConfigureAwait(false); +#pragma warning restore RS0030 // Do not use banned APIs if (orphanedItemIds.Count == 0) { @@ -97,11 +80,16 @@ public class CleanupOrphanedExtras : IAsyncMigrationRoutine var itemsToDelete = new List(); foreach (var itemId in orphanedItemIds) { - var item = _libraryManager.GetItemById(itemId); - if (item is not null) - { - itemsToDelete.Add(item); - } + itemsToDelete.Add(BaseItemMapper.DeserializeBaseItem( + new Database.Implementations.Entities.BaseItemEntity() + { + Id = itemId.Id, + Path = itemId.Path, + Type = itemId.Type + }, + _logger, + null, + true)!); } _libraryManager.DeleteItemsUnsafeFast(itemsToDelete); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs index 0e28abc862..f9cb9aa736 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.Designer.cs @@ -270,6 +270,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs index c84086d992..388906c064 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs @@ -23,12 +23,40 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations name: "BaseItemEntityId", table: "BaseItems"); + migrationBuilder.Sql( + """ + UPDATE BaseItems + SET OwnerId = '00000000-0000-0000-0000-000000000001' + WHERE OwnerId IS NOT NULL + AND OwnerId NOT IN (SELECT Id FROM BaseItems); + """); + migrationBuilder.AddForeignKey( name: "FK_BaseItems_BaseItems_OwnerId", table: "BaseItems", column: "OwnerId", principalTable: "BaseItems", principalColumn: "Id"); + + migrationBuilder.AddColumn( + name: "IsOriginal", + table: "MediaStreamInfos", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "OriginalLanguage", + table: "BaseItems", + type: "TEXT", + nullable: true); + + migrationBuilder.UpdateData( + table: "BaseItems", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000001"), + column: "OriginalLanguage", + value: null); } /// @@ -62,6 +90,14 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations column: "BaseItemEntityId", principalTable: "BaseItems", principalColumn: "Id"); + + migrationBuilder.DropColumn( + name: "IsOriginal", + table: "MediaStreamInfos"); + + migrationBuilder.DropColumn( + name: "OriginalLanguage", + table: "BaseItems"); } } } diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs index 92ed0cf6bf..29874264af 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs index 89fb3ee815..8282a8a582 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs index 83a6a7baf3..5541a0191b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs index 1b396a707c..f6fd1db21e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs index ca995decde..5f7131ff65 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs index 0184154566..0499921fec 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260215201634_ChangePrimaryVersionIdToGuid.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs index 4c9ccc13bf..bf46ad9b39 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs index 23ab2a4674..fc5c7afa0e 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.Designer.cs @@ -267,6 +267,9 @@ namespace Jellyfin.Database.Providers.Sqlite.Migrations b.Property("OfficialRating") .HasColumnType("TEXT"); + b.Property("OriginalLanguage") + .HasColumnType("TEXT"); + b.Property("OriginalTitle") .HasColumnType("TEXT"); diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs deleted file mode 100644 index e0f5125da1..0000000000 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.Designer.cs +++ /dev/null @@ -1,1802 +0,0 @@ -// -using System; -using Jellyfin.Database.Implementations; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Jellyfin.Database.Providers.Sqlite.Migrations -{ - [DbContext(typeof(JellyfinDbContext))] - [Migration("20260504180809_AddOriginalLanguage")] - partial class AddOriginalLanguage - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.7"); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DayOfWeek") - .HasColumnType("INTEGER"); - - b.Property("EndHour") - .HasColumnType("REAL"); - - b.Property("StartHour") - .HasColumnType("REAL"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AccessSchedules"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ActivityLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("LogSeverity") - .HasColumnType("INTEGER"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("ShortOverview") - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("Type") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DateCreated"); - - b.ToTable("ActivityLogs"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ParentItemId") - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "ParentItemId"); - - b.HasIndex("ParentItemId"); - - b.ToTable("AncestorIds"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Index") - .HasColumnType("INTEGER"); - - b.Property("Codec") - .HasColumnType("TEXT"); - - b.Property("CodecTag") - .HasColumnType("TEXT"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("Filename") - .HasColumnType("TEXT"); - - b.Property("MimeType") - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "Index"); - - b.ToTable("AttachmentStreamInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Album") - .HasColumnType("TEXT"); - - b.Property("AlbumArtists") - .HasColumnType("TEXT"); - - b.Property("Artists") - .HasColumnType("TEXT"); - - b.Property("Audio") - .HasColumnType("INTEGER"); - - b.Property("ChannelId") - .HasColumnType("TEXT"); - - b.Property("CleanName") - .HasColumnType("TEXT"); - - b.Property("CommunityRating") - .HasColumnType("REAL"); - - b.Property("CriticRating") - .HasColumnType("REAL"); - - b.Property("CustomRating") - .HasColumnType("TEXT"); - - b.Property("Data") - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastMediaAdded") - .HasColumnType("TEXT"); - - b.Property("DateLastRefreshed") - .HasColumnType("TEXT"); - - b.Property("DateLastSaved") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("EndDate") - .HasColumnType("TEXT"); - - b.Property("EpisodeTitle") - .HasColumnType("TEXT"); - - b.Property("ExternalId") - .HasColumnType("TEXT"); - - b.Property("ExternalSeriesId") - .HasColumnType("TEXT"); - - b.Property("ExternalServiceId") - .HasColumnType("TEXT"); - - b.Property("ExtraType") - .HasColumnType("INTEGER"); - - b.Property("ForcedSortName") - .HasColumnType("TEXT"); - - b.Property("Genres") - .HasColumnType("TEXT"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IndexNumber") - .HasColumnType("INTEGER"); - - b.Property("InheritedParentalRatingSubValue") - .HasColumnType("INTEGER"); - - b.Property("InheritedParentalRatingValue") - .HasColumnType("INTEGER"); - - b.Property("IsFolder") - .HasColumnType("INTEGER"); - - b.Property("IsInMixedFolder") - .HasColumnType("INTEGER"); - - b.Property("IsLocked") - .HasColumnType("INTEGER"); - - b.Property("IsMovie") - .HasColumnType("INTEGER"); - - b.Property("IsRepeat") - .HasColumnType("INTEGER"); - - b.Property("IsSeries") - .HasColumnType("INTEGER"); - - b.Property("IsVirtualItem") - .HasColumnType("INTEGER"); - - b.Property("LUFS") - .HasColumnType("REAL"); - - b.Property("MediaType") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("NormalizationGain") - .HasColumnType("REAL"); - - b.Property("OfficialRating") - .HasColumnType("TEXT"); - - b.Property("OriginalLanguage") - .HasColumnType("TEXT"); - - b.Property("OriginalTitle") - .HasColumnType("TEXT"); - - b.Property("Overview") - .HasColumnType("TEXT"); - - b.Property("OwnerId") - .HasColumnType("TEXT"); - - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("ParentIndexNumber") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.Property("PreferredMetadataCountryCode") - .HasColumnType("TEXT"); - - b.Property("PreferredMetadataLanguage") - .HasColumnType("TEXT"); - - b.Property("PremiereDate") - .HasColumnType("TEXT"); - - b.Property("PresentationUniqueKey") - .HasColumnType("TEXT"); - - b.Property("PrimaryVersionId") - .HasColumnType("TEXT"); - - b.Property("ProductionLocations") - .HasColumnType("TEXT"); - - b.Property("ProductionYear") - .HasColumnType("INTEGER"); - - b.Property("RunTimeTicks") - .HasColumnType("INTEGER"); - - b.Property("SeasonId") - .HasColumnType("TEXT"); - - b.Property("SeasonName") - .HasColumnType("TEXT"); - - b.Property("SeriesId") - .HasColumnType("TEXT"); - - b.Property("SeriesName") - .HasColumnType("TEXT"); - - b.Property("SeriesPresentationUniqueKey") - .HasColumnType("TEXT"); - - b.Property("ShowId") - .HasColumnType("TEXT"); - - b.Property("Size") - .HasColumnType("INTEGER"); - - b.Property("SortName") - .HasColumnType("TEXT"); - - b.Property("StartDate") - .HasColumnType("TEXT"); - - b.Property("Studios") - .HasColumnType("TEXT"); - - b.Property("Tagline") - .HasColumnType("TEXT"); - - b.Property("Tags") - .HasColumnType("TEXT"); - - b.Property("TopParentId") - .HasColumnType("TEXT"); - - b.Property("TotalBitrate") - .HasColumnType("INTEGER"); - - b.Property("Type") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UnratedType") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.HasIndex("OwnerId"); - - b.HasIndex("ParentId"); - - b.HasIndex("Path"); - - b.HasIndex("PresentationUniqueKey"); - - b.HasIndex("SeasonId"); - - b.HasIndex("SeriesId"); - - b.HasIndex("SeriesName"); - - b.HasIndex("ExtraType", "OwnerId"); - - b.HasIndex("TopParentId", "Id"); - - b.HasIndex("Type", "CleanName"); - - b.HasIndex("TopParentId", "Type", "IsVirtualItem") - .HasFilter("\"PrimaryVersionId\" IS NULL AND (\"OwnerId\" IS NULL OR \"ExtraType\" IS NOT NULL)"); - - b.HasIndex("Type", "TopParentId", "Id"); - - b.HasIndex("Type", "TopParentId", "PresentationUniqueKey"); - - b.HasIndex("Type", "TopParentId", "SortName"); - - b.HasIndex("Type", "TopParentId", "StartDate"); - - b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey"); - - b.HasIndex("TopParentId", "IsFolder", "IsVirtualItem", "DateCreated"); - - b.HasIndex("TopParentId", "MediaType", "IsVirtualItem", "DateCreated"); - - b.HasIndex("TopParentId", "Type", "IsVirtualItem", "DateCreated"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "ParentIndexNumber", "IndexNumber"); - - b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName"); - - b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated"); - - b.ToTable("BaseItems"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - - b.HasData( - new - { - Id = new Guid("00000000-0000-0000-0000-000000000001"), - IsFolder = false, - IsInMixedFolder = false, - IsLocked = false, - IsMovie = false, - IsRepeat = false, - IsSeries = false, - IsVirtualItem = false, - Name = "This is a placeholder item for UserData that has been detached from its original item", - Type = "PLACEHOLDER" - }); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Blurhash") - .HasColumnType("BLOB"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("ImageType") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("ItemId", "ImageType"); - - b.ToTable("BaseItemImageInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemMetadataFields"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ProviderId") - .HasColumnType("TEXT"); - - b.Property("ProviderValue") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ItemId", "ProviderId"); - - b.HasIndex("ProviderId", "ItemId", "ProviderValue"); - - b.ToTable("BaseItemProviders"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => - { - b.Property("Id") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("Id", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("BaseItemTrailerTypes"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ChapterIndex") - .HasColumnType("INTEGER"); - - b.Property("ImageDateModified") - .HasColumnType("TEXT"); - - b.Property("ImagePath") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("StartPositionTicks") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "ChapterIndex"); - - b.ToTable("Chapters"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.CustomItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Key") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client", "Key") - .IsUnique(); - - b.ToTable("CustomItemDisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ChromecastVersion") - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DashboardTheme") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("EnableNextVideoInfoOverlay") - .HasColumnType("INTEGER"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("ScrollDirection") - .HasColumnType("INTEGER"); - - b.Property("ShowBackdrop") - .HasColumnType("INTEGER"); - - b.Property("ShowSidebar") - .HasColumnType("INTEGER"); - - b.Property("SkipBackwardLength") - .HasColumnType("INTEGER"); - - b.Property("SkipForwardLength") - .HasColumnType("INTEGER"); - - b.Property("TvHome") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "ItemId", "Client") - .IsUnique(); - - b.ToTable("DisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("DisplayPreferencesId") - .HasColumnType("INTEGER"); - - b.Property("Order") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("DisplayPreferencesId"); - - b.ToTable("HomeSection"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("LastModified") - .HasColumnType("TEXT"); - - b.Property("Path") - .IsRequired() - .HasMaxLength(512) - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("ImageInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Client") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("IndexBy") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("RememberIndexing") - .HasColumnType("INTEGER"); - - b.Property("RememberSorting") - .HasColumnType("INTEGER"); - - b.Property("SortBy") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("ViewType") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("ItemDisplayPreferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => - { - b.Property("ItemValueId") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("CleanValue") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("Value") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("ItemValueId"); - - b.HasIndex("Type", "CleanValue"); - - b.HasIndex("Type", "Value") - .IsUnique(); - - b.ToTable("ItemValues"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => - { - b.Property("ItemValueId") - .HasColumnType("TEXT"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.HasKey("ItemValueId", "ItemId"); - - b.HasIndex("ItemId"); - - b.ToTable("ItemValuesMap"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.PrimitiveCollection("KeyframeTicks") - .HasColumnType("TEXT"); - - b.Property("TotalDuration") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId"); - - b.ToTable("KeyframeData"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b => - { - b.Property("ParentId") - .HasColumnType("TEXT"); - - b.Property("ChildId") - .HasColumnType("TEXT"); - - b.Property("ChildType") - .HasColumnType("INTEGER"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.HasKey("ParentId", "ChildId"); - - b.HasIndex("ChildId", "ChildType"); - - b.HasIndex("ParentId", "ChildType"); - - b.HasIndex("ParentId", "SortOrder"); - - b.ToTable("LinkedChildren", (string)null); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaSegment", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("EndTicks") - .HasColumnType("INTEGER"); - - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("SegmentProviderId") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("StartTicks") - .HasColumnType("INTEGER"); - - b.Property("Type") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("MediaSegments"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("StreamIndex") - .HasColumnType("INTEGER"); - - b.Property("AspectRatio") - .HasColumnType("TEXT"); - - b.Property("AverageFrameRate") - .HasColumnType("REAL"); - - b.Property("BitDepth") - .HasColumnType("INTEGER"); - - b.Property("BitRate") - .HasColumnType("INTEGER"); - - b.Property("BlPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("ChannelLayout") - .HasColumnType("TEXT"); - - b.Property("Channels") - .HasColumnType("INTEGER"); - - b.Property("Codec") - .HasColumnType("TEXT"); - - b.Property("CodecTag") - .HasColumnType("TEXT"); - - b.Property("CodecTimeBase") - .HasColumnType("TEXT"); - - b.Property("ColorPrimaries") - .HasColumnType("TEXT"); - - b.Property("ColorSpace") - .HasColumnType("TEXT"); - - b.Property("ColorTransfer") - .HasColumnType("TEXT"); - - b.Property("Comment") - .HasColumnType("TEXT"); - - b.Property("DvBlSignalCompatibilityId") - .HasColumnType("INTEGER"); - - b.Property("DvLevel") - .HasColumnType("INTEGER"); - - b.Property("DvProfile") - .HasColumnType("INTEGER"); - - b.Property("DvVersionMajor") - .HasColumnType("INTEGER"); - - b.Property("DvVersionMinor") - .HasColumnType("INTEGER"); - - b.Property("ElPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("Hdr10PlusPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IsAnamorphic") - .HasColumnType("INTEGER"); - - b.Property("IsAvc") - .HasColumnType("INTEGER"); - - b.Property("IsDefault") - .HasColumnType("INTEGER"); - - b.Property("IsExternal") - .HasColumnType("INTEGER"); - - b.Property("IsForced") - .HasColumnType("INTEGER"); - - b.Property("IsHearingImpaired") - .HasColumnType("INTEGER"); - - b.Property("IsInterlaced") - .HasColumnType("INTEGER"); - - b.Property("IsOriginal") - .HasColumnType("INTEGER"); - - b.Property("KeyFrames") - .HasColumnType("TEXT"); - - b.Property("Language") - .HasColumnType("TEXT"); - - b.Property("Level") - .HasColumnType("REAL"); - - b.Property("NalLengthSize") - .HasColumnType("TEXT"); - - b.Property("Path") - .HasColumnType("TEXT"); - - b.Property("PixelFormat") - .HasColumnType("TEXT"); - - b.Property("Profile") - .HasColumnType("TEXT"); - - b.Property("RealFrameRate") - .HasColumnType("REAL"); - - b.Property("RefFrames") - .HasColumnType("INTEGER"); - - b.Property("Rotation") - .HasColumnType("INTEGER"); - - b.Property("RpuPresentFlag") - .HasColumnType("INTEGER"); - - b.Property("SampleRate") - .HasColumnType("INTEGER"); - - b.Property("StreamType") - .HasColumnType("INTEGER"); - - b.Property("TimeBase") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "StreamIndex"); - - b.ToTable("MediaStreamInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PersonType") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("Peoples"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("PeopleId") - .HasColumnType("TEXT"); - - b.Property("Role") - .HasColumnType("TEXT"); - - b.Property("ListOrder") - .HasColumnType("INTEGER"); - - b.Property("SortOrder") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "PeopleId", "Role"); - - b.HasIndex("PeopleId"); - - b.HasIndex("ItemId", "ListOrder"); - - b.HasIndex("ItemId", "SortOrder"); - - b.ToTable("PeopleBaseItemMap"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Permission_Permissions_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Permissions"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("Kind") - .HasColumnType("INTEGER"); - - b.Property("Preference_Preferences_Guid") - .HasColumnType("TEXT"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId", "Kind") - .IsUnique() - .HasFilter("[UserId] IS NOT NULL"); - - b.ToTable("Preferences"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.ApiKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken") - .IsUnique(); - - b.ToTable("ApiKeys"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AccessToken") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("AppName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("AppVersion") - .IsRequired() - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DateCreated") - .HasColumnType("TEXT"); - - b.Property("DateLastActivity") - .HasColumnType("TEXT"); - - b.Property("DateModified") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("DeviceName") - .IsRequired() - .HasMaxLength(64) - .HasColumnType("TEXT"); - - b.Property("IsActive") - .HasColumnType("INTEGER"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("AccessToken", "DateLastActivity"); - - b.HasIndex("DeviceId", "DateLastActivity"); - - b.HasIndex("UserId", "DeviceId"); - - b.ToTable("Devices"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.DeviceOptions", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CustomName") - .HasColumnType("TEXT"); - - b.Property("DeviceId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("DeviceId") - .IsUnique(); - - b.ToTable("DeviceOptions"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.TrickplayInfo", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.Property("Bandwidth") - .HasColumnType("INTEGER"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("Interval") - .HasColumnType("INTEGER"); - - b.Property("ThumbnailCount") - .HasColumnType("INTEGER"); - - b.Property("TileHeight") - .HasColumnType("INTEGER"); - - b.Property("TileWidth") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "Width"); - - b.ToTable("TrickplayInfos"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("AudioLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("AuthenticationProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("CastReceiverId") - .HasMaxLength(32) - .HasColumnType("TEXT"); - - b.Property("DisplayCollectionsView") - .HasColumnType("INTEGER"); - - b.Property("DisplayMissingEpisodes") - .HasColumnType("INTEGER"); - - b.Property("EnableAutoLogin") - .HasColumnType("INTEGER"); - - b.Property("EnableLocalPassword") - .HasColumnType("INTEGER"); - - b.Property("EnableNextEpisodeAutoPlay") - .HasColumnType("INTEGER"); - - b.Property("EnableUserPreferenceAccess") - .HasColumnType("INTEGER"); - - b.Property("HidePlayedInLatest") - .HasColumnType("INTEGER"); - - b.Property("InternalId") - .HasColumnType("INTEGER"); - - b.Property("InvalidLoginAttemptCount") - .HasColumnType("INTEGER"); - - b.Property("LastActivityDate") - .HasColumnType("TEXT"); - - b.Property("LastLoginDate") - .HasColumnType("TEXT"); - - b.Property("LoginAttemptsBeforeLockout") - .HasColumnType("INTEGER"); - - b.Property("MaxActiveSessions") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalRatingScore") - .HasColumnType("INTEGER"); - - b.Property("MaxParentalRatingSubScore") - .HasColumnType("INTEGER"); - - b.Property("MustUpdatePassword") - .HasColumnType("INTEGER"); - - b.Property("Password") - .HasMaxLength(65535) - .HasColumnType("TEXT"); - - b.Property("PasswordResetProviderId") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("PlayDefaultAudioTrack") - .HasColumnType("INTEGER"); - - b.Property("RememberAudioSelections") - .HasColumnType("INTEGER"); - - b.Property("RememberSubtitleSelections") - .HasColumnType("INTEGER"); - - b.Property("RemoteClientBitrateLimit") - .HasColumnType("INTEGER"); - - b.Property("RowVersion") - .IsConcurrencyToken() - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguagePreference") - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.Property("SubtitleMode") - .HasColumnType("INTEGER"); - - b.Property("SyncPlayAccess") - .HasColumnType("INTEGER"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(255) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => - { - b.Property("ItemId") - .HasColumnType("TEXT"); - - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("CustomDataKey") - .HasColumnType("TEXT"); - - b.Property("AudioStreamIndex") - .HasColumnType("INTEGER"); - - b.Property("IsFavorite") - .HasColumnType("INTEGER"); - - b.Property("LastPlayedDate") - .HasColumnType("TEXT"); - - b.Property("Likes") - .HasColumnType("INTEGER"); - - b.Property("PlayCount") - .HasColumnType("INTEGER"); - - b.Property("PlaybackPositionTicks") - .HasColumnType("INTEGER"); - - b.Property("Played") - .HasColumnType("INTEGER"); - - b.Property("Rating") - .HasColumnType("REAL"); - - b.Property("RetentionDate") - .HasColumnType("TEXT"); - - b.Property("SubtitleStreamIndex") - .HasColumnType("INTEGER"); - - b.HasKey("ItemId", "UserId", "CustomDataKey"); - - b.HasIndex("ItemId", "UserId", "IsFavorite"); - - b.HasIndex("ItemId", "UserId", "LastPlayedDate"); - - b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks"); - - b.HasIndex("ItemId", "UserId", "Played"); - - b.HasIndex("UserId", "IsFavorite", "ItemId"); - - b.HasIndex("UserId", "ItemId", "LastPlayedDate"); - - b.HasIndex("UserId", "Played", "ItemId"); - - b.ToTable("UserData"); - - b.HasAnnotation("Sqlite:UseSqlReturningClause", false); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AccessSchedule", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("AccessSchedules") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AncestorId", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Parents") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "ParentItem") - .WithMany("Children") - .HasForeignKey("ParentItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ParentItem"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.AttachmentStreamInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Owner") - .WithMany("Extras") - .HasForeignKey("OwnerId") - .OnDelete(DeleteBehavior.NoAction); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "DirectParent") - .WithMany("DirectChildren") - .HasForeignKey("ParentId") - .OnDelete(DeleteBehavior.Cascade); - - b.Navigation("DirectParent"); - - b.Navigation("Owner"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemImageInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Images") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemMetadataField", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("LockedFields") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemProvider", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Provider") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemTrailerType", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("TrailerTypes") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Chapter", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Chapters") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("DisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.HomeSection", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.DisplayPreferences", null) - .WithMany("HomeSections") - .HasForeignKey("DisplayPreferencesId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ImageInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithOne("ProfileImage") - .HasForeignKey("Jellyfin.Database.Implementations.Entities.ImageInfo", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemDisplayPreferences", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("ItemDisplayPreferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValueMap", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("ItemValues") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.ItemValue", "ItemValue") - .WithMany("BaseItemsMap") - .HasForeignKey("ItemValueId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("ItemValue"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.KeyframeData", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany() - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.LinkedChildEntity", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Child") - .WithMany("LinkedChildOfEntities") - .HasForeignKey("ChildId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Parent") - .WithMany("LinkedChildEntities") - .HasForeignKey("ParentId") - .OnDelete(DeleteBehavior.NoAction) - .IsRequired(); - - b.Navigation("Child"); - - b.Navigation("Parent"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.MediaStreamInfo", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("MediaStreams") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.PeopleBaseItemMap", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("Peoples") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.People", "People") - .WithMany("BaseItems") - .HasForeignKey("PeopleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("People"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Permission", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Preference", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", null) - .WithMany("Preferences") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.Security.Device", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.UserData", b => - { - b.HasOne("Jellyfin.Database.Implementations.Entities.BaseItemEntity", "Item") - .WithMany("UserData") - .HasForeignKey("ItemId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Jellyfin.Database.Implementations.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Item"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.BaseItemEntity", b => - { - b.Navigation("Chapters"); - - b.Navigation("Children"); - - b.Navigation("DirectChildren"); - - b.Navigation("Extras"); - - b.Navigation("Images"); - - b.Navigation("ItemValues"); - - b.Navigation("LinkedChildEntities"); - - b.Navigation("LinkedChildOfEntities"); - - b.Navigation("LockedFields"); - - b.Navigation("MediaStreams"); - - b.Navigation("Parents"); - - b.Navigation("Peoples"); - - b.Navigation("Provider"); - - b.Navigation("TrailerTypes"); - - b.Navigation("UserData"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.DisplayPreferences", b => - { - b.Navigation("HomeSections"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.ItemValue", b => - { - b.Navigation("BaseItemsMap"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.People", b => - { - b.Navigation("BaseItems"); - }); - - modelBuilder.Entity("Jellyfin.Database.Implementations.Entities.User", b => - { - b.Navigation("AccessSchedules"); - - b.Navigation("DisplayPreferences"); - - b.Navigation("ItemDisplayPreferences"); - - b.Navigation("Permissions"); - - b.Navigation("Preferences"); - - b.Navigation("ProfileImage"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs deleted file mode 100644 index cda226309a..0000000000 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504180809_AddOriginalLanguage.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Jellyfin.Database.Providers.Sqlite.Migrations -{ - /// - public partial class AddOriginalLanguage : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "IsOriginal", - table: "MediaStreamInfos", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "OriginalLanguage", - table: "BaseItems", - type: "TEXT", - nullable: true); - - migrationBuilder.UpdateData( - table: "BaseItems", - keyColumn: "Id", - keyValue: new Guid("00000000-0000-0000-0000-000000000001"), - column: "OriginalLanguage", - value: null); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "IsOriginal", - table: "MediaStreamInfos"); - - migrationBuilder.DropColumn( - name: "OriginalLanguage", - table: "BaseItems"); - } - } -} From 6b1378fa893821e2e3167ba3a8ad7ecef5d0d962 Mon Sep 17 00:00:00 2001 From: lednurb Date: Sat, 16 May 2026 04:45:38 -0400 Subject: [PATCH 08/63] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 9aea3adc22..898f5892c9 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "HeaderContinueWatching": "Verderkijken", + "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderLiveTV": "Live-tv", From 5c3c3e35b904346851cc267836d05e5222b2dcf7 Mon Sep 17 00:00:00 2001 From: rimasx Date: Sat, 16 May 2026 04:51:48 -0400 Subject: [PATCH 09/63] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index b530f19fa9..bc089d836c 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -106,5 +106,6 @@ "TaskExtractMediaSegmentsDescription": "Eraldab või võtab meedialõigud MediaSegment'i toega pluginatest.", "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "CleanupUserDataTask": "Puhasta kasutajaandmed", - "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud." + "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.", + "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus" } From 8740f3d154b409ed83f5eb7dd07c82bd520327d1 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 16 May 2026 18:51:30 +0200 Subject: [PATCH 10/63] Move ComicVine and GoogleBooks ExternalUrl providers to MediaBrowser.Providers.Books --- .../ComicVine/ComicVineExternalId.cs | 2 +- .../ComicVine/ComicVineExternalUrlProvider.cs | 2 +- .../ComicVine/ComicVinePersonExternalId.cs | 2 +- .../GoogleBooks/GoogleBooksExternalId.cs | 2 +- .../GoogleBooksExternalUrlProvider.cs | 2 +- .../MediaBrowser.Providers.csproj | 12 ----- .../Configuration/PluginConfiguration.cs | 10 ----- .../Plugins/ComicVine/Plugin.cs | 45 ------------------- .../ComicVine/jellyfin-plugin-comicvine.svg | 16 ------- .../Configuration/PluginConfiguration.cs | 10 ----- .../Plugins/GoogleBooks/Plugin.cs | 45 ------------------- .../jellyfin-plugin-googlebooks.svg | 18 -------- .../ComicVineExternalUrlProviderTests.cs | 2 +- .../GoogleBooksExternalUrlProviderTests.cs | 2 +- 14 files changed, 7 insertions(+), 163 deletions(-) rename MediaBrowser.Providers/{Plugins => Books}/ComicVine/ComicVineExternalId.cs (91%) rename MediaBrowser.Providers/{Plugins => Books}/ComicVine/ComicVineExternalUrlProvider.cs (93%) rename MediaBrowser.Providers/{Plugins => Books}/ComicVine/ComicVinePersonExternalId.cs (92%) rename MediaBrowser.Providers/{Plugins => Books}/GoogleBooks/GoogleBooksExternalId.cs (91%) rename MediaBrowser.Providers/{Plugins => Books}/GoogleBooks/GoogleBooksExternalUrlProvider.cs (92%) delete mode 100644 MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs delete mode 100644 MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs delete mode 100644 MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg delete mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs delete mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs delete mode 100644 MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs similarity index 91% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs index 8cbd1f89a7..e2e785eaca 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.ComicVine +namespace MediaBrowser.Providers.Books.ComicVine { /// public class ComicVineExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs similarity index 93% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs index 9122399179..a8450ec599 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVineExternalUrlProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.Plugins.ComicVine; +namespace MediaBrowser.Providers.Books.ComicVine; /// public class ComicVineExternalUrlProvider : IExternalUrlProvider diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs similarity index 92% rename from MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs rename to MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs index 26b8e11380..f625fb9649 100644 --- a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs +++ b/MediaBrowser.Providers/Books/ComicVine/ComicVinePersonExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.ComicVine +namespace MediaBrowser.Providers.Books.ComicVine { /// public class ComicVinePersonExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs similarity index 91% rename from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs rename to MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs index 02d3b36974..aac8cdff65 100644 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs +++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalId.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -namespace MediaBrowser.Providers.Plugins.GoogleBooks +namespace MediaBrowser.Providers.Books.GoogleBooks { /// public class GoogleBooksExternalId : IExternalId diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs similarity index 92% rename from MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs rename to MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs index 95047ee83e..0559db2e2b 100644 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Books/GoogleBooks/GoogleBooksExternalUrlProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.Plugins.GoogleBooks; +namespace MediaBrowser.Providers.Books.GoogleBooks; /// public class GoogleBooksExternalUrlProvider : IExternalUrlProvider diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 181cda3288..1032582900 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -55,18 +55,6 @@ - - - - - - - - - - - - diff --git a/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs deleted file mode 100644 index d35639d7e8..0000000000 --- a/MediaBrowser.Providers/Plugins/ComicVine/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Plugins; - -namespace MediaBrowser.Providers.Plugins.ComicVine; - -/// -/// Plugin configuration for the Comic Vine provider. -/// -public class PluginConfiguration : BasePluginConfiguration -{ -} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs b/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs deleted file mode 100644 index 101fa103b0..0000000000 --- a/MediaBrowser.Providers/Plugins/ComicVine/Plugin.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Providers.Plugins.ComicVine; - -/// -/// ComicVine plugin instance. -/// -public class Plugin : BasePlugin, IHasEmbeddedImage -{ - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - - /// - /// Gets the current plugin instance. - /// - public static Plugin? Instance { get; private set; } - - /// - public override Guid Id => new("3ade6fd1-c76c-4560-b2df-f6bca4c2332f"); - - /// - public override string Name => "Comic Vine"; - - /// - public override string Description => "Get external links for comic books from Comic Vine."; - - /// - // TODO remove when plugin removed from server. - public override string ConfigurationFileName => "Jellyfin.Plugin.ComicVine.xml"; - - /// - public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-comicvine.svg"; -} diff --git a/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg b/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg deleted file mode 100644 index 81bde53a51..0000000000 --- a/MediaBrowser.Providers/Plugins/ComicVine/jellyfin-plugin-comicvine.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs deleted file mode 100644 index c32f764810..0000000000 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Plugins; - -namespace MediaBrowser.Providers.Plugins.GoogleBooks; - -/// -/// Plugin configuration for the Google Books provider. -/// -public class PluginConfiguration : BasePluginConfiguration -{ -} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs deleted file mode 100644 index 645e27f5f9..0000000000 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/Plugin.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Serialization; - -namespace MediaBrowser.Providers.Plugins.GoogleBooks; - -/// -/// Google Books plugin instance. -/// -public class Plugin : BasePlugin, IHasEmbeddedImage -{ - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) - : base(applicationPaths, xmlSerializer) - { - Instance = this; - } - - /// - /// Gets the current plugin instance. - /// - public static Plugin? Instance { get; private set; } - - /// - public override Guid Id => new("9f97232d-e7f4-432d-92d3-c709ce47e30b"); - - /// - public override string Name => "Google Books"; - - /// - public override string Description => "Get external links for books from Google Books."; - - /// - // TODO remove when plugin removed from server. - public override string ConfigurationFileName => "Jellyfin.Plugin.GoogleBooks.xml"; - - /// - public string ImageResourceName => GetType().Namespace + ".jellyfin-plugin-googlebooks.svg"; -} diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg b/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg deleted file mode 100644 index f93ca4a18e..0000000000 --- a/MediaBrowser.Providers/Plugins/GoogleBooks/jellyfin-plugin-googlebooks.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs index 99604e0933..aaa500b762 100644 --- a/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/ExternalId/ComicVineExternalUrlProviderTests.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.ComicVine; +using MediaBrowser.Providers.Books.ComicVine; using Xunit; namespace Jellyfin.Providers.Tests.ExternalId diff --git a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs index eec64ac53f..b9ce895dbc 100644 --- a/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/ExternalId/GoogleBooksExternalUrlProviderTests.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.Plugins.GoogleBooks; +using MediaBrowser.Providers.Books.GoogleBooks; using Xunit; namespace Jellyfin.Providers.Tests.ExternalId From ed3c62b66ed8968b643e7d20ebcc2b21f3ca5f95 Mon Sep 17 00:00:00 2001 From: Luca Date: Sat, 16 May 2026 11:27:34 -0400 Subject: [PATCH 11/63] Translated using Weblate (Romanian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ro/ --- Emby.Server.Implementations/Localization/Core/ro.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 7b01fbec89..ea83b88951 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -106,5 +106,7 @@ "TaskDownloadMissingLyrics": "Descarcă versurile lipsă", "TaskDownloadMissingLyricsDescription": "Descarcă versuri pentru melodii", "CleanupUserDataTask": "Sarcina de curatare a datelor utilizatorului", - "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile." + "CleanupUserDataTaskDescription": "Sterge toate datele utilizatorului (starea vizionarii, starea favoritelor etc.) de pe suporturile media care nu mai sunt prezente timp de cel puțin 90 de zile.", + "LyricDownloadFailureFromForItem": "Versurile nu au putut fi descărcate din {0} pentru {1}", + "Original": "Original" } From 2f8bf92fb80c8bf8568e8b22aba31a839b8862d3 Mon Sep 17 00:00:00 2001 From: Bruno Ferreira Date: Sun, 17 May 2026 09:01:39 -0300 Subject: [PATCH 12/63] fix: add null check for non-existent program in GetProgram (#16858) fix: add null check for non-existent program in GetProgram --- Jellyfin.Api/Controllers/LiveTvController.cs | 10 +++++++++- src/Jellyfin.LiveTv/LiveTvManager.cs | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 074cdb24e0..113298c251 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -744,10 +744,12 @@ public class LiveTvController : BaseJellyfinApiController /// Program id. /// Optional. Attach user data. /// Program returned. + /// Program not found. /// An containing the livetv program. [HttpGet("Programs/{programId}")] [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetProgram( [FromRoute, Required] string programId, [FromQuery] Guid? userId) @@ -756,8 +758,14 @@ public class LiveTvController : BaseJellyfinApiController var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); + var result = await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); - return await _liveTvManager.GetProgram(programId, CancellationToken.None, user).ConfigureAwait(false); + if (result is null) + { + return NotFound(); + } + + return Ok(result); } /// diff --git a/src/Jellyfin.LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index 2abc8a8c09..173d3c3e8e 100644 --- a/src/Jellyfin.LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -178,6 +178,11 @@ namespace Jellyfin.LiveTv { var program = _libraryManager.GetItemById(id); + if (program is null) + { + return null; + } + var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); var list = new List<(BaseItemDto ItemDto, string ExternalId, string ExternalSeriesId)> From aa960dc6967d1e62a1dca70c7fae90f0798b14da Mon Sep 17 00:00:00 2001 From: Translation expert Date: Sun, 17 May 2026 14:57:25 -0400 Subject: [PATCH 13/63] Translated using Weblate (Arabic) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ar/ --- Emby.Server.Implementations/Localization/Core/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 1eaf1d79e8..17af935562 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "ينقل ملفات معاينات التنقل الحالية وفقاً لإعدادات المكتبة.", "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم", "CleanupUserDataTaskDescription": "ينظف جميع بيانات المستخدم (مثل حالة المشاهدة وحالة المفضلة وغيرها) للمحتوى الذي لم يعد موجوداً لمدة 90 يوماً على الأقل.", - "Original": "فريد" + "Original": "فريد", + "LyricDownloadFailureFromForItem": "فشل تحميل الكلمات من {0} إلى {1}" } From 8b84bf6e21a2df5fcfb8c9e3ea4260117f602096 Mon Sep 17 00:00:00 2001 From: rimasx Date: Sun, 17 May 2026 06:18:24 -0400 Subject: [PATCH 14/63] Translated using Weblate (Estonian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/et/ --- Emby.Server.Implementations/Localization/Core/et.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index bc089d836c..e6bf1f25b5 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Muuda trickplay piltide asukoht", "CleanupUserDataTask": "Puhasta kasutajaandmed", "CleanupUserDataTaskDescription": "Puhastab kõik kasutajaandmed (vaatamise olek, lemmikute olek jne) meediast, mida pole enam vähemalt 90 päeva saadaval olnud.", - "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus" + "LyricDownloadFailureFromForItem": "Laulusõnade hankimine teenusest {0} loole {1} nurjus", + "Original": "Algne" } From 7e7bacd0138499ae0f194d168d8717add134f9e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:27:11 +0000 Subject: [PATCH 15/63] Update dependency coverlet.collector to 10.0.1 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e722bed973..f2a3aacee8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,7 +13,7 @@ - + From 3a1bd5984d18e647363b9ea0ad8aa6fb12f5d199 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:27:29 +0000 Subject: [PATCH 16/63] Update dependency BitFaster.Caching to 2.6.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e722bed973..51a9268af3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + From 3430f4fa57ace45d321c16cdf64914d3fe390e90 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:27:37 +0000 Subject: [PATCH 17/63] Update dependency Svg.Skia to 3.7.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e722bed973..6549ee21ff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ - + From 8d28c15e3e6ec9a24eaeb804a8813ad35fb72d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:27:47 +0000 Subject: [PATCH 18/63] Update ghcr.io/devcontainers/features/docker-in-docker Docker tag to v3 --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c67c292372..4ce0d7583a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,7 +33,7 @@ "libfontconfig1" ] }, - "ghcr.io/devcontainers/features/docker-in-docker:2": { + "ghcr.io/devcontainers/features/docker-in-docker:3": { "dockerDashComposeVersion": "v2" }, "ghcr.io/devcontainers/features/github-cli:1": {}, From 80a552a35d2790d57c73c24fd1fe8240dd6ecfa6 Mon Sep 17 00:00:00 2001 From: psmattas Date: Mon, 18 May 2026 14:39:32 +0100 Subject: [PATCH 19/63] fix: suppress repeated PriorityClass warning in MediaEncoder When Jellyfin runs without permission to set process priority (e.g. Docker), StartProcess() logged a warning for every file probed during a library scan. Add a _canSetProcessPriority flag: warn once on first failure, skip all subsequent attempts. Fixes #15287 --- .../Encoder/MediaEncoder.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index f34e911a05..66bf6ebd24 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -85,6 +85,8 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceSupportVulkanDrmModifier = false; private bool _isVaapiDeviceSupportVulkanDrmInterop = false; + private bool _canSetProcessPriority = true; + private bool _isVideoToolboxAv1DecodeAvailable = false; private static string[] _vulkanImageDrmFmtModifierExts = @@ -1123,13 +1125,17 @@ namespace MediaBrowser.MediaEncoding.Encoder { process.Process.Start(); - try + if (_canSetProcessPriority) { - process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}", process.Process.StartInfo.FileName); + try + { + process.Process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch (Exception ex) + { + _canSetProcessPriority = false; + _logger.LogWarning(ex, "Unable to set process priority to BelowNormal for {ProcessFileName}. Further attempts will be skipped.", process.Process.StartInfo.FileName); + } } lock (_runningProcessesLock) From 07c63d5ebfda00bf98776b1a7afc642993916c10 Mon Sep 17 00:00:00 2001 From: GolanGitHub Date: Mon, 18 May 2026 10:57:33 -0400 Subject: [PATCH 20/63] Translated using Weblate (Spanish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es/ --- Emby.Server.Implementations/Localization/Core/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index d61ce396cb..35efcf74d3 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay", "CleanupUserDataTask": "Tarea de limpieza de datos del usuario", "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "No se pudieron descargar las letras desde {0} para {1}." } From 5e8b86f80c604a3485808bd50bcc445fdf254e35 Mon Sep 17 00:00:00 2001 From: Szilki077 Date: Mon, 18 May 2026 03:00:51 -0400 Subject: [PATCH 21/63] Translated using Weblate (Hungarian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hu/ --- Emby.Server.Implementations/Localization/Core/hu.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index e1c1ec109d..1995d7a4cf 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -1,5 +1,5 @@ { - "AppDeviceValues": "Program: {0}, Eszköz: {1}", + "AppDeviceValues": "alkalmazás: {0}, eszköz: {1}", "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve", "Books": "Könyvek", @@ -107,5 +107,6 @@ "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.", "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.", "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat", - "Original": "Eredeti" + "Original": "Eredeti", + "LyricDownloadFailureFromForItem": "Dalszöveg letöltése {0}-tól {1}-hez sikertelen" } From dac6b70f522d419257a5978f642532f09949af59 Mon Sep 17 00:00:00 2001 From: Vincenzo Reale Date: Mon, 18 May 2026 02:12:32 -0400 Subject: [PATCH 22/63] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/it/ --- Emby.Server.Implementations/Localization/Core/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 6053aec896..f13944e6be 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -107,5 +107,6 @@ "TaskExtractMediaSegments": "Scansiona Segmento Media", "CleanupUserDataTask": "Task di pulizia dei dati utente", "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni.", - "Original": "Originale" + "Original": "Originale", + "LyricDownloadFailureFromForItem": "Scaricamento dei testi non riuscito da {0} per {1}" } From 2c7addc671edf0b0487ee68738319de1deec3f78 Mon Sep 17 00:00:00 2001 From: PlinioRegisNeto Date: Mon, 18 May 2026 08:35:39 -0400 Subject: [PATCH 23/63] Translated using Weblate (Portuguese (Brazil)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_BR/ --- Emby.Server.Implementations/Localization/Core/pt-BR.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 9b2ef063a6..1db500adf3 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -106,5 +106,7 @@ "TaskExtractMediaSegmentsDescription": "Extrai ou obtém segmentos de mídia de plug-ins habilitados para MediaSegment.", "TaskMoveTrickplayImages": "Migrar o local da imagem do Trickplay", "CleanupUserDataTask": "Tarefa de limpeza de dados do usuário", - "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias." + "CleanupUserDataTaskDescription": "Limpa todos os dados do usuário (estado de visualização, status de favorito, etc.) de mídias que não estão presentes por pelo menos 90 dias.", + "LyricDownloadFailureFromForItem": "Download das Letras falharam em {0} para o item {1}", + "Original": "Original" } From e00d01cc301b7a9a181056b54bdb120ffe76e853 Mon Sep 17 00:00:00 2001 From: Dan Tsivinsky Date: Mon, 18 May 2026 10:36:30 -0400 Subject: [PATCH 24/63] Translated using Weblate (Russian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ru/ --- Emby.Server.Implementations/Localization/Core/ru.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index d7eccf5f25..40d5e3985d 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Перемещает существующие файлы trickplay в соответствии с настройками медиатеки.", "CleanupUserDataTask": "Задача очистки пользовательских данных", "CleanupUserDataTaskDescription": "Очищает все пользовательские данные (состояние просмотра, статус избранного и т.д.) с медиа, отсутствующих по меньшей мере в течение 90 дней.", - "Original": "Оригинальный" + "Original": "Оригинальный", + "LyricDownloadFailureFromForItem": "Не получилось скачать текст песни с {0} для {1}" } From ed27de13a0b6e1ddbe7f5836d6c21ed6d8adf2d9 Mon Sep 17 00:00:00 2001 From: Vilhelm Prytz Date: Mon, 18 May 2026 08:28:14 -0400 Subject: [PATCH 25/63] Translated using Weblate (Swedish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/sv/ --- Emby.Server.Implementations/Localization/Core/sv.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index af3fbbaded..7384967122 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Flyttar befintliga trickplay-filer enligt bibliotekets inställningar.", "CleanupUserDataTaskDescription": "Tar bort all användardata (såsom vad du sett, favoriter med mera) för media som inte funnits på enheten på minst 90 dagar.", "CleanupUserDataTask": "Uppgift för rensning av användardata", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "Misslyckades att ladda ner låttexter från {0} för {1}" } From c4957dff31a57b5c32817f28df802139464e3621 Mon Sep 17 00:00:00 2001 From: DyingSlacker <2391773977@outlook.com> Date: Mon, 18 May 2026 02:31:26 -0400 Subject: [PATCH 26/63] Translated using Weblate (Chinese (Simplified Han script)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hans/ --- Emby.Server.Implementations/Localization/Core/zh-CN.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 8c36139f29..18418ae0bc 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -106,5 +106,7 @@ "TaskExtractMediaSegmentsDescription": "从支持 MediaSegment 的插件中提取或获取媒体分段。", "TaskMoveTrickplayImagesDescription": "根据媒体库设置移动现有的进度条预览图文件。", "CleanupUserDataTask": "用户数据清理任务", - "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。" + "CleanupUserDataTaskDescription": "清理已被删除超过90天的媒体中的所有用户数据(观看状态、收藏夹状态等)。", + "LyricDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的歌词", + "Original": "原始" } From 3a4e6236a85a3403a6b82c12542a77b898e669ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Mon, 18 May 2026 04:24:29 -0400 Subject: [PATCH 27/63] Translated using Weblate (Irish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ga/ --- Emby.Server.Implementations/Localization/Core/ga.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ga.json b/Emby.Server.Implementations/Localization/Core/ga.json index 8badf551b3..1ee606cc64 100644 --- a/Emby.Server.Implementations/Localization/Core/ga.json +++ b/Emby.Server.Implementations/Localization/Core/ga.json @@ -107,5 +107,6 @@ "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh", "CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora", "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad.", - "Original": "Bunaidh" + "Original": "Bunaidh", + "LyricDownloadFailureFromForItem": "Theip ar liricí a íoslódáil ó {0} do {1}" } From 8deb5c1d2ac59befa100326eac57b099acfe4e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tayfun=20Akg=C3=BC=C3=A7?= Date: Mon, 18 May 2026 21:22:20 +0300 Subject: [PATCH 28/63] fix: write livetv recording NFO dateadded as UTC (#16863) fix: write livetv recording NFO dateadded as UTC --- .../Recordings/RecordingsMetadataManager.cs | 2 +- .../RecordingsMetadataManagerTests.cs | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs index 3a2c463695..7e68dbb547 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs @@ -288,7 +288,7 @@ public class RecordingsMetadataManager null, "dateadded", null, - DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); + DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); if (item.ProductionYear.HasValue) { diff --git a/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs new file mode 100644 index 0000000000..14ce470fb4 --- /dev/null +++ b/tests/Jellyfin.LiveTv.Tests/Recordings/RecordingsMetadataManagerTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.IO; +using System.Threading.Tasks; +using System.Xml; +using Jellyfin.Extensions; +using Jellyfin.LiveTv.Recordings; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.LiveTv; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Jellyfin.LiveTv.Tests.Recordings; + +public sealed class RecordingsMetadataManagerTests +{ + private readonly string _tempDir = + Path.Combine(Path.GetTempPath(), "jellyfin-test-" + Guid.NewGuid()); + + [Fact] + public async Task SaveRecordingMetadata_DateAddedIsUtc() + { + Directory.CreateDirectory(_tempDir); + var recordingPath = Path.Combine(_tempDir, "test-recording.ts"); + FileHelper.CreateEmpty(recordingPath); + + var config = new Mock(); + config.Setup(c => c.GetConfiguration("livetv")) + .Returns(new LiveTvOptions { SaveRecordingNFO = true, SaveRecordingImages = false }); + config.Setup(c => c.GetConfiguration("xbmcmetadata")) + .Returns(new XbmcMetadataOptions()); + + var libraryManager = new Mock(); + libraryManager + .Setup(l => l.GetItemList(It.IsAny())) + .Returns(Array.Empty()); + + var manager = new RecordingsMetadataManager( + NullLogger.Instance, + config.Object, + libraryManager.Object); + + var timer = new TimerInfo { Name = "Test Recording", ProgramId = null }; + + var beforeUtc = DateTime.UtcNow.AddSeconds(-2); + await manager.SaveRecordingMetadata(timer, recordingPath, null); + var afterUtc = DateTime.UtcNow.AddSeconds(2); + + var doc = new XmlDocument(); + doc.Load(Path.ChangeExtension(recordingPath, ".nfo")); + var dateAddedText = doc.SelectSingleNode("//dateadded")?.InnerText ?? string.Empty; + var parsed = DateTime.ParseExact( + dateAddedText, + "yyyy-MM-dd HH:mm:ss", + CultureInfo.InvariantCulture); + + Assert.InRange(parsed, beforeUtc, afterUtc); + } +} From 2c66447f08f740193c4dd4f340691d2cdb07ea49 Mon Sep 17 00:00:00 2001 From: Gargotaire Date: Mon, 18 May 2026 16:43:32 -0400 Subject: [PATCH 29/63] Translated using Weblate (Catalan) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ca/ --- Emby.Server.Implementations/Localization/Core/ca.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 1465a4d5e1..6c81726ee6 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.", "CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.", "CleanupUserDataTask": "Tasca de neteja de dades d'usuari", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "No s'han pogut descarregar les lletres des de {0} per a {1}" } From 2a689f268bc88ee7ab7e25121a6d43f71c1f8a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= Date: Sat, 21 Mar 2026 21:57:58 +0100 Subject: [PATCH 30/63] Embed external subtitles into MKV when transcoding Allow external subtitle files (SRT, ASS, PGS, etc.) to be muxed into MKV output containers when the device profile requests Embed delivery. Previously, the IsExternal guard in GetSubtitleProfile excluded external subtitles from Embed consideration entirely, forcing them to be served as separate sidecar files even when the output container supports embedding. Changes: - Extract CanConsiderEmbedSubtitle in StreamBuilder to allow external subs through when transcoding to MKV - Add external subtitle file as FFmpeg input (-i) for Embed delivery - Map external embedded subs from the correct FFmpeg input index - Fix external audio map index to account for the new subtitle input - Extract NeedsExternalSubtitleMuxing in EncodingHelper to deduplicate the external subtitle input check Fixes #16403 --- .../MediaEncoding/EncodingHelper.cs | 58 ++++++++++++------- MediaBrowser.Model/Dlna/StreamBuilder.cs | 15 ++++- .../Dlna/StreamBuilderTests.cs | 54 +++++++++++++++++ 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 65f6b79656..1fdb5fd4bd 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1267,22 +1267,23 @@ namespace MediaBrowser.Controller.MediaEncoding .Append(_mediaEncoder.GetInputPathArgument(state)); } - // sub2video for external graphical subtitles - if (state.SubtitleStream is not null - && ShouldEncodeSubtitle(state) - && !state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleStream.IsExternal) + if (NeedsExternalSubtitleMuxing(state)) { var subtitlePath = state.SubtitleStream.Path; - var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); + var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream; - // dvdsub/vobsub graphical subtitles use .sub+.idx pairs - if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) + if (isGraphicalBurnIn) { - var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); - if (File.Exists(idxFile)) + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); + + // dvdsub/vobsub graphical subtitles use .sub+.idx pairs + if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) { - subtitlePath = idxFile; + var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); + if (File.Exists(idxFile)) + { + subtitlePath = idxFile; + } } } @@ -1307,7 +1308,7 @@ namespace MediaBrowser.Controller.MediaEncoding arg.Append(' ').Append(seekSubParam); } - if (!string.IsNullOrEmpty(canvasArgs)) + if (isGraphicalBurnIn && !string.IsNullOrEmpty(canvasArgs)) { arg.Append(canvasArgs); } @@ -3072,11 +3073,8 @@ namespace MediaBrowser.Controller.MediaEncoding int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream); if (state.AudioStream.IsExternal) { - bool hasExternalGraphicsSubs = state.SubtitleStream is not null - && ShouldEncodeSubtitle(state) - && state.SubtitleStream.IsExternal - && !state.SubtitleStream.IsTextSubtitleStream; - int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1; + bool hasExternalSubAsInput = NeedsExternalSubtitleMuxing(state); + int externalAudioMapIndex = hasExternalSubAsInput ? 2 : 1; args += string.Format( CultureInfo.InvariantCulture, @@ -3104,12 +3102,20 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (subtitleMethod == SubtitleDeliveryMethod.Embed) { - int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream); + if (state.SubtitleStream.IsExternal) + { + // External subtitle file is added as second FFmpeg input + args += " -map 1:0"; + } + else + { + int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream); - args += string.Format( - CultureInfo.InvariantCulture, - " -map 0:{0}", - subtitleStreamIndex); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + subtitleStreamIndex); + } } else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { @@ -7886,6 +7892,14 @@ namespace MediaBrowser.Controller.MediaEncoding || (state.BaseRequest.AlwaysBurnInSubtitleWhenTranscoding && !IsCopyCodec(state.OutputVideoCodec)); } + private static bool NeedsExternalSubtitleMuxing(EncodingJobInfo state) + { + return state.SubtitleStream is not null + && state.SubtitleStream.IsExternal + && (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Embed + || (ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream)); + } + public static string GetVideoSyncOption(string videoSync, Version encoderVersion) { if (string.IsNullOrEmpty(videoSync)) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 44697837ca..2ccd2a6c28 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1451,7 +1451,7 @@ namespace MediaBrowser.Model.Dlna string? outputContainer, MediaStreamProtocol? transcodingSubProtocol) { - if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || transcodingSubProtocol != MediaStreamProtocol.hls)) + if (CanConsiderEmbedSubtitle(subtitleStream, playMethod, transcodingSubProtocol, outputContainer)) { // Look for supported embedded subs of the same format foreach (var profile in subtitleProfiles) @@ -1540,6 +1540,19 @@ namespace MediaBrowser.Model.Dlna return false; } + private static bool CanConsiderEmbedSubtitle(MediaStream subtitleStream, PlayMethod playMethod, MediaStreamProtocol? transcodingSubProtocol, string? outputContainer) + { + if (subtitleStream.IsExternal) + { + return playMethod == PlayMethod.Transcode + && transcodingSubProtocol != MediaStreamProtocol.hls + && IsSubtitleEmbedSupported(outputContainer); + } + + return playMethod != PlayMethod.Transcode + || transcodingSubProtocol != MediaStreamProtocol.hls; + } + private static SubtitleProfile? GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion) { foreach (var profile in subtitleProfiles) diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 0b103debad..16c586bcda 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -675,5 +675,59 @@ namespace Jellyfin.Model.Tests Assert.Equal(expectedMethod, result.Method); } + + [Theory] + // External text subs embedded into MKV when transcoding (#16403) + [InlineData("srt", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("ass", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + // External graphical subs embedded into MKV when transcoding + [InlineData("pgssub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("dvdsub", true, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + // External subs remain external when transcoding to non-MKV containers + [InlineData("srt", true, PlayMethod.Transcode, "mp4", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)] + [InlineData("srt", true, PlayMethod.Transcode, "ts", MediaStreamProtocol.hls, SubtitleDeliveryMethod.External)] + // External subs remain external during DirectPlay even with MKV + [InlineData("srt", true, PlayMethod.DirectPlay, "mkv", null, SubtitleDeliveryMethod.External)] + // Internal subs still embedded into MKV when transcoding (existing behavior) + [InlineData("srt", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + [InlineData("pgssub", false, PlayMethod.Transcode, "mkv", MediaStreamProtocol.http, SubtitleDeliveryMethod.Embed)] + public void GetSubtitleProfile_ReturnsExpectedDeliveryMethod( + string codec, + bool isExternal, + PlayMethod playMethod, + string outputContainer, + MediaStreamProtocol? transcodingSubProtocol, + SubtitleDeliveryMethod expectedMethod) + { + var mediaSource = new MediaSourceInfo(); + var subtitleStream = new MediaStream + { + Codec = codec, + Language = "eng", + IsExternal = isExternal, + Type = MediaStreamType.Subtitle, + SupportsExternalStream = true + }; + + var subtitleProfiles = new[] + { + new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.Embed }, + new SubtitleProfile { Format = codec, Method = SubtitleDeliveryMethod.External } + }; + + var transcoderSupport = new Mock(); + transcoderSupport.Setup(x => x.CanExtractSubtitles(It.IsAny())).Returns(true); + + var result = StreamBuilder.GetSubtitleProfile( + mediaSource, + subtitleStream, + subtitleProfiles, + playMethod, + transcoderSupport.Object, + outputContainer, + transcodingSubProtocol); + + Assert.Equal(expectedMethod, result.Method); + } } } From a15b426e73afb46d7337b98f8279e83847e20f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= Date: Tue, 24 Mar 2026 11:02:10 +0100 Subject: [PATCH 31/63] Fix external subtitle stream mapping for multi-stream containers Compute the in-file stream index for external subtitles instead of hardcoding -map 1:0. For single-stream files (SRT/ASS/VTT) the index is always 0, preserving existing behavior. For multi-stream containers like MKS, the correct track is selected by counting sibling streams that share the same Path. Add unit tests for GetMapArgs covering internal subs, external SRT, multiple external files, and multi-stream MKS containers. --- .../MediaEncoding/EncodingHelper.cs | 15 +- .../MediaEncoding/EncodingHelperTests.cs | 216 ++++++++++++++++++ 2 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 1fdb5fd4bd..40bb3913e9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3104,8 +3104,19 @@ namespace MediaBrowser.Controller.MediaEncoding { if (state.SubtitleStream.IsExternal) { - // External subtitle file is added as second FFmpeg input - args += " -map 1:0"; + // External subtitle file is added as second FFmpeg input. + // For single-stream files (SRT/ASS/VTT) the in-file index is always 0. + // For multi-stream containers (MKS) we count how many streams from + // the same file appear before the selected one. + var inFileIndex = state.MediaSource.MediaStreams + .Where(s => string.Equals(s.Path, state.SubtitleStream.Path, StringComparison.Ordinal)) + .TakeWhile(s => s.Index != state.SubtitleStream.Index) + .Count(); + + args += string.Format( + CultureInfo.InvariantCulture, + " -map 1:{0}", + inFileIndex); } else { diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs new file mode 100644 index 0000000000..e6276f0e9e --- /dev/null +++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Enums; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using Moq; +using Xunit; + +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; + +namespace Jellyfin.Controller.Tests.MediaEncoding +{ + public class EncodingHelperTests + { + [Fact] + public void GetMapArgs_NoSubtitle_ExcludesAllSubs() + { + var state = BuildState(subtitle: null, deliveryMethod: null); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map -0:s", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSrt_MapsFromPrimaryInput() + { + var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:2", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSubAtHigherIndex_MapsCorrectIndex() + { + var sub0 = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var sub1 = new MediaStream { Index = 3, Type = MediaStreamType.Subtitle, Codec = "ass" }; + var state = BuildState(sub1, SubtitleDeliveryMethod.Embed, additionalStreams: [sub0, sub1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:3", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_ExternalSrt_MapsFirstStreamFromInput1() + { + var sub = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0() + { + var ext1 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var ext2 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.fr.srt" + }; + var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]); + var args = CreateHelper().GetMapArgs(state); + + // Different file from ext1, so in-file index is 0 + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_MksFirstTrack_MapsInFileIndex0() + { + var mks0 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks0, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_MksSecondTrack_MapsInFileIndex1() + { + var mks0 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks2 = new MediaStream + { + Index = 4, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]); + var args = CreateHelper().GetMapArgs(state); + + // Second track in the same .mks file → in-file index 1 + Assert.Contains("-map 1:1", args, StringComparison.Ordinal); + } + + private static EncodingJobInfo BuildState( + MediaStream? subtitle, + SubtitleDeliveryMethod? deliveryMethod, + MediaStream[]? additionalStreams = null) + { + var video = new MediaStream { Index = 0, Type = MediaStreamType.Video, Codec = "h264" }; + var audio = new MediaStream { Index = 1, Type = MediaStreamType.Audio, Codec = "aac" }; + var streams = new List { video, audio }; + + if (additionalStreams is not null) + { + streams.AddRange(additionalStreams); + } + else if (subtitle is not null) + { + streams.Add(subtitle); + } + + return new EncodingJobInfo(TranscodingJobType.Progressive) + { + MediaSource = new MediaSourceInfo + { + Container = "mkv", + MediaStreams = streams, + }, + VideoStream = video, + AudioStream = audio, + SubtitleStream = subtitle, + SubtitleDeliveryMethod = deliveryMethod ?? SubtitleDeliveryMethod.Drop, + BaseRequest = new VideoRequestDto(), + IsVideoRequest = true, + IsInputVideo = true, + }; + } + + private static EncodingHelper CreateHelper() + { + var appPaths = Mock.Of(); + var mediaEncoder = new Mock(); + var subtitleEncoder = new Mock(); + var config = new Mock(); + var configurationManager = new Mock(); + var pathManager = new Mock(); + + return new EncodingHelper( + appPaths, + mediaEncoder.Object, + subtitleEncoder.Object, + config.Object, + configurationManager.Object, + pathManager.Object); + } + } +} From 405d987557b2638afc89edf3dff20360e39cb09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= Date: Tue, 24 Mar 2026 14:26:25 +0100 Subject: [PATCH 32/63] Normalize VobSub .sub to .idx for embedding, add EncodingHelper tests Move the .sub to .idx path normalization outside the burn-in check so it applies to subtitle embedding as well. ffmpeg requires the .idx file to read VobSub subtitles. Add unit tests for GetMapArgs and GetInputArgument covering internal subs, external SRT, multi-file SRT, multi-stream MKS containers, and VobSub .sub/.idx path normalization. --- .../MediaEncoding/EncodingHelper.cs | 16 +++---- .../MediaEncoding/EncodingHelperTests.cs | 47 ++++++++++++++++++- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 40bb3913e9..6ff2873cc5 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1272,18 +1272,14 @@ namespace MediaBrowser.Controller.MediaEncoding var subtitlePath = state.SubtitleStream.Path; var isGraphicalBurnIn = ShouldEncodeSubtitle(state) && !state.SubtitleStream.IsTextSubtitleStream; - if (isGraphicalBurnIn) + // dvdsub/vobsub graphical subtitles use .sub+.idx pairs + var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); + if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) { - var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan()); - - // dvdsub/vobsub graphical subtitles use .sub+.idx pairs - if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)) + var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); + if (File.Exists(idxFile)) { - var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); - if (File.Exists(idxFile)) - { - subtitlePath = idxFile; - } + subtitlePath = idxFile; } } diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs index e6276f0e9e..6563dd63ab 100644 --- a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs +++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using Jellyfin.Data.Enums; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Streaming; +using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -71,6 +73,8 @@ namespace Jellyfin.Controller.Tests.MediaEncoding [Fact] public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0() { + // Two separate .srt files — selecting the second one still maps 1:0 + // because Jellyfin feeds only the selected file as ffmpeg input 1. var ext1 = new MediaStream { Index = 2, @@ -92,7 +96,6 @@ namespace Jellyfin.Controller.Tests.MediaEncoding var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]); var args = CreateHelper().GetMapArgs(state); - // Different file from ext1, so in-file index is 0 Assert.Contains("-map 1:0", args, StringComparison.Ordinal); } @@ -156,10 +159,50 @@ namespace Jellyfin.Controller.Tests.MediaEncoding var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]); var args = CreateHelper().GetMapArgs(state); - // Second track in the same .mks file → in-file index 1 Assert.Contains("-map 1:1", args, StringComparison.Ordinal); } + [Theory] + [InlineData(SubtitleDeliveryMethod.Embed, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Encode, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Embed, false, "movie.sub")] + [InlineData(SubtitleDeliveryMethod.Encode, false, "movie.sub")] + public void GetInputArgument_VobSub_UsesCorrectPath( + SubtitleDeliveryMethod deliveryMethod, + bool createIdxFile, + string expectedFilename) + { + var tempDir = Directory.CreateTempSubdirectory("jellyfin-test-"); + try + { + var subFile = Path.Combine(tempDir.FullName, "movie.sub"); + File.WriteAllText(subFile, "dummy"); + + if (createIdxFile) + { + File.WriteAllText(Path.Combine(tempDir.FullName, "movie.idx"), "dummy"); + } + + var sub = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "dvdsub", + IsExternal = true, + SupportsExternalStream = true, + Path = subFile + }; + var state = BuildState(sub, deliveryMethod); + var inputArgs = CreateHelper().GetInputArgument(state, new EncodingOptions(), null); + + Assert.Contains(expectedFilename, inputArgs, StringComparison.Ordinal); + } + finally + { + tempDir.Delete(true); + } + } + private static EncodingJobInfo BuildState( MediaStream? subtitle, SubtitleDeliveryMethod? deliveryMethod, From f6af1a9fb626e4a2f39f0caceebb590340d54c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Nie=C5=82acny?= Date: Tue, 19 May 2026 13:01:25 +0200 Subject: [PATCH 33/63] Use file-scoped namespace in EncodingHelperTests --- .../MediaEncoding/EncodingHelperTests.cs | 435 +++++++++--------- 1 file changed, 217 insertions(+), 218 deletions(-) diff --git a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs index 6563dd63ab..d7ae6a8a18 100644 --- a/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs +++ b/tests/Jellyfin.Controller.Tests/MediaEncoding/EncodingHelperTests.cs @@ -16,244 +16,243 @@ using Xunit; using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; -namespace Jellyfin.Controller.Tests.MediaEncoding +namespace Jellyfin.Controller.Tests.MediaEncoding; + +public class EncodingHelperTests { - public class EncodingHelperTests + [Fact] + public void GetMapArgs_NoSubtitle_ExcludesAllSubs() { - [Fact] - public void GetMapArgs_NoSubtitle_ExcludesAllSubs() + var state = BuildState(subtitle: null, deliveryMethod: null); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map -0:s", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSrt_MapsFromPrimaryInput() + { + var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:2", args, StringComparison.Ordinal); + Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_InternalSubAtHigherIndex_MapsCorrectIndex() + { + var sub0 = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; + var sub1 = new MediaStream { Index = 3, Type = MediaStreamType.Subtitle, Codec = "ass" }; + var state = BuildState(sub1, SubtitleDeliveryMethod.Embed, additionalStreams: [sub0, sub1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 0:3", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_ExternalSrt_MapsFirstStreamFromInput1() + { + var sub = new MediaStream { - var state = BuildState(subtitle: null, deliveryMethod: null); - var args = CreateHelper().GetMapArgs(state); + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var state = BuildState(sub, SubtitleDeliveryMethod.Embed); + var args = CreateHelper().GetMapArgs(state); - Assert.Contains("-map -0:s", args, StringComparison.Ordinal); - Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); - } + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } - [Fact] - public void GetMapArgs_InternalSrt_MapsFromPrimaryInput() + [Fact] + public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0() + { + // Two separate .srt files — selecting the second one still maps 1:0 + // because Jellyfin feeds only the selected file as ffmpeg input 1. + var ext1 = new MediaStream { - var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; - var state = BuildState(sub, SubtitleDeliveryMethod.Embed); - var args = CreateHelper().GetMapArgs(state); - - Assert.Contains("-map 0:2", args, StringComparison.Ordinal); - Assert.DoesNotContain("-map 1:", args, StringComparison.Ordinal); - } - - [Fact] - public void GetMapArgs_InternalSubAtHigherIndex_MapsCorrectIndex() + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.en.srt" + }; + var ext2 = new MediaStream { - var sub0 = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, Codec = "srt" }; - var sub1 = new MediaStream { Index = 3, Type = MediaStreamType.Subtitle, Codec = "ass" }; - var state = BuildState(sub1, SubtitleDeliveryMethod.Embed, additionalStreams: [sub0, sub1]); - var args = CreateHelper().GetMapArgs(state); + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "srt", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.fr.srt" + }; + var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]); + var args = CreateHelper().GetMapArgs(state); - Assert.Contains("-map 0:3", args, StringComparison.Ordinal); - } + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } - [Fact] - public void GetMapArgs_ExternalSrt_MapsFirstStreamFromInput1() + [Fact] + public void GetMapArgs_MksFirstTrack_MapsInFileIndex0() + { + var mks0 = new MediaStream { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks0, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + } + + [Fact] + public void GetMapArgs_MksSecondTrack_MapsInFileIndex1() + { + var mks0 = new MediaStream + { + Index = 2, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks1 = new MediaStream + { + Index = 3, + Type = MediaStreamType.Subtitle, + Codec = "ass", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var mks2 = new MediaStream + { + Index = 4, + Type = MediaStreamType.Subtitle, + Codec = "subrip", + IsExternal = true, + SupportsExternalStream = true, + Path = "/media/movie.mks" + }; + var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]); + var args = CreateHelper().GetMapArgs(state); + + Assert.Contains("-map 1:1", args, StringComparison.Ordinal); + } + + [Theory] + [InlineData(SubtitleDeliveryMethod.Embed, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Encode, true, "movie.idx")] + [InlineData(SubtitleDeliveryMethod.Embed, false, "movie.sub")] + [InlineData(SubtitleDeliveryMethod.Encode, false, "movie.sub")] + public void GetInputArgument_VobSub_UsesCorrectPath( + SubtitleDeliveryMethod deliveryMethod, + bool createIdxFile, + string expectedFilename) + { + var tempDir = Directory.CreateTempSubdirectory("jellyfin-test-"); + try + { + var subFile = Path.Combine(tempDir.FullName, "movie.sub"); + File.WriteAllText(subFile, "dummy"); + + if (createIdxFile) + { + File.WriteAllText(Path.Combine(tempDir.FullName, "movie.idx"), "dummy"); + } + var sub = new MediaStream { Index = 2, Type = MediaStreamType.Subtitle, - Codec = "srt", + Codec = "dvdsub", IsExternal = true, SupportsExternalStream = true, - Path = "/media/movie.en.srt" + Path = subFile }; - var state = BuildState(sub, SubtitleDeliveryMethod.Embed); - var args = CreateHelper().GetMapArgs(state); + var state = BuildState(sub, deliveryMethod); + var inputArgs = CreateHelper().GetInputArgument(state, new EncodingOptions(), null); - Assert.Contains("-map 1:0", args, StringComparison.Ordinal); + Assert.Contains(expectedFilename, inputArgs, StringComparison.Ordinal); } - - [Fact] - public void GetMapArgs_SecondExternalSrt_StillMaps1Colon0() + finally { - // Two separate .srt files — selecting the second one still maps 1:0 - // because Jellyfin feeds only the selected file as ffmpeg input 1. - var ext1 = new MediaStream - { - Index = 2, - Type = MediaStreamType.Subtitle, - Codec = "srt", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.en.srt" - }; - var ext2 = new MediaStream - { - Index = 3, - Type = MediaStreamType.Subtitle, - Codec = "srt", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.fr.srt" - }; - var state = BuildState(ext2, SubtitleDeliveryMethod.Embed, additionalStreams: [ext1, ext2]); - var args = CreateHelper().GetMapArgs(state); - - Assert.Contains("-map 1:0", args, StringComparison.Ordinal); - } - - [Fact] - public void GetMapArgs_MksFirstTrack_MapsInFileIndex0() - { - var mks0 = new MediaStream - { - Index = 2, - Type = MediaStreamType.Subtitle, - Codec = "subrip", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.mks" - }; - var mks1 = new MediaStream - { - Index = 3, - Type = MediaStreamType.Subtitle, - Codec = "ass", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.mks" - }; - var state = BuildState(mks0, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1]); - var args = CreateHelper().GetMapArgs(state); - - Assert.Contains("-map 1:0", args, StringComparison.Ordinal); - } - - [Fact] - public void GetMapArgs_MksSecondTrack_MapsInFileIndex1() - { - var mks0 = new MediaStream - { - Index = 2, - Type = MediaStreamType.Subtitle, - Codec = "subrip", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.mks" - }; - var mks1 = new MediaStream - { - Index = 3, - Type = MediaStreamType.Subtitle, - Codec = "ass", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.mks" - }; - var mks2 = new MediaStream - { - Index = 4, - Type = MediaStreamType.Subtitle, - Codec = "subrip", - IsExternal = true, - SupportsExternalStream = true, - Path = "/media/movie.mks" - }; - var state = BuildState(mks1, SubtitleDeliveryMethod.Embed, additionalStreams: [mks0, mks1, mks2]); - var args = CreateHelper().GetMapArgs(state); - - Assert.Contains("-map 1:1", args, StringComparison.Ordinal); - } - - [Theory] - [InlineData(SubtitleDeliveryMethod.Embed, true, "movie.idx")] - [InlineData(SubtitleDeliveryMethod.Encode, true, "movie.idx")] - [InlineData(SubtitleDeliveryMethod.Embed, false, "movie.sub")] - [InlineData(SubtitleDeliveryMethod.Encode, false, "movie.sub")] - public void GetInputArgument_VobSub_UsesCorrectPath( - SubtitleDeliveryMethod deliveryMethod, - bool createIdxFile, - string expectedFilename) - { - var tempDir = Directory.CreateTempSubdirectory("jellyfin-test-"); - try - { - var subFile = Path.Combine(tempDir.FullName, "movie.sub"); - File.WriteAllText(subFile, "dummy"); - - if (createIdxFile) - { - File.WriteAllText(Path.Combine(tempDir.FullName, "movie.idx"), "dummy"); - } - - var sub = new MediaStream - { - Index = 2, - Type = MediaStreamType.Subtitle, - Codec = "dvdsub", - IsExternal = true, - SupportsExternalStream = true, - Path = subFile - }; - var state = BuildState(sub, deliveryMethod); - var inputArgs = CreateHelper().GetInputArgument(state, new EncodingOptions(), null); - - Assert.Contains(expectedFilename, inputArgs, StringComparison.Ordinal); - } - finally - { - tempDir.Delete(true); - } - } - - private static EncodingJobInfo BuildState( - MediaStream? subtitle, - SubtitleDeliveryMethod? deliveryMethod, - MediaStream[]? additionalStreams = null) - { - var video = new MediaStream { Index = 0, Type = MediaStreamType.Video, Codec = "h264" }; - var audio = new MediaStream { Index = 1, Type = MediaStreamType.Audio, Codec = "aac" }; - var streams = new List { video, audio }; - - if (additionalStreams is not null) - { - streams.AddRange(additionalStreams); - } - else if (subtitle is not null) - { - streams.Add(subtitle); - } - - return new EncodingJobInfo(TranscodingJobType.Progressive) - { - MediaSource = new MediaSourceInfo - { - Container = "mkv", - MediaStreams = streams, - }, - VideoStream = video, - AudioStream = audio, - SubtitleStream = subtitle, - SubtitleDeliveryMethod = deliveryMethod ?? SubtitleDeliveryMethod.Drop, - BaseRequest = new VideoRequestDto(), - IsVideoRequest = true, - IsInputVideo = true, - }; - } - - private static EncodingHelper CreateHelper() - { - var appPaths = Mock.Of(); - var mediaEncoder = new Mock(); - var subtitleEncoder = new Mock(); - var config = new Mock(); - var configurationManager = new Mock(); - var pathManager = new Mock(); - - return new EncodingHelper( - appPaths, - mediaEncoder.Object, - subtitleEncoder.Object, - config.Object, - configurationManager.Object, - pathManager.Object); + tempDir.Delete(true); } } + + private static EncodingJobInfo BuildState( + MediaStream? subtitle, + SubtitleDeliveryMethod? deliveryMethod, + MediaStream[]? additionalStreams = null) + { + var video = new MediaStream { Index = 0, Type = MediaStreamType.Video, Codec = "h264" }; + var audio = new MediaStream { Index = 1, Type = MediaStreamType.Audio, Codec = "aac" }; + var streams = new List { video, audio }; + + if (additionalStreams is not null) + { + streams.AddRange(additionalStreams); + } + else if (subtitle is not null) + { + streams.Add(subtitle); + } + + return new EncodingJobInfo(TranscodingJobType.Progressive) + { + MediaSource = new MediaSourceInfo + { + Container = "mkv", + MediaStreams = streams, + }, + VideoStream = video, + AudioStream = audio, + SubtitleStream = subtitle, + SubtitleDeliveryMethod = deliveryMethod ?? SubtitleDeliveryMethod.Drop, + BaseRequest = new VideoRequestDto(), + IsVideoRequest = true, + IsInputVideo = true, + }; + } + + private static EncodingHelper CreateHelper() + { + var appPaths = Mock.Of(); + var mediaEncoder = new Mock(); + var subtitleEncoder = new Mock(); + var config = new Mock(); + var configurationManager = new Mock(); + var pathManager = new Mock(); + + return new EncodingHelper( + appPaths, + mediaEncoder.Object, + subtitleEncoder.Object, + config.Object, + configurationManager.Object, + pathManager.Object); + } } From e805cd4df70dfe6053ef29963c94314ed292392b Mon Sep 17 00:00:00 2001 From: Fjuro Date: Tue, 19 May 2026 09:53:01 -0400 Subject: [PATCH 34/63] Translated using Weblate (Czech) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/cs/ --- Emby.Server.Implementations/Localization/Core/cs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 9e3d4456a8..28f0e2df97 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.", "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.", "CleanupUserDataTask": "Pročistit uživatelská data", - "Original": "Originál" + "Original": "Originál", + "LyricDownloadFailureFromForItem": "Nepodařilo se stáhnout texty pro {1} ze služby {0}" } From 8384ef8a04fcee5942b0325a8bac6751117109c8 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 19 May 2026 17:29:27 -0500 Subject: [PATCH 35/63] Use --batch --yes for installing jellyfin keyring in devcontainer Fixes the .devcontainer setup's install-ffmpeg.sh halting at File '/etc/apt/keyrings/jellyfin.gpg' exists. Overwrite? (y/N) --- .devcontainer/install-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/install-ffmpeg.sh b/.devcontainer/install-ffmpeg.sh index 1e58e6ef44..1344634630 100644 --- a/.devcontainer/install-ffmpeg.sh +++ b/.devcontainer/install-ffmpeg.sh @@ -15,7 +15,7 @@ sudo apt-get install software-properties-common -y sudo add-apt-repository universe -y sudo mkdir -p /etc/apt/keyrings -curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg +curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/jellyfin.gpg export VERSION_OS="$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release )" export VERSION_CODENAME="$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )" export DPKG_ARCHITECTURE="$( dpkg --print-architecture )" From 9dfcc0918f797a0937b688426232e17898648f94 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 20 May 2026 11:34:45 +0200 Subject: [PATCH 36/63] Add support for filtering boxsets by parentId --- Jellyfin.Api/Controllers/ItemsController.cs | 8 ++++++++ .../Item/BaseItemRepository.TranslateQuery.cs | 9 +++++++++ MediaBrowser.Controller/Entities/InternalItemsQuery.cs | 7 +++++++ 3 files changed, 24 insertions(+) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 43e4737694..f3f0bd2482 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -280,10 +280,17 @@ public class ItemsController : BaseJellyfinApiController var item = _libraryManager.GetParentItem(parentId, userId); QueryResult result; + Guid[] boxSetLinkedChildAncestorIds = []; if (includeItemTypes.Length == 1 && includeItemTypes[0] == BaseItemKind.BoxSet && item is not BoxSet) { + var isBoxSetsLibrary = item is IHasCollectionType hct && hct.CollectionType == CollectionType.boxsets; + if (parentId.HasValue && item is not UserRootFolder && !isBoxSetsLibrary) + { + boxSetLinkedChildAncestorIds = [parentId.Value]; + } + parentId = null; item = _libraryManager.GetUserRootFolder(); } @@ -405,6 +412,7 @@ public class ItemsController : BaseJellyfinApiController MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), AudioLanguages = audioLanguages, SubtitleLanguages = subtitleLanguages, + LinkedChildAncestorIds = boxSetLinkedChildAncestorIds, }; if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs index 624b1b561c..f33a65a703 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.TranslateQuery.cs @@ -1027,6 +1027,15 @@ public sealed partial class BaseItemRepository baseQuery = baseQuery.Where(e => e.Parents!.AsQueryable().Any(ancestorFilter)); } + if (filter.LinkedChildAncestorIds.Length > 0) + { + // Keep folder-like items (BoxSets, Playlists) whose linked children descend from any of the requested ancestor ids. + var linkedChildAncestorIds = filter.LinkedChildAncestorIds; + baseQuery = baseQuery.Where(e => context.LinkedChildren.Any(lc => + lc.ParentId == e.Id + && lc.Child!.Parents!.Any(a => linkedChildAncestorIds.Contains(a.ParentItemId)))); + } + if (!string.IsNullOrWhiteSpace(filter.AncestorWithPresentationUniqueKey)) { baseQuery = baseQuery diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 1e5b5aa164..422c40ce5d 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -21,6 +21,7 @@ namespace MediaBrowser.Controller.Entities AlbumArtistIds = []; AlbumIds = []; AncestorIds = []; + LinkedChildAncestorIds = []; ArtistIds = []; BlockUnratedItems = []; BoxSetLibraryFolders = []; @@ -265,6 +266,12 @@ namespace MediaBrowser.Controller.Entities public Guid[] AncestorIds { get; set; } + /// + /// Gets or sets a list of ancestor ids that the item's linked children must descend from. + /// Useful for filtering BoxSets/Playlists to only those that contain items from a specific library. + /// + public Guid[] LinkedChildAncestorIds { get; set; } + public Guid[] TopParentIds { get; set; } public CollectionType?[] PresetViews { get; set; } From 5bc7969c2c01ebbad4b1d2402070ba2e590ae15c Mon Sep 17 00:00:00 2001 From: Ekaterine Papava Date: Wed, 20 May 2026 02:13:22 -0400 Subject: [PATCH 37/63] Translated using Weblate (Georgian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ka/ --- Emby.Server.Implementations/Localization/Core/ka.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index c0b7a196f7..d7649c1236 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -2,7 +2,7 @@ "Genres": "ჟანრები", "TasksApplicationCategory": "აპლიკაცია", "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}", - "Artists": "არტისტი", + "Artists": "შემსრულებლები", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", "Forced": "იძულებითი", @@ -33,7 +33,7 @@ "TasksChannelsCategory": "ინტერნეტ-არხები", "TaskRefreshChannelsDescription": "ინტერნეტ-არხის ინფორმაციის განახლება.", "Collections": "კოლექციები", - "Default": "ნაგულისხმები", + "Default": "ნაგულისხმევი", "Favorites": "რჩეულები", "Folders": "საქაღალდეები", "HeaderFavoriteShows": "რჩეული სერიალები", @@ -106,5 +106,7 @@ "TaskMoveTrickplayImages": "Trickplay სურათების მიგრაცია", "TaskMoveTrickplayImagesDescription": "გადააქვს trickplay ფაილები ბიბლიოთეკის პარამეტრებზე დაყრდნობით.", "CleanupUserDataTask": "მომხმარებლების მონაცემების გასუფთავება", - "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ." + "CleanupUserDataTaskDescription": "ასუფთავებს მომხმარებლების მონაცემებს (ყურების სტატუსი, ფავორიტები ანდ ა.შ) მედია ელემენტებისთვის რომლების 90 დღეზე მეტია აღარ არსებობენ.", + "LyricDownloadFailureFromForItem": "{1}-ისთვის {0}-დან ლირიკის გადმოწერა ჩავარდა", + "Original": "ორიგინალი" } From afb4d180d9f9174be754045d30e7388394f21488 Mon Sep 17 00:00:00 2001 From: Landyn Frisby Date: Wed, 20 May 2026 01:53:14 -0400 Subject: [PATCH 38/63] Translated using Weblate (Maori) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/mi/ --- Emby.Server.Implementations/Localization/Core/mi.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/mi.json b/Emby.Server.Implementations/Localization/Core/mi.json index 000e82ebdf..74fae52dca 100644 --- a/Emby.Server.Implementations/Localization/Core/mi.json +++ b/Emby.Server.Implementations/Localization/Core/mi.json @@ -2,5 +2,14 @@ "AppDeviceValues": "Taupānga: {0}, Pūrere: {1}", "Artists": "Kaiwaiata", "AuthenticationSucceededWithUserName": "{0} has been successfully authenticated", - "Books": "Ngā pukapuka" + "Books": "Ngā pukapuka", + "Default": "Taunoa", + "Collections": "Kohinga", + "External": "Waho", + "Folders": "Kōpaki", + "Forced": "Kaha", + "Music": "Waiata", + "Movies": "Kiriata", + "Latest": "Hou", + "Inherit": "Riro" } From 57bc534f40d5c67d97fd4dfebd1f7d4692d3e5b9 Mon Sep 17 00:00:00 2001 From: Klomer Date: Wed, 20 May 2026 12:49:50 -0400 Subject: [PATCH 39/63] Added translation using Weblate (Breton) --- Emby.Server.Implementations/Localization/Core/br.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 Emby.Server.Implementations/Localization/Core/br.json diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -0,0 +1 @@ +{} From 9f7eee2592624328dac5649a553e3a2861927cde Mon Sep 17 00:00:00 2001 From: Klomer Date: Wed, 20 May 2026 12:58:35 -0400 Subject: [PATCH 40/63] Translated using Weblate (Breton) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/br/ --- .../Localization/Core/br.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json index 0967ef424b..44ac1be32e 100644 --- a/Emby.Server.Implementations/Localization/Core/br.json +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -1 +1,19 @@ -{} +{ + "Artists": "Arzourien", + "AuthenticationSucceededWithUserName": "{0} kennasket gant berzh", + "Books": "Levrioù", + "ChapterNameValue": "Pennad {0}", + "Collections": "Dastumadegoù", + "Default": "Dre ziouer", + "External": "Diavaez", + "FailedLoginAttemptWithUserName": "Kennaskañ c'hwitet gant {0}", + "Favorites": "Sinedoù", + "Folders": "Teuliadoù", + "Forced": "Rediet", + "Genres": "Doareoù", + "HeaderContinueWatching": "Kenderc'hel da sellet", + "HeaderFavoriteEpisodes": "Rannoù Karetañ", + "HeaderFavoriteShows": "Heuliadennoù Karetañ", + "HeaderLiveTV": "TV war-eeun", + "HeaderNextUp": "Da c'houde" +} From 3e5cf13968e1f8fbb998ff5f6b0be6faa4cf2886 Mon Sep 17 00:00:00 2001 From: Bas <44002186+854562@users.noreply.github.com> Date: Wed, 20 May 2026 14:33:26 -0400 Subject: [PATCH 41/63] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 898f5892c9..9aea3adc22 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "HeaderContinueWatching": "Verder kijken", + "HeaderContinueWatching": "Verderkijken", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderLiveTV": "Live-tv", From 325be80a2b1f6b2319d8c49aa89aaf7722008e10 Mon Sep 17 00:00:00 2001 From: Klomer Date: Wed, 20 May 2026 13:37:43 -0400 Subject: [PATCH 42/63] Translated using Weblate (Breton) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/br/ --- .../Localization/Core/br.json | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json index 44ac1be32e..9c379f1916 100644 --- a/Emby.Server.Implementations/Localization/Core/br.json +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -15,5 +15,25 @@ "HeaderFavoriteEpisodes": "Rannoù Karetañ", "HeaderFavoriteShows": "Heuliadennoù Karetañ", "HeaderLiveTV": "TV war-eeun", - "HeaderNextUp": "Da c'houde" + "HeaderNextUp": "Da c'houde", + "HearingImpaired": "Tud fall o C'hleved", + "HomeVideos": "Videoioù Personel", + "Inherit": "Hêrezhiñ", + "LabelIpAddressValue": "Chomlec'h IP : {0}", + "LabelRunningTimeValue": "Padelezh : {0}", + "Latest": "Diwezhañ", + "AppDeviceValues": "Arload : {0}, Trobarzhell : {1}", + "LyricDownloadFailureFromForItem": "C'hwitet eo pellgargañ ar c'homzoù eus {0} evit {1}", + "MixedContent": "Danvez mesket", + "Movies": "Filmoù", + "Music": "Sonerezh", + "MusicVideos": "Videoioù Sonerezh", + "NameInstallFailed": "{0} c'hwitet war ar staliadur", + "NameSeasonNumber": "Koulzad {0}", + "NameSeasonUnknown": "Koulzad Dianav", + "NewVersionIsAvailable": "Ur stumm Servijer Jellyfin nevez a c'haller pellgargañ.", + "NotificationOptionApplicationUpdateAvailable": "Hizivadur an arload zo da gaout", + "NotificationOptionApplicationUpdateInstalled": "Hizivadur an arload staliet", + "NotificationOptionAudioPlayback": "Lenn aodio lañset", + "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet" } From c6fd0be8922ce238b834c5cb8ca4e09fb5d8bba8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 07:02:03 +0000 Subject: [PATCH 43/63] Update actions/stale action to v10.3.0 --- .github/workflows/issue-stale.yml | 2 +- .github/workflows/pull-request-stale.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index 339fcf569e..d6372ef6f4 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index e114276c28..6f225a4714 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true From 2ed07155b2cf3d8f4b0c9b028dfdf4bb583b85dc Mon Sep 17 00:00:00 2001 From: Bijai Chandra Date: Thu, 21 May 2026 03:18:40 -0400 Subject: [PATCH 44/63] Translated using Weblate (Malayalam) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ml/ --- Emby.Server.Implementations/Localization/Core/ml.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index c43a5a7431..dbf2ed4648 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -98,5 +98,10 @@ "TaskAudioNormalization": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുക", "TaskAudioNormalizationDescription": "സാധാരണ ശബ്ദ നിലയിലെത്തിലെത്തിക്കുന്ന ഡാറ്റയ്ക്കായി ഫയലുകൾ സ്കാൻ ചെയ്യുക.", "TaskRefreshTrickplayImages": "ട്രിക്ക് പ്ലേ ചിത്രങ്ങൾ സൃഷ്ടിക്കുക", - "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു." + "TaskRefreshTrickplayImagesDescription": "പ്രവർത്തനക്ഷമമാക്കിയ ലൈബ്രറികളിൽ വീഡിയോകൾക്കായി ട്രിക്ക്പ്ലേ പ്രിവ്യൂകൾ സൃഷ്ടിക്കുന്നു.", + "Original": "ഓറിജിനൽ", + "TaskDownloadMissingLyrics": "ഇല്ലാത്ത വരികൾ ഡൗൺലോഡ് ചെയ്യുക", + "TaskDownloadMissingLyricsDescription": "പാട്ടുകളുടെ വരികൾ ഡൗൺലോഡ് ചെയ്യുന്നു", + "TaskExtractMediaSegments": "മീഡിയ സെഗ്‌മെന്റ് സ്‌കാൻ", + "TaskExtractMediaSegmentsDescription": "മീഡിയസെഗ്മെന്റ് പ്രാപ്തമാക്കിയ പ്ലഗിനുകളിൽ നിന്ന് മീഡിയ സെഗ്‌മെന്റുകൾ എക്‌സ്‌ട്രാക്റ്റുചെയ്യുന്നു അല്ലെങ്കിൽ നേടുന്നു." } From 51ff31cd4168e8e011b585031b182917c211016f Mon Sep 17 00:00:00 2001 From: Ekaterine Papava Date: Thu, 21 May 2026 02:21:53 -0400 Subject: [PATCH 45/63] Translated using Weblate (Georgian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ka/ --- Emby.Server.Implementations/Localization/Core/ka.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index d7649c1236..6b3aeb1218 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -37,18 +37,18 @@ "Favorites": "რჩეულები", "Folders": "საქაღალდეები", "HeaderFavoriteShows": "რჩეული სერიალები", - "HeaderLiveTV": "ლაივ ტელევიზია", + "HeaderLiveTV": "ცოცხალი ტელევიზია", "HeaderNextUp": "შემდეგი", "HomeVideos": "სახლის ვიდეოები", "NameSeasonNumber": "სეზონი {0}", "NameSeasonUnknown": "სეზონი უცნობია", - "NotificationOptionPluginError": "მოდულის შეცდომა", - "NotificationOptionPluginInstalled": "მოდული დაყენებულია", + "NotificationOptionPluginError": "დამატების შეცდომა", + "NotificationOptionPluginInstalled": "დამატება დაყენებულია", "NotificationOptionPluginUninstalled": "მოდული წაიშალა", - "ScheduledTaskFailedWithName": "{0} ვერ შესრულდა", + "ScheduledTaskFailedWithName": "{0} ჩავარდა", "TvShows": "სატელევიზიო სერიალები", "TaskRefreshPeople": "ხალხის განახლება", - "TaskUpdatePlugins": "მოდულების განახლება", + "TaskUpdatePlugins": "დამატებების განახლება", "TaskRefreshChannels": "არხების განახლება", "TaskOptimizeDatabase": "მონაცემთა ბაზის ოპტიმიზაცია", "TaskKeyframeExtractor": "საკვანძო კადრის გამომღები", From d65eafdfe2d6523e6acae4895d9c0625e168d53c Mon Sep 17 00:00:00 2001 From: aleksantero Date: Thu, 21 May 2026 06:42:10 -0400 Subject: [PATCH 46/63] Translated using Weblate (Finnish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fi/ --- Emby.Server.Implementations/Localization/Core/fi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index d08f652e58..d080eb0236 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti", "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.", "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä", - "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään." + "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään.", + "LyricDownloadFailureFromForItem": "Sanoitusten lataus kohteesta {0} kappaleelle {1} epäonnistui", + "Original": "Alkuperäinen" } From 49cc2f78990d2b78c0dabea327ddd79c8f90e2ef Mon Sep 17 00:00:00 2001 From: JPVenson Date: Thu, 21 May 2026 19:27:55 +0200 Subject: [PATCH 47/63] Modernize version display in UI (#16862) Modernize version display in UI --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 10 +++++++++- Jellyfin.Server/ServerSetupApp/index.mstemplate.html | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 05975929db..37bb1abe71 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -90,6 +90,13 @@ public sealed class SetupServer : IDisposable var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false); _startupUiRenderer = (await ParserOptionsBuilder.New() .WithTemplate(fileTemplate) + .WithFormatter( + (Version version, int arg) => + { + // version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually. + return version.ToString(arg); + }, + "ToString") .WithFormatter( (StartupLogTopic logEntry, IEnumerable children) => { @@ -237,6 +244,7 @@ public sealed class SetupServer : IDisposable }); }); + var version = typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version!; app.Run(async (context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; @@ -250,7 +258,7 @@ public sealed class SetupServer : IDisposable { { "isInReportingMode", _isUnhealthy }, { "retryValue", retryAfterValue }, - { "version", typeof(Emby.Server.Implementations.ApplicationHost).Assembly.GetName().Version! }, + { "version", version }, { "logs", startupLogEntries }, { "networkManagerReady", networkManager is not null }, { "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) } diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html index 890a77619d..5706ce1fac 100644 --- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html +++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html @@ -173,9 +173,9 @@
{{^IF isInReportingMode}} -

Jellyfin Server {{version}} still starting. Please wait.

+

Jellyfin Server {{version.ToString(2)}} still starting. Please wait.

{{#ELSE}} -

Jellyfin Server has encountered an error and was not able to start.

+

Jellyfin Server {{version.ToString(2)}} has encountered an error and was not able to start.

{{/ELSE}} {{/IF}} From 40485aa2f67c903baab2cdde939f4ee13c40b569 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 21 May 2026 13:30:24 -0400 Subject: [PATCH 48/63] Have device deletion take list of ids (#12834) * add delete multiple devices via id * make atomic * use Add * Update Jellyfin.Api/Controllers/DevicesController.cs Co-authored-by: JPVenson * remove model binding, use functional device selection * use singular id * use BadRequest instead --------- Co-authored-by: JPVenson --- Jellyfin.Api/Controllers/DevicesController.cs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index eadb8c9855..2bbfeb40b8 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Threading.Tasks; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; +using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Dtos; using Jellyfin.Data.Queries; using MediaBrowser.Common.Api; @@ -112,28 +116,31 @@ public class DevicesController : BaseJellyfinApiController } /// - /// Deletes a device. + /// Deletes devices. /// - /// Device Id. + /// Device Ids. /// Device deleted. - /// Device not found. - /// A on success, or a if the device could not be found. + /// A requested device is invalid. + /// A on success, or a if a requested device is invalid. [HttpDelete] [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task DeleteDevice([FromQuery, Required] string id) + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task DeleteDevice([FromQuery] string[] id) { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice is null) + var devices = id.Select(_deviceManager.GetDevice).ToArray(); + if (devices.Any(f => f is null)) { - return NotFound(); + return BadRequest(); } - var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }); - - foreach (var session in sessions.Items) + foreach (var device in devices) { - await _sessionManager.Logout(session).ConfigureAwait(false); + var sessions = _deviceManager.GetDevices(new DeviceQuery { DeviceId = device!.Id }); + + foreach (var session in sessions.Items) + { + await _sessionManager.Logout(session).ConfigureAwait(false); + } } return NoContent(); From 37350282cc18a977162dcf14fe9b6068d29d6edf Mon Sep 17 00:00:00 2001 From: jakobkukla Date: Thu, 16 Apr 2026 11:38:01 +0200 Subject: [PATCH 49/63] Run tree-wide dotnet format --- .../ExternalFiles/ExternalPathParser.cs | 2 +- .../AppBase/BaseConfigurationManager.cs | 2 +- .../Chapters/ChapterManager.cs | 10 +- .../Library/PathExtensions.cs | 2 +- .../Library/Resolvers/TV/SeriesResolver.cs | 2 +- .../Localization/LocalizationManager.cs | 4 +- .../Tasks/MediaSegmentExtractionTask.cs | 2 +- .../Session/SessionManager.cs | 2 +- Emby.Server.Implementations/SystemManager.cs | 10 +- Jellyfin.Api/Controllers/AudioController.cs | 24 +- .../Controllers/DynamicHlsController.cs | 76 ++-- Jellyfin.Api/Controllers/ItemsController.cs | 2 +- .../Controllers/UniversalAudioController.cs | 4 +- .../Controllers/UserViewsController.cs | 2 +- Jellyfin.Api/Controllers/VideosController.cs | 24 +- .../Helpers/FileStreamResponseHelpers.cs | 10 +- .../SystemInfoDtos/LibraryStorageDto.cs | 2 +- Jellyfin.Data/Enums/ActivityLogSortBy.cs | 2 +- .../Activity/ActivityManager.cs | 6 +- .../Consumers/Session/PlaybackStopLogger.cs | 6 +- .../Item/PeopleRepository.cs | 8 +- .../Security/AuthorizationContext.cs | 2 +- .../Filters/CachingOpenApiProvider.cs | 2 +- .../Migrations/MigrationOptions.cs | 2 +- .../Entities/Extensions.cs | 2 +- .../Entities/TagExtensions.cs | 2 +- .../Library/ItemResolveArgs.cs | 2 +- .../MediaEncoding/EncodingHelper.cs | 34 +- .../MediaSegments/IMediaSegmentProvider.cs | 2 +- .../SyncPlay/GroupStates/WaitingGroupState.cs | 3 +- .../Parsers/BaseItemXmlParser.cs | 380 +++++++++--------- .../Parsers/BoxSetXmlParser.cs | 36 +- .../Parsers/PlaylistXmlParser.cs | 30 +- .../Attachments/AttachmentExtractor.cs | 46 +-- .../Subtitles/SubtitleEncoder.cs | 28 +- .../Extensions/EnumerableExtensions.cs | 2 +- .../MediaSegmentGenerationRequest.cs | 2 +- .../SyncPlay/PlaybackRequestType.cs | 2 +- .../SyncPlayGroupDoesNotExistUpdate.cs | 2 +- .../SyncPlay/SyncPlayGroupJoinedUpdate.cs | 2 +- .../SyncPlay/SyncPlayGroupLeftUpdate.cs | 2 +- .../SyncPlayLibraryAccessDeniedUpdate.cs | 2 +- .../SyncPlay/SyncPlayNotInGroupUpdate.cs | 2 +- .../SyncPlay/SyncPlayPlayQueueUpdate.cs | 2 +- .../SyncPlay/SyncPlayStateUpdate.cs | 2 +- .../SyncPlay/SyncPlayUserJoinedUpdate.cs | 2 +- .../SyncPlay/SyncPlayUserLeftUpdate.cs | 2 +- MediaBrowser.Model/Users/UserPolicy.cs | 2 +- .../Books/OpenPackagingFormat/OpfProvider.cs | 2 +- ...icBrainzReleaseGroupExternalUrlProvider.cs | 2 +- .../MusicBrainzTrackExternalUrlProvider.cs | 2 +- .../TV/Zap2ItExternalUrlProvider.cs | 2 +- .../Parsers/MovieNfoParser.cs | 2 +- ...082930_MarkSegmentProviderIdNonNullable.cs | 2 +- .../20241020103111_LibraryDbMigration.cs | 2 +- .../20241111131257_AddedCustomDataKey.cs | 2 +- .../20241111135439_AddedCustomDataKeyKey.cs | 2 +- .../20241112152323_FixAncestorIdConfig.cs | 2 +- .../20241112232041_fixMediaStreams.cs | 2 +- .../20241112234144_FixMediaStreams2.cs | 2 +- .../20241113133548_EnforceUniqueItemValue.cs | 2 +- .../20250202021306_FixedCollation.cs | 2 +- ...20250204092455_MakeStartEndDateNullable.cs | 2 +- .../20250214031148_ChannelIdGuid.cs | 2 +- ...5026_AddInheritedParentalRatingSubValue.cs | 2 +- .../20250327101120_AddKeyframeData.cs | 2 +- .../20250327171413_AddHdr10PlusFlag.cs | 2 +- .../20250331182844_FixAttachmentMigration.cs | 2 +- .../Migrations/20250401142247_FixAncestors.cs | 2 +- .../20250405075612_FixItemValuesIndices.cs | 2 +- ...609115616_DetachUserDataInsteadOfDelete.cs | 2 +- ...2_BaseItemImageInfoDateModifiedNullable.cs | 2 +- .../20250714044826_ResetJournalMode.cs | 2 +- ...rParentChildRelationBaseItemWithCascade.cs | 2 +- .../20250925203415_ExtendPeopleMapKey.cs | 2 +- .../20260113102337_AddLinkedChildrenTable.cs | 2 +- .../20260113203012_ChangeOwnerIdToGuid.cs | 2 +- .../20260113233000_AddForeignKeyToOwnerId.cs | 2 +- .../20260113233500_DropExtraIdsColumn.cs | 2 +- ...114245_AddLatestItemsDateCreatedIndexes.cs | 2 +- .../20260118182305_AddIndicesToImageInfo.cs | 2 +- .../20260130232147_AddBaseItemNameIndex.cs | 2 +- .../20260206224832_IndexOptimizations.cs | 2 +- .../20260308123920_AddTypeCleanNameIndex.cs | 2 +- ...0504075755_AddPartialIndexForItemCounts.cs | 2 +- src/Jellyfin.Drawing.Skia/SkiaEncoder.cs | 26 +- .../Channels/ChannelManager.cs | 2 +- .../Listings/ListingsManager.cs | 4 +- .../Timers/ItemDataProvider.cs | 4 +- .../TunerHosts/TunerHostManager.cs | 2 +- .../Manager/NetworkManager.cs | 2 +- .../MediaInfo/FFProbeVideoInfoTests.cs | 18 +- 92 files changed, 466 insertions(+), 465 deletions(-) diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs index 3461b3c0d6..8e7da5db42 100644 --- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -70,7 +70,7 @@ namespace Emby.Naming.ExternalFiles if (lastSeparator == -1) { - break; + break; } string currentSlice = languageString[lastSeparator..]; diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index ef5fa8bef9..aa19948e36 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.AppBase } else { - _configurationFactories = [.._configurationFactories, factory]; + _configurationFactories = [.. _configurationFactories, factory]; } _configurationStores = _configurationFactories diff --git a/Emby.Server.Implementations/Chapters/ChapterManager.cs b/Emby.Server.Implementations/Chapters/ChapterManager.cs index 8a4721ce62..69cbe533c6 100644 --- a/Emby.Server.Implementations/Chapters/ChapterManager.cs +++ b/Emby.Server.Implementations/Chapters/ChapterManager.cs @@ -240,15 +240,15 @@ public class ChapterManager : IChapterManager public void SaveChapters(BaseItem item, IReadOnlyList chapters) { if (!Supports(item)) - { - _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); - return; - } + { + _logger.LogWarning("Attempted to save chapters for unsupported item type {Type}: {Name} ({Id})", item.GetType().Name, item.Name, item.Id); + return; + } // Remove any chapters that are outside of the runtime of the item var validChapters = chapters.Where(c => c.StartPositionTicks < item.RunTimeTicks).ToList(); _chapterRepository.SaveChapters(item.Id, validChapters); -} + } /// public ChapterInfo? GetChapter(Guid baseItemId, int index) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index cfa3e7c31d..7591359ea4 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Library '[' => ']', '(' => ')', '{' => '}', - _ => '\0' + _ => '\0' }; if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-')) { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index c81a0adb89..769d721665 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV ///
/// The logger. /// The naming options. - public SeriesResolver(ILogger logger, NamingOptions namingOptions) + public SeriesResolver(ILogger logger, NamingOptions namingOptions) { _logger = logger; _namingOptions = namingOptions; diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 0b0b300d30..843e35afcc 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -318,13 +318,13 @@ namespace Emby.Server.Implementations.Localization // A lot of countries don't explicitly have a separate rating for adult content if (ratings.All(x => x.RatingScore?.Score != 1000)) { - ratings.Add(new ParentalRating("XXX", new(1000, null))); + ratings.Add(new ParentalRating("XXX", new(1000, null))); } // A lot of countries don't explicitly have a separate rating for banned content if (ratings.All(x => x.RatingScore?.Score != 1001)) { - ratings.Add(new ParentalRating("Banned", new(1001, null))); + ratings.Add(new ParentalRating("Banned", new(1001, null))); } return [.. ratings.OrderBy(r => r.RatingScore?.Score).ThenBy(r => r.RatingScore?.SubScore)]; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs index 51920c5b14..5e92808f78 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 2885b89e3a..5148b62655 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -386,7 +386,7 @@ namespace Emby.Server.Implementations.Session { if (session is null) { - return; + return; } if (string.IsNullOrEmpty(info.MediaSourceId)) diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs index d140426ddf..11a94648f8 100644 --- a/Emby.Server.Implementations/SystemManager.cs +++ b/Emby.Server.Implementations/SystemManager.cs @@ -89,11 +89,11 @@ public class SystemManager : ISystemManager .GetVirtualFolders() .Where(e => !string.IsNullOrWhiteSpace(e.ItemId)) // this should not be null but for some users it is. .Select(e => new LibraryStorageInfo() - { - Id = Guid.Parse(e.ItemId), - Name = e.Name, - Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() - }); + { + Id = Guid.Parse(e.ItemId), + Name = e.Name, + Folders = e.Locations.Select(f => StorageHelper.GetFreeSpaceOf(f)).ToArray() + }); return new SystemStorageInfo() { diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 590bd05da4..77bb6ee7e7 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -91,18 +91,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -112,7 +112,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -131,8 +131,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -255,18 +255,18 @@ public class AudioController : BaseJellyfinApiController [ProducesAudioFile] public async Task GetAudioStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -276,7 +276,7 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -295,8 +295,8 @@ public class AudioController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index c059f5880d..838f48949d 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -167,18 +167,18 @@ public class DynamicHlsController : BaseJellyfinApiController [ProducesPlaylistFile] public async Task GetLiveHlsStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -188,7 +188,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -207,8 +207,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -413,12 +413,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -428,7 +428,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -449,8 +449,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -586,12 +586,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery, Required] string mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -602,7 +602,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -621,8 +621,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -753,12 +753,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -768,7 +768,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -789,8 +789,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -922,12 +922,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -938,7 +938,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -957,8 +957,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1092,7 +1092,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1100,12 +1100,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1115,7 +1115,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1136,8 +1136,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -1274,7 +1274,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromRoute, Required] Guid itemId, [FromRoute, Required] string playlistId, [FromRoute, Required] int segmentId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery, Required] long runtimeTicks, [FromQuery, Required] long actualSegmentLengthTicks, [FromQuery] bool? @static, @@ -1282,12 +1282,12 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -1298,7 +1298,7 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -1317,8 +1317,8 @@ public class DynamicHlsController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index f3f0bd2482..82a1bdebd7 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -271,7 +271,7 @@ public class ItemsController : BaseJellyfinApiController && user.GetPreference(PreferenceKind.AllowedTags).Length != 0 && !fields.Contains(ItemFields.Tags)) { - fields = [..fields, ItemFields.Tags]; + fields = [.. fields, ItemFields.Tags]; } var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index d4e9b234c5..2f5ed327c0 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -102,13 +102,13 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, [FromQuery] Guid? userId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] int? maxAudioChannels, [FromQuery] int? transcodingAudioChannels, [FromQuery] int? maxStreamingBitrate, [FromQuery] int? audioBitRate, [FromQuery] long? startTimeTicks, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? transcodingContainer, [FromQuery] MediaStreamProtocol? transcodingProtocol, [FromQuery] int? maxAudioSampleRate, [FromQuery] int? maxAudioBitDepth, diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index c1d06bad36..8b359c48af 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -88,7 +88,7 @@ public class UserViewsController : BaseJellyfinApiController var folders = _userViewManager.GetUserViews(query); var dtoOptions = new DtoOptions(); - dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; + dtoOptions.Fields = [.. dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId]; var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user)); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 2c2cbf1ec6..ed6d3f5bde 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -317,18 +317,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public async Task GetVideoStream( [FromRoute, Required] Guid itemId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery, ParameterObsolete] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -338,7 +338,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -359,8 +359,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, @@ -555,18 +555,18 @@ public class VideosController : BaseJellyfinApiController [ProducesVideoFile] public Task GetVideoStreamByContainer( [FromRoute, Required] Guid itemId, - [FromRoute, Required] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, + [FromRoute, Required][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string container, [FromQuery] bool? @static, [FromQuery] string? @params, [FromQuery] string? tag, [FromQuery] string? deviceProfileId, [FromQuery] string? playSessionId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? segmentContainer, [FromQuery] int? segmentLength, [FromQuery] int? minSegments, [FromQuery] string? mediaSourceId, [FromQuery] string? deviceId, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? audioCodec, [FromQuery] bool? enableAutoStreamCopy, [FromQuery] bool? allowVideoStreamCopy, [FromQuery] bool? allowAudioStreamCopy, @@ -576,7 +576,7 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? audioChannels, [FromQuery] int? maxAudioChannels, [FromQuery] string? profile, - [FromQuery] [RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, + [FromQuery][RegularExpression(EncodingHelper.LevelValidationRegexStr)] string? level, [FromQuery] float? framerate, [FromQuery] float? maxFramerate, [FromQuery] bool? copyTimestamps, @@ -597,8 +597,8 @@ public class VideosController : BaseJellyfinApiController [FromQuery] int? cpuCoreLimit, [FromQuery] string? liveStreamId, [FromQuery] bool? enableMpegtsM2TsMode, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, - [FromQuery] [RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? videoCodec, + [FromQuery][RegularExpression(EncodingHelper.ContainerValidationRegexStr)] string? subtitleCodec, [FromQuery] string? transcodeReasons, [FromQuery] int? audioStreamIndex, [FromQuery] int? videoStreamIndex, diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 4034a80887..d123dbc82e 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -62,12 +62,12 @@ public static class FileStreamResponseHelpers if (response.Headers.TryGetValues(HeaderNames.AcceptRanges, out var acceptRangesHeaders)) { // Prefer upstream server's Accept-Ranges header if available - acceptRangesValue = string.Join(", ", acceptRangesHeaders); - upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase); + acceptRangesValue = string.Join(", ", acceptRangesHeaders); + upstreamSupportsRange |= acceptRangesValue.Contains("bytes", StringComparison.OrdinalIgnoreCase); } else if (upstreamSupportsRange) // If we got 206 but no Accept-Ranges header, assume bytes { - acceptRangesValue = "bytes"; + acceptRangesValue = "bytes"; } // Set Accept-Ranges header for the client based on upstream support @@ -76,13 +76,13 @@ public static class FileStreamResponseHelpers // Set Content-Range header if upstream provided it (implies partial content) if (response.Content.Headers.ContentRange is not null) { - httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString(); + httpContext.Response.Headers[HeaderNames.ContentRange] = response.Content.Headers.ContentRange.ToString(); } // Set Content-Length header. For partial content, this is the length of the partial segment. if (response.Content.Headers.ContentLength.HasValue) { - httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; + httpContext.Response.ContentLength = response.Content.Headers.ContentLength.Value; } // Set Content-Type header diff --git a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs index c138324d2e..6e4ba91133 100644 --- a/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs +++ b/Jellyfin.Api/Models/SystemInfoDtos/LibraryStorageDto.cs @@ -10,7 +10,7 @@ namespace Jellyfin.Api.Models.SystemInfoDtos; ///
public record LibraryStorageDto { - /// + /// /// Gets or sets the Library Id. /// public required Guid Id { get; set; } diff --git a/Jellyfin.Data/Enums/ActivityLogSortBy.cs b/Jellyfin.Data/Enums/ActivityLogSortBy.cs index d6d44e8c07..a24185e365 100644 --- a/Jellyfin.Data/Enums/ActivityLogSortBy.cs +++ b/Jellyfin.Data/Enums/ActivityLogSortBy.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Data.Enums; +namespace Jellyfin.Data.Enums; /// /// Activity log sorting options. diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs index fe987b9d86..ba24dc3864 100644 --- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -58,9 +58,9 @@ public class ActivityManager : IActivityManager { // TODO switch to LeftJoin in .NET 10. var entries = from a in dbContext.ActivityLogs - join u in dbContext.Users on a.UserId equals u.Id into ugj - from u in ugj.DefaultIfEmpty() - select new ExpandedActivityLog { ActivityLog = a, Username = u.Username }; + join u in dbContext.Users on a.UserId equals u.Id into ugj + from u in ugj.DefaultIfEmpty() + select new ExpandedActivityLog { ActivityLog = a, Username = u.Username }; if (query.HasUserId is not null) { diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs index 09d68e4451..a88904c727 100644 --- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs +++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs @@ -75,9 +75,9 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session eventArgs.DeviceName), notificationType, user.Id) - { - ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), - }) + { + ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture), + }) .ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs index 8f8741d00f..b612112d49 100644 --- a/Jellyfin.Server.Implementations/Item/PeopleRepository.cs +++ b/Jellyfin.Server.Implementations/Item/PeopleRepository.cs @@ -110,10 +110,10 @@ public class PeopleRepository(IDbContextFactory dbProvider, I using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); var existingPersons = context.Peoples.Select(e => new - { - item = e, - SelectionKey = e.Name.ToLower() + "-" + e.PersonType - }) + { + item = e, + SelectionKey = e.Name.ToLower() + "-" + e.PersonType + }) .Where(p => personKeys.Contains(p.SelectionKey)) .Select(f => f.item) .ToArray(); diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index e3fe517c49..8657cb7dbb 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -302,7 +302,7 @@ namespace Jellyfin.Server.Implementations.Security } else if (!escaped && token == '=') { - key = authorizationHeader[start.. i].Trim().ToString(); + key = authorizationHeader[start..i].Trim().ToString(); start = i + 1; } } diff --git a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs index fdc49a9840..c9fd031ef9 100644 --- a/Jellyfin.Server/Filters/CachingOpenApiProvider.cs +++ b/Jellyfin.Server/Filters/CachingOpenApiProvider.cs @@ -68,7 +68,7 @@ internal sealed class CachingOpenApiProvider : ISwaggerProvider try { - openApiDocument = _swaggerGenerator.GetSwagger(documentName); + openApiDocument = _swaggerGenerator.GetSwagger(documentName); } catch (Exception ex) { diff --git a/Jellyfin.Server/Migrations/MigrationOptions.cs b/Jellyfin.Server/Migrations/MigrationOptions.cs index c9710f1fd1..cd1b74a613 100644 --- a/Jellyfin.Server/Migrations/MigrationOptions.cs +++ b/Jellyfin.Server/Migrations/MigrationOptions.cs @@ -16,7 +16,7 @@ namespace Jellyfin.Server.Migrations Applied = new List<(Guid Id, string Name)>(); } -// .Net xml serializer can't handle interfaces + // .Net xml serializer can't handle interfaces #pragma warning disable CA1002 // Do not expose generic lists /// /// Gets the list of applied migration routine names. diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index c56603a3eb..380041af84 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Controller.Entities } else { - item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl]; + item.RemoteTrailers = [.. item.RemoteTrailers, mediaUrl]; } } } diff --git a/MediaBrowser.Controller/Entities/TagExtensions.cs b/MediaBrowser.Controller/Entities/TagExtensions.cs index c1e4d1db2f..4ddba9835b 100644 --- a/MediaBrowser.Controller/Entities/TagExtensions.cs +++ b/MediaBrowser.Controller/Entities/TagExtensions.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Controller.Entities } else { - item.Tags = [..current, name]; + item.Tags = [.. current, name]; } } } diff --git a/MediaBrowser.Controller/Library/ItemResolveArgs.cs b/MediaBrowser.Controller/Library/ItemResolveArgs.cs index b558ef73d5..c5e7ae4913 100644 --- a/MediaBrowser.Controller/Library/ItemResolveArgs.cs +++ b/MediaBrowser.Controller/Library/ItemResolveArgs.cs @@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.Library get { var paths = string.IsNullOrEmpty(Path) ? Array.Empty() : [Path]; - return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations]; + return AdditionalLocations is null ? paths : [.. paths, .. AdditionalLocations]; } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 6ff2873cc5..9b6ec207d8 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1763,13 +1763,13 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -preset p7", - EncoderPreset.slower => " -preset p6", - EncoderPreset.slow => " -preset p5", - EncoderPreset.medium => " -preset p4", - EncoderPreset.fast => " -preset p3", - EncoderPreset.faster => " -preset p2", - _ => " -preset p1" + EncoderPreset.veryslow => " -preset p7", + EncoderPreset.slower => " -preset p6", + EncoderPreset.slow => " -preset p5", + EncoderPreset.medium => " -preset p4", + EncoderPreset.fast => " -preset p3", + EncoderPreset.faster => " -preset p2", + _ => " -preset p1" }; } else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf) @@ -1779,11 +1779,11 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -quality quality", - EncoderPreset.slower => " -quality quality", - EncoderPreset.slow => " -quality quality", - EncoderPreset.medium => " -quality balanced", - _ => " -quality speed" + EncoderPreset.veryslow => " -quality quality", + EncoderPreset.slower => " -quality quality", + EncoderPreset.slow => " -quality quality", + EncoderPreset.medium => " -quality balanced", + _ => " -quality speed" }; if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) @@ -1803,11 +1803,11 @@ namespace MediaBrowser.Controller.MediaEncoding { param += encoderPreset switch { - EncoderPreset.veryslow => " -prio_speed 0", - EncoderPreset.slower => " -prio_speed 0", - EncoderPreset.slow => " -prio_speed 0", - EncoderPreset.medium => " -prio_speed 0", - _ => " -prio_speed 1" + EncoderPreset.veryslow => " -prio_speed 0", + EncoderPreset.slower => " -prio_speed 0", + EncoderPreset.slow => " -prio_speed 0", + EncoderPreset.medium => " -prio_speed 0", + _ => " -prio_speed 1" }; } diff --git a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs index 54da218530..9bee653e2e 100644 --- a/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs +++ b/MediaBrowser.Controller/MediaSegments/IMediaSegmentProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs index 132765b719..eb38eeb503 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupStates/WaitingGroupState.cs @@ -141,7 +141,8 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates _logger.LogError("Unable to set playing queue in group {GroupId}.", context.GroupId.ToString()); // Ignore request and return to previous state. - IGroupState newState = prevState switch { + IGroupState newState = prevState switch + { GroupStateType.Playing => new PlayingGroupState(LoggerFactory), GroupStateType.Paused => new PausedGroupState(LoggerFactory), _ => new IdleGroupState(LoggerFactory) diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index cf1423d02d..340d9843ff 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -143,16 +143,16 @@ namespace MediaBrowser.LocalMetadata.Parsers item.Name = reader.ReadNormalizedString(); break; case "CriticRating": - { - var text = reader.ReadElementContentAsString(); - - if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) { - item.CriticRating = value; - } + var text = reader.ReadElementContentAsString(); - break; - } + if (float.TryParse(text, CultureInfo.InvariantCulture, out var value)) + { + item.CriticRating = value; + } + + break; + } case "SortTitle": item.ForcedSortName = reader.ReadNormalizedString(); @@ -176,55 +176,55 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockedFields": - { - var val = reader.ReadElementContentAsString(); - - if (!string.IsNullOrWhiteSpace(val)) { - item.LockedFields = val.Split('|').Select(i => + var val = reader.ReadElementContentAsString(); + + if (!string.IsNullOrWhiteSpace(val)) { - if (Enum.TryParse(i, true, out MetadataField field)) + item.LockedFields = val.Split('|').Select(i => { - return (MetadataField?)field; - } + if (Enum.TryParse(i, true, out MetadataField field)) + { + return (MetadataField?)field; + } - return null; - }).Where(i => i.HasValue).Select(i => i!.Value).ToArray(); + return null; + }).Where(i => i.HasValue).Select(i => i!.Value).ToArray(); + } + + break; } - break; - } - case "TagLines": - { - if (!reader.IsEmptyElement) { - using (var subtree = reader.ReadSubtree()) + if (!reader.IsEmptyElement) { - FetchFromTaglinesNode(subtree, item); + using (var subtree = reader.ReadSubtree()) + { + FetchFromTaglinesNode(subtree, item); + } + } + else + { + reader.Read(); } - } - else - { - reader.Read(); - } - break; - } + break; + } case "Countries": - { - if (!reader.IsEmptyElement) { - reader.Skip(); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + reader.Skip(); + } + else + { + reader.Read(); + } - break; - } + break; + } case "ContentRating": case "MPAARating": @@ -307,19 +307,19 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Trailers": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchDataFromTrailersNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchDataFromTrailersNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "ProductionYear": if (reader.TryReadInt(out var productionYear) && productionYear > 1850) @@ -330,20 +330,20 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Rating": case "IMDBrating": - { - var rating = reader.ReadNormalizedString(); - - if (!string.IsNullOrEmpty(rating)) { - // All external meta is saving this as '.' for decimal I believe...but just to be sure - if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) - { - item.CommunityRating = val; - } - } + var rating = reader.ReadNormalizedString(); - break; - } + if (!string.IsNullOrEmpty(rating)) + { + // All external meta is saving this as '.' for decimal I believe...but just to be sure + if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) + { + item.CommunityRating = val; + } + } + + break; + } case "BirthDate": case "PremiereDate": @@ -370,144 +370,144 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "Genres": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromGenresNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromGenresNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Tags": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromTagsNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromTagsNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Persons": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchDataFromPersonsNode(subtree, itemResult); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchDataFromPersonsNode(subtree, itemResult); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Studios": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - FetchFromStudiosNode(subtree, item); - } - else - { - reader.Read(); - } + if (!reader.IsEmptyElement) + { + using var subtree = reader.ReadSubtree(); + FetchFromStudiosNode(subtree, item); + } + else + { + reader.Read(); + } - break; - } + break; + } case "Shares": - { - if (!reader.IsEmptyElement) { - using var subtree = reader.ReadSubtree(); - if (item is IHasShares hasShares) + if (!reader.IsEmptyElement) { - FetchFromSharesNode(subtree, hasShares); + using var subtree = reader.ReadSubtree(); + if (item is IHasShares hasShares) + { + FetchFromSharesNode(subtree, hasShares); + } + } + else + { + reader.Read(); } - } - else - { - reader.Read(); - } - break; - } + break; + } case "OwnerUserId": - { - var val = reader.ReadNormalizedString(); - - if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) { - if (item is Playlist playlist) - { - playlist.OwnerUserId = guid; - } - } + var val = reader.ReadNormalizedString(); - break; - } + if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) + { + if (item is Playlist playlist) + { + playlist.OwnerUserId = guid; + } + } + + break; + } case "Format3D": - { - var val = reader.ReadNormalizedString(); - - if (item is Video video) { - if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfSideBySide; - } - else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.HalfTopAndBottom; - } - else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullTopAndBottom; - } - else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.FullSideBySide; - } - else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) - { - video.Video3DFormat = Video3DFormat.MVC; - } - } + var val = reader.ReadNormalizedString(); - break; - } + if (item is Video video) + { + if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfSideBySide; + } + else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.HalfTopAndBottom; + } + else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullTopAndBottom; + } + else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.FullSideBySide; + } + else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase)) + { + video.Video3DFormat = Video3DFormat.MVC; + } + } + + break; + } default: - { - string readerName = reader.Name; - if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) { - var id = reader.ReadNormalizedString(); - item.TrySetProviderId(providerIdValue, id); - } - else - { - reader.Skip(); - } + string readerName = reader.Name; + if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) + { + var id = reader.ReadNormalizedString(); + item.TrySetProviderId(providerIdValue, id); + } + else + { + reader.Skip(); + } - break; - } + break; + } } } @@ -526,31 +526,31 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Share": - { - if (reader.IsEmptyElement) { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - var child = GetShare(subReader); - - if (child is not null) + if (reader.IsEmptyElement) { - list.Add(child); + reader.Read(); + continue; } - } - break; - } + using (var subReader = reader.ReadSubtree()) + { + var child = GetShare(subReader); + + if (child is not null) + { + list.Add(child); + } + } + + break; + } default: - { - reader.Skip(); - break; - } + { + reader.Skip(); + break; + } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs index 00634de5b5..324505d17c 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BoxSetXmlParser.cs @@ -64,32 +64,32 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "CollectionItem": - { - if (!reader.IsEmptyElement) { - using (var subReader = reader.ReadSubtree()) + if (!reader.IsEmptyElement) { - var child = GetLinkedChild(subReader); - - if (child is not null) + using (var subReader = reader.ReadSubtree()) { - list.Add(child); + var child = GetLinkedChild(subReader); + + if (child is not null) + { + list.Add(child); + } } } - } - else - { - reader.Read(); - } + else + { + reader.Read(); + } - break; - } + break; + } default: - { - reader.Skip(); - break; - } + { + reader.Skip(); + break; + } } } else diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs index e0277870d1..0bda9e300a 100644 --- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs @@ -76,25 +76,25 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "PlaylistItem": - { - if (reader.IsEmptyElement) { - reader.Read(); - continue; - } - - using (var subReader = reader.ReadSubtree()) - { - var child = GetLinkedChild(subReader); - - if (child is not null) + if (reader.IsEmptyElement) { - list.Add(child); + reader.Read(); + continue; } - } - break; - } + using (var subReader = reader.ReadSubtree()) + { + var child = GetLinkedChild(subReader); + + if (child is not null) + { + list.Add(child); + } + } + + break; + } default: reader.Skip(); diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index 7f40f4fd3e..d9cb7a450f 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -163,19 +163,19 @@ namespace MediaBrowser.MediaEncoding.Attachments int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = outputFolder, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = outputFolder, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -295,18 +295,18 @@ namespace MediaBrowser.MediaEncoding.Attachments int exitCode; using (var process = new Process + { + StartInfo = new ProcessStartInfo { - StartInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }, - EnableRaisingEvents = true - }) + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) { _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 8ad66fce40..e0c5f3ad39 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -190,10 +190,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles await using (stream.ConfigureAwait(false)) { - using var reader = new StreamReader(stream, detected.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -1027,20 +1027,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles switch (protocol) { case MediaProtocol.Http: - { - using var stream = await _httpClientFactory - .CreateClient(NamedClient.Default) - .GetStreamAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); + { + using var stream = await _httpClientFactory + .CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(path), cancellationToken) + .ConfigureAwait(false); - return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); - } + return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); + } case MediaProtocol.File: - { - return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) - .ConfigureAwait(false); - } + { + return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) + .ConfigureAwait(false); + } default: throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol"); diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs index 7c9ee18ca4..28c3c66af7 100644 --- a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs +++ b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Model.Extensions return 0; }) - .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1) ) + .ThenByDescending(i => Math.Round(i.CommunityRating ?? 0, 1)) .ThenByDescending(i => i.VoteCount ?? 0); } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs index 53d0173750..9a21461d82 100644 --- a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Jellyfin.Database.Implementations.Entities; using MediaBrowser.Model.MediaSegments; diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs index 4429623dd9..ded66652ce 100644 --- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -50,7 +50,7 @@ namespace MediaBrowser.Model.SyncPlay /// Seek = 8, - /// + /// /// A user is signaling that playback is buffering. /// Buffer = 9, diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs index 7e2d10c8b8..ccf5fdb07e 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupDoesNotExistUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs index bfb49152a3..dcb039ee93 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupJoinedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs index 5ff60c5c27..f20e143e02 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayGroupLeftUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs index 0d9a722f78..89e5706d86 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayLibraryAccessDeniedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs index a3b610f619..4ba893be5b 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayNotInGroupUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs index 83d9bd40bc..a39f20735b 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayPlayQueueUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs index 744ca46a0b..61cb8adbaa 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayStateUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs index e8c6b4df41..247e6a57b2 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserJoinedUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs index 97be8e63a8..ba053747cc 100644 --- a/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs +++ b/MediaBrowser.Model/SyncPlay/SyncPlayUserLeftUpdate.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; namespace MediaBrowser.Model.SyncPlay; diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index 2c393ca862..95e4d46c59 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.Model.Users [Required(AllowEmptyStrings = false)] public string AuthenticationProviderId { get; set; } - [Required(AllowEmptyStrings= false)] + [Required(AllowEmptyStrings = false)] public string PasswordResetProviderId { get; set; } /// diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs index 6e678802c1..d2331c6864 100644 --- a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs +++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Threading; using System.Threading.Tasks; using System.Xml; diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs index dd0a939f72..f7c570692d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs index 59e6f42b19..c2bbd8ba86 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider { if (item is Audio) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; } diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs index 52b0583e58..e01b6c78ed 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -19,6 +19,6 @@ public class Zap2ItExternalUrlProvider : IExternalUrlProvider if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId)) { yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; - } + } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 137abff478..f013863336 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -96,7 +96,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var artist = reader.ReadNormalizedString(); if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo) { - artistVideo.Artists = [..artistVideo.Artists, artist]; + artistVideo.Artists = [.. artistVideo.Artists, artist]; } break; diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs index 55b90a54d7..ff10440e0c 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20240928082930_MarkSegmentProviderIdNonNullable.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs index 8cc7fb452d..9c03bfed9d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241020103111_LibraryDbMigration.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs index ac78019eda..3fe61f91df 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111131257_AddedCustomDataKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs index 4558d7c49c..d6b351e2ab 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241111135439_AddedCustomDataKeyKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs index 70e81f3676..a7c9e6fb50 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112152323_FixAncestorIdConfig.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs index d57ea81b3a..ab8b792a5f 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112232041_fixMediaStreams.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs index 78611b9e4c..1ed23e7c42 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241112234144_FixMediaStreams2.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs index d1b06ceaec..e3a3f3ac64 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20241113133548_EnforceUniqueItemValue.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs index e82575e418..3d4fd85af2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250202021306_FixedCollation.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs index 2c60dd7a62..1493df35d0 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250204092455_MakeStartEndDateNullable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs index 1e904e833e..713b5c0434 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250214031148_ChannelIdGuid.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs index 71f56a1492..7049ccc214 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250326065026_AddInheritedParentalRatingSubValue.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs index c17b35b404..d84940b7e6 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327101120_AddKeyframeData.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs index 5766cd3825..63010679e5 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250327171413_AddHdr10PlusFlag.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs index f921856a20..ceb3d32452 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250331182844_FixAttachmentMigration.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs index e1220bfcf7..1f6012bbf2 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250401142247_FixAncestors.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs index aa667bafd4..6032969cf3 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250405075612_FixItemValuesIndices.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs index 2935a608d1..a3d8fe2c3a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250609115616_DetachUserDataInsteadOfDelete.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs index bce6029d5b..44b44dd581 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250622170802_BaseItemImageInfoDateModifiedNullable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs index 23cb0c8ba5..e88518d74a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250714044826_ResetJournalMode.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs index 38033d07f0..a7f5e369ab 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250913211637_AddProperParentChildRelationBaseItemWithCascade.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs index 7c1bcdf445..097504aebb 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20250925203415_ExtendPeopleMapKey.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs index 198bc78cff..1ab6b4240a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113102337_AddLinkedChildrenTable.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs index 6334d8b5f1..4927b0e78d 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113203012_ChangeOwnerIdToGuid.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs index 388906c064..39a0805d2a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233000_AddForeignKeyToOwnerId.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs index 5387d3351d..6440d0a395 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260113233500_DropExtraIdsColumn.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs index ba1a131e9b..710ffc35b1 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260116114245_AddLatestItemsDateCreatedIndexes.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs index 8c8768645b..7e1d619b8a 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260118182305_AddIndicesToImageInfo.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs index da57c71662..0b540d799b 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260130232147_AddBaseItemNameIndex.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs index 92836e753f..ef0c039ffe 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260206224832_IndexOptimizations.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs index 3932e1c3e4..00d4f24403 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260308123920_AddTypeCleanNameIndex.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs index e1f62c12fb..ad51786581 100644 --- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs +++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Sqlite/Migrations/20260504075755_AddPartialIndexForItemCounts.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 3f7ae4d2cd..b6d2914efa 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -234,20 +234,20 @@ public class SkiaEncoder : IImageEncoder return default; default: - { - var boundsInfo = SKBitmap.DecodeBounds(safePath); - if (boundsInfo.Width > 0 && boundsInfo.Height > 0) { - return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + var boundsInfo = SKBitmap.DecodeBounds(safePath); + if (boundsInfo.Width > 0 && boundsInfo.Height > 0) + { + return new ImageDimensions(boundsInfo.Width, boundsInfo.Height); + } + + _logger.LogWarning( + "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", + path, + result); + + return default; } - - _logger.LogWarning( - "Unable to determine image dimensions for {FilePath}: {SkCodecResult}", - path, - result); - - return default; - } } } finally @@ -398,7 +398,7 @@ public class SkiaEncoder : IImageEncoder try { - // If we have to resize these they often end up distorted + // If we have to resize these they often end up distorted if (resultBitmap.ColorType == SKColorType.Gray8) { using (resultBitmap) diff --git a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 2b8e5a0a08..ed02fe6a1d 100644 --- a/src/Jellyfin.LiveTv/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -1129,7 +1129,7 @@ namespace Jellyfin.LiveTv.Channels { if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase)) { - item.Tags = [..item.Tags, "livestream"]; + item.Tags = [.. item.Tags, "livestream"]; _logger.LogDebug("Forcing update due to Tags {0}", item.Name); forceUpdate = true; } diff --git a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs index 58683deb30..15e20d6f64 100644 --- a/src/Jellyfin.LiveTv/Listings/ListingsManager.cs +++ b/src/Jellyfin.LiveTv/Listings/ListingsManager.cs @@ -67,7 +67,7 @@ public class ListingsManager : IListingsManager if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - config.ListingProviders = [..list, info]; + config.ListingProviders = [.. list, info]; } else { @@ -255,7 +255,7 @@ public class ListingsManager : IListingsManager Name = tunerChannelNumber, Value = providerChannelNumber }; - listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem]; + listingsProviderInfo.ChannelMappings = [.. listingsProviderInfo.ChannelMappings, newItem]; } _config.SaveConfiguration("livetv", config); diff --git a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs index 6a68b8c25c..74fa1415c6 100644 --- a/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs +++ b/src/Jellyfin.LiveTv/Timers/ItemDataProvider.cs @@ -116,7 +116,7 @@ namespace Jellyfin.LiveTv.Timers throw new ArgumentException("item already exists", nameof(item)); } - _items = [.._items, item]; + _items = [.. _items, item]; SaveList(); } @@ -131,7 +131,7 @@ namespace Jellyfin.LiveTv.Timers int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); if (index == -1) { - _items = [.._items, item]; + _items = [.. _items, item]; } else { diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs index cfd763b6fd..7c16d2b363 100644 --- a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs @@ -83,7 +83,7 @@ public class TunerHostManager : ITunerHostManager if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - config.TunerHosts = [..list, info]; + config.TunerHosts = [.. list, info]; } else { diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 0fe2fc43ad..4559f68ce8 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -356,7 +356,7 @@ public class NetworkManager : INetworkManager, IDisposable { lock (_initLock) { - _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); + _interfaces = FilterBindSettings(config, _interfaces, IsIPv4Enabled, IsIPv6Enabled).ToList(); } } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs index a7491f42e9..2438ef06d1 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs @@ -37,9 +37,9 @@ public class FFProbeVideoInfoTests { Assert.Throws( () => _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - })); + { + RunTimeTicks = runtime + })); } [Theory] @@ -53,9 +53,9 @@ public class FFProbeVideoInfoTests public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount) { var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - }); + { + RunTimeTicks = runtime + }); Assert.Equal(chaptersCount, chapters.Length); } @@ -69,9 +69,9 @@ public class FFProbeVideoInfoTests public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime) { var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video() - { - RunTimeTicks = runtime - }); + { + RunTimeTicks = runtime + }); Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime)); } From eb2cef1b7eaac440f3cc1d3ee2f36a32f72ae37a Mon Sep 17 00:00:00 2001 From: jakobkukla Date: Mon, 27 Oct 2025 16:51:34 +0100 Subject: [PATCH 50/63] Fix StyleCop pattern matching whitespace false-positive --- .../MediaEncoding/EncodingHelper.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 9b6ec207d8..8f6e36bce4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2759,25 +2759,29 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase)) { +#pragma warning disable SA1008 return (inputChannels, outputChannels) switch { - (>= 6, >= 6 or 0) => Math.Min(640000, bitrate), - (> 0, > 0) => Math.Min(outputChannels * 128000, bitrate), - (> 0, _) => Math.Min(inputChannels * 128000, bitrate), + ( >= 6, >= 6 or 0) => Math.Min(640000, bitrate), + ( > 0, > 0) => Math.Min(outputChannels * 128000, bitrate), + ( > 0, _) => Math.Min(inputChannels * 128000, bitrate), (_, _) => Math.Min(384000, bitrate) }; +#pragma warning restore SA1008 } if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase) || string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase)) { +#pragma warning disable SA1008 return (inputChannels, outputChannels) switch { - (>= 6, >= 6 or 0) => Math.Min(768000, bitrate), - (> 0, > 0) => Math.Min(outputChannels * 136000, bitrate), - (> 0, _) => Math.Min(inputChannels * 136000, bitrate), + ( >= 6, >= 6 or 0) => Math.Min(768000, bitrate), + ( > 0, > 0) => Math.Min(outputChannels * 136000, bitrate), + ( > 0, _) => Math.Min(inputChannels * 136000, bitrate), (_, _) => Math.Min(672000, bitrate) }; +#pragma warning restore SA1008 } // Empty bitrate area is not allow on iOS From 0ef4f86ed7bdfde86046fb8ac694892e8206447e Mon Sep 17 00:00:00 2001 From: jakobkukla Date: Mon, 27 Oct 2025 15:07:33 +0100 Subject: [PATCH 51/63] Add CI format check --- .github/workflows/ci-format.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/ci-format.yml diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml new file mode 100644 index 0000000000..a07764b30a --- /dev/null +++ b/.github/workflows/ci-format.yml @@ -0,0 +1,25 @@ +name: Format +on: + push: + branches: + - master + # Run formatter against the forked branch, but + # do not allow access to secrets + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories + pull_request: + +env: + SDK_VERSION: "10.0.x" + +jobs: + format-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + with: + dotnet-version: ${{ env.SDK_VERSION }} + + - name: Run DotNet Format + run: dotnet format --verify-no-changes --verbosity minimal From bc8a66a35174acad9aca983d5fd311f5070b83a3 Mon Sep 17 00:00:00 2001 From: Lino Silva Date: Thu, 21 May 2026 10:42:37 -0400 Subject: [PATCH 52/63] Translated using Weblate (Portuguese) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt/ --- Emby.Server.Implementations/Localization/Core/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 15b1543d8e..ce338acf34 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -107,5 +107,6 @@ "TaskMoveTrickplayImages": "Migrar a localização da imagem do Trickplay", "CleanupUserDataTask": "Task de limpeza de dados do usuário", "CleanupUserDataTaskDescription": "Remove todos os dados do usuário (progresso, favoritos etc) de mídias que não estão presentes há pelo menos 90 dias.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "Erro ao descarregar letras de {0} para {1}" } From 17e2d3bb11b62ed22e42e4d2728b74ea29a9b193 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Thu, 21 May 2026 21:40:30 +0200 Subject: [PATCH 53/63] Fix Merge Conflict Labeler https://github.com/eps1lon/actions-label-merge-conflict#example-usage --- .github/workflows/pull-request-conflict.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml index b003636a6e..32628ac912 100644 --- a/.github/workflows/pull-request-conflict.yml +++ b/.github/workflows/pull-request-conflict.yml @@ -4,7 +4,7 @@ on: push: branches: - master - pull_request: + pull_request_target: issue_comment: permissions: {} From 4361e073eddc3c01f761ef9ac6e5f319f6ad9485 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Thu, 21 May 2026 22:21:18 +0200 Subject: [PATCH 54/63] Add support for filtering playlists by parentId --- Jellyfin.Api/Controllers/ItemsController.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 82a1bdebd7..363af9e43b 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -14,6 +14,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -280,15 +281,19 @@ public class ItemsController : BaseJellyfinApiController var item = _libraryManager.GetParentItem(parentId, userId); QueryResult result; - Guid[] boxSetLinkedChildAncestorIds = []; + Guid[] linkedChildAncestorIds = []; if (includeItemTypes.Length == 1 - && includeItemTypes[0] == BaseItemKind.BoxSet - && item is not BoxSet) + && (includeItemTypes[0] == BaseItemKind.BoxSet || includeItemTypes[0] == BaseItemKind.Playlist) + && item is not BoxSet + && item is not Playlist) { - var isBoxSetsLibrary = item is IHasCollectionType hct && hct.CollectionType == CollectionType.boxsets; - if (parentId.HasValue && item is not UserRootFolder && !isBoxSetsLibrary) + var itemCollectionType = item is IHasCollectionType hct ? hct.CollectionType : null; + var targetCollectionType = includeItemTypes[0] == BaseItemKind.BoxSet + ? CollectionType.boxsets + : CollectionType.playlists; + if (parentId.HasValue && item is not UserRootFolder && itemCollectionType != targetCollectionType) { - boxSetLinkedChildAncestorIds = [parentId.Value]; + linkedChildAncestorIds = [parentId.Value]; } parentId = null; @@ -412,7 +417,7 @@ public class ItemsController : BaseJellyfinApiController MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), AudioLanguages = audioLanguages, SubtitleLanguages = subtitleLanguages, - LinkedChildAncestorIds = boxSetLinkedChildAncestorIds, + LinkedChildAncestorIds = linkedChildAncestorIds, }; if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm)) From 623b1c39e588135d4b16e9cab025ce0813562499 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Thu, 21 May 2026 20:42:19 -0400 Subject: [PATCH 55/63] Update issue template version to 10.11.9 --- .github/ISSUE_TEMPLATE/issue report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml index 45235be712..a7e644a55f 100644 --- a/.github/ISSUE_TEMPLATE/issue report.yml +++ b/.github/ISSUE_TEMPLATE/issue report.yml @@ -87,6 +87,7 @@ body: label: Jellyfin Server version description: What version of Jellyfin are you using? options: + - 10.11.9 - 10.11.8 - 10.11.7 - 10.11.6 From 4ca3cd79fa3e5413e1de6df567d99ade05eaf339 Mon Sep 17 00:00:00 2001 From: someone522 Date: Fri, 22 May 2026 01:07:59 -0400 Subject: [PATCH 56/63] Translated using Weblate (Uzbek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uz/ --- Emby.Server.Implementations/Localization/Core/uz.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json index 998d799a95..bee822e4cd 100644 --- a/Emby.Server.Implementations/Localization/Core/uz.json +++ b/Emby.Server.Implementations/Localization/Core/uz.json @@ -82,5 +82,6 @@ "TaskRefreshChapterImages": "Sahnadan tasvirini chiqarish", "TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.", "TaskRefreshLibrary": "Media kutubxonangizni skanerlash", - "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi." + "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi.", + "Original": "Original" } From 8ddb485192d0ab5745ab615167201ca8d9ed1d40 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 11:41:30 +0000 Subject: [PATCH 57/63] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-format.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index b14e5958ab..cf4cc1c7f1 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -32,13 +32,13 @@ jobs: dotnet-version: '10.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml index a07764b30a..667d6e628f 100644 --- a/.github/workflows/ci-format.yml +++ b/.github/workflows/ci-format.yml @@ -15,9 +15,9 @@ jobs: format-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - - uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0 + - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: ${{ env.SDK_VERSION }} From aacb147f4dea48a6e78ad8ab74aa76ee7e287c7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 15:21:22 +0000 Subject: [PATCH 58/63] Update actions/checkout action to v6 --- .github/workflows/ci-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-format.yml b/.github/workflows/ci-format.yml index 667d6e628f..c2cca262bf 100644 --- a/.github/workflows/ci-format.yml +++ b/.github/workflows/ci-format.yml @@ -15,7 +15,7 @@ jobs: format-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: From df6296bc304b81b4dd1943911bfd3fd9722823e7 Mon Sep 17 00:00:00 2001 From: Maldark Date: Fri, 22 May 2026 13:36:26 -0400 Subject: [PATCH 59/63] Translated using Weblate (Danish) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/da/ --- Emby.Server.Implementations/Localization/Core/da.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index e30528b3cd..697d9c090f 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -106,5 +106,7 @@ "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.", "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.", "CleanupUserDataTask": "Brugerdata oprydningsopgave", - "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage." + "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage.", + "LyricDownloadFailureFromForItem": "Sangtekster kunne ikke downloades fra {0} til {1}", + "Original": "Original" } From 17a32f055c2cdb8cc65ea7d49b32515be9727ae7 Mon Sep 17 00:00:00 2001 From: someone522 Date: Sat, 23 May 2026 02:34:59 -0400 Subject: [PATCH 60/63] Translated using Weblate (Uzbek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/uz/ --- Emby.Server.Implementations/Localization/Core/uz.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/uz.json b/Emby.Server.Implementations/Localization/Core/uz.json index bee822e4cd..3215733c1a 100644 --- a/Emby.Server.Implementations/Localization/Core/uz.json +++ b/Emby.Server.Implementations/Localization/Core/uz.json @@ -83,5 +83,6 @@ "TaskRefreshChapterImagesDescription": "Sahnalarni o'z ichiga olgan videolar uchun eskizlarni yaratadi.", "TaskRefreshLibrary": "Media kutubxonangizni skanerlash", "TaskCleanLogsDescription": "{0} kundan eski log fayllarni o'chiradi.", - "Original": "Original" + "Original": "Original", + "LyricDownloadFailureFromForItem": "{0} dan {1} gacha qo'shiq matninin yuklab olishda xatolik ketdi" } From 26dbd86b81a1d6a988568373fbb162478e384702 Mon Sep 17 00:00:00 2001 From: Ekaterine Papava Date: Fri, 22 May 2026 23:00:42 -0400 Subject: [PATCH 61/63] Translated using Weblate (Georgian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ka/ --- Emby.Server.Implementations/Localization/Core/ka.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index 6b3aeb1218..5245d89948 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -22,7 +22,7 @@ "HearingImpaired": "სმენადაქვეითებული", "LabelRunningTimeValue": "ხანგრძლივობა: {0}", "MixedContent": "შერეული შემცველობა", - "MusicVideos": "მუსიკალური ვიდეოები", + "MusicVideos": "მუსიკის ვიდეოები", "NotificationOptionInstallationFailed": "დაყენების შეცდომა", "NotificationOptionApplicationUpdateInstalled": "აპლიკაციის განახლება დაყენებულია", "NotificationOptionAudioPlayback": "აუდიოს დაკვრა დაწყებულია", From 863730e64799312cfe5d249cc7baba9b27944dc9 Mon Sep 17 00:00:00 2001 From: Klomer Date: Fri, 22 May 2026 14:08:15 -0400 Subject: [PATCH 62/63] Translated using Weblate (Breton) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/br/ --- .../Localization/Core/br.json | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/br.json b/Emby.Server.Implementations/Localization/Core/br.json index 9c379f1916..cedc87e5a6 100644 --- a/Emby.Server.Implementations/Localization/Core/br.json +++ b/Emby.Server.Implementations/Localization/Core/br.json @@ -35,5 +35,29 @@ "NotificationOptionApplicationUpdateAvailable": "Hizivadur an arload zo da gaout", "NotificationOptionApplicationUpdateInstalled": "Hizivadur an arload staliet", "NotificationOptionAudioPlayback": "Lenn aodio lañset", - "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet" + "NotificationOptionAudioPlaybackStopped": "Lenn aodio ehanet", + "Original": "Orin", + "Photos": "Fotoioù", + "Shows": "Heuliadennoù", + "Undefined": "Dianav", + "TasksMaintenanceCategory": "Trezalc’h", + "TasksLibraryCategory": "Levraoueg", + "TasksApplicationCategory": "Arload", + "NotificationOptionInstallationFailed": "C'hwitet war staliañ", + "NotificationOptionPluginError": "Fazi Askouezh", + "NotificationOptionPluginInstalled": "Askouezh staliet", + "NotificationOptionPluginUninstalled": "Askouezh distaliet", + "ScheduledTaskFailedWithName": "c'hwitadenn war {0}", + "TvShows": "Heuliadennoù TV", + "VersionNumber": "Stumm {0}", + "TasksChannelsCategory": "Chadennoù enlinenn", + "TaskAudioNormalization": "Normalizadur an aodio", + "TaskRefreshPeople": "Freskaat ar gomedianed", + "TaskUpdatePlugins": "Hizivaat an askouezhioù", + "TaskRefreshChannels": "Freskaat ar chadennoù", + "TaskOptimizeDatabase": "Gwellekaat an diaz roadennoù", + "TaskKeyframeExtractor": "Eztenner skeudennoù-alc'hwez", + "NotificationOptionCameraImageUploaded": "Karget eo skeudenn ar benveg", + "NotificationOptionNewLibraryContent": "Danvez nevez ouzhpennet", + "NotificationOptionPluginUpdateInstalled": "Staliet eo hizivadur an askouezh" } From a3849819c27d9a99d1fc537af39e2064142594c0 Mon Sep 17 00:00:00 2001 From: lednurb Date: Sat, 23 May 2026 03:51:13 -0400 Subject: [PATCH 63/63] Translated using Weblate (Dutch) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/nl/ --- Emby.Server.Implementations/Localization/Core/nl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 9aea3adc22..898f5892c9 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -8,7 +8,7 @@ "FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}", "Favorites": "Favorieten", "Folders": "Mappen", - "HeaderContinueWatching": "Verderkijken", + "HeaderContinueWatching": "Verder kijken", "HeaderFavoriteEpisodes": "Favoriete afleveringen", "HeaderFavoriteShows": "Favoriete series", "HeaderLiveTV": "Live-tv",