2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'jellyfinorigin/master' into feature/pgsql_provider

JPVenson 4 сар өмнө
parent
commit
dfdef511a5
44 өөрчлөгдсөн 3752 нэмэгдсэн , 231 устгасан
  1. 5 5
      Directory.Packages.props
  2. 10 0
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  3. 1 0
      Emby.Server.Implementations/Localization/Core/ht.json
  4. 7 1
      Emby.Server.Implementations/Localization/Ratings/br.csv
  5. 0 2
      Emby.Server.Implementations/Localization/Ratings/ca.csv
  6. 1 1
      Emby.Server.Implementations/Localization/Ratings/es.csv
  7. 2 1
      Emby.Server.Implementations/Localization/Ratings/gb.csv
  8. 1 0
      Emby.Server.Implementations/Localization/Ratings/ie.csv
  9. 1 0
      Emby.Server.Implementations/Localization/Ratings/no.csv
  10. 1 0
      Emby.Server.Implementations/Localization/Ratings/nz.csv
  11. 2 0
      Emby.Server.Implementations/Localization/Ratings/us.csv
  12. 7 6
      Jellyfin.Api/Auth/CustomAuthenticationHandler.cs
  13. 5 0
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  14. 2 1
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  15. 2 1
      Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
  16. 2 2
      Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
  17. 1623 0
      Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs
  18. 55 0
      Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs
  19. 2 2
      Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs
  20. 1595 0
      Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs
  21. 55 0
      Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs
  22. 2 2
      Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs
  23. 2 2
      Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
  24. 2 4
      Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
  25. 8 0
      Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
  26. 13 4
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  27. 10 10
      MediaBrowser.Controller/Net/AuthorizationInfo.cs
  28. 23 14
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs
  29. 66 17
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  30. 29 0
      MediaBrowser.Model/Dlna/TranscodingProfile.cs
  31. 1 0
      MediaBrowser.Providers/Manager/ImageSaver.cs
  32. 3 3
      MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
  33. 23 22
      MediaBrowser.Providers/TV/SeriesMetadataService.cs
  34. 16 22
      MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
  35. 10 5
      MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
  36. 3 1
      MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
  37. 3 1
      src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
  38. 109 82
      src/Jellyfin.LiveTv/Guide/GuideManager.cs
  39. 15 16
      src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs
  40. 7 2
      src/Jellyfin.Networking/Manager/NetworkManager.cs
  41. 2 1
      tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs
  42. 1 1
      tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
  43. 18 0
      tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
  44. 7 0
      tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo

+ 5 - 5
Directory.Packages.props

@@ -9,14 +9,14 @@
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <PackageVersion Include="BDInfo" Version="0.8.0" />
     <PackageVersion Include="BDInfo" Version="0.8.0" />
-    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.3" />
-    <PackageVersion Include="BlurHashSharp" Version="1.3.3" />
+    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
+    <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="coverlet.collector" Version="6.0.4" />
     <PackageVersion Include="coverlet.collector" Version="6.0.4" />
     <PackageVersion Include="Diacritics" Version="3.3.29" />
     <PackageVersion Include="Diacritics" Version="3.3.29" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
-    <PackageVersion Include="FsCheck.Xunit" Version="3.0.1" />
+    <PackageVersion Include="FsCheck.Xunit" Version="3.1.0" />
     <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
     <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
     <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
     <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
@@ -51,7 +51,7 @@
     <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
     <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
     <PackageVersion Include="MimeTypes" Version="2.5.2" />
     <PackageVersion Include="MimeTypes" Version="2.5.2" />
     <PackageVersion Include="Moq" Version="4.18.4" />
     <PackageVersion Include="Moq" Version="4.18.4" />
-    <PackageVersion Include="NEbml" Version="0.11.0" />
+    <PackageVersion Include="NEbml" Version="0.12.0" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
     <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
     <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
     <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
     <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
@@ -80,7 +80,7 @@
     <PackageVersion Include="System.Text.Json" Version="9.0.1" />
     <PackageVersion Include="System.Text.Json" Version="9.0.1" />
     <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.1" />
     <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.1" />
     <PackageVersion Include="TagLibSharp" Version="2.3.0" />
     <PackageVersion Include="TagLibSharp" Version="2.3.0" />
-    <PackageVersion Include="z440.atl.core" Version="6.14.0" />
+    <PackageVersion Include="z440.atl.core" Version="6.15.0" />
     <PackageVersion Include="TMDbLib" Version="2.2.0" />
     <PackageVersion Include="TMDbLib" Version="2.2.0" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />

+ 10 - 0
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -276,6 +276,13 @@ namespace Emby.Server.Implementations.IO
                         {
                         {
                             _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
                             _logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
                         }
                         }
+                        catch (IOException ex)
+                        {
+                            // IOException generally means the file is not accessible due to filesystem issues
+                            // Catch this exception and mark the file as not exist to ignore it
+                            _logger.LogError(ex, "Reading the file at {Path} failed due to an IO Exception. Marking the file as not existing", fileInfo.FullName);
+                            result.Exists = false;
+                        }
                     }
                     }
                 }
                 }
 
 
@@ -590,6 +597,9 @@ namespace Emby.Server.Implementations.IO
         /// <inheritdoc />
         /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
         {
         {
+            // Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
+            // But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
+            // For example, the scanner will remove everything in that path due to unhandled errors.
             var directoryInfo = new DirectoryInfo(path);
             var directoryInfo = new DirectoryInfo(path);
             var enumerationOptions = GetEnumerationOptions(recursive);
             var enumerationOptions = GetEnumerationOptions(recursive);
 
 

+ 1 - 0
Emby.Server.Implementations/Localization/Core/ht.json

@@ -0,0 +1 @@
+{}

+ 7 - 1
Emby.Server.Implementations/Localization/Ratings/br.csv

@@ -1,8 +1,14 @@
 Livre,0
 Livre,0
 L,0
 L,0
-ER,9
+AL,0
+ER,10
 10,10
 10,10
+A10,10
 12,12
 12,12
+A12,12
 14,14
 14,14
+A14,14
 16,16
 16,16
+A16,16
 18,18
 18,18
+A18,18

+ 0 - 2
Emby.Server.Implementations/Localization/Ratings/ca.csv

@@ -6,8 +6,6 @@ TV-Y7,7
 TV-Y7-FV,7
 TV-Y7-FV,7
 PG,9
 PG,9
 TV-PG,9
 TV-PG,9
-PG-13,13
-13+,13
 TV-14,14
 TV-14,14
 14A,14
 14A,14
 16+,16
 16+,16

+ 1 - 1
Emby.Server.Implementations/Localization/Ratings/es.csv

@@ -1,7 +1,7 @@
 A,0
 A,0
 A/fig,0
 A/fig,0
 A/i,0
 A/i,0
-A/fig/i,0
+A/i/fig,0
 APTA,0
 APTA,0
 ERI,0
 ERI,0
 TP,0
 TP,0

+ 2 - 1
Emby.Server.Implementations/Localization/Ratings/gb.csv

@@ -6,10 +6,11 @@ U,0
 6+,6
 6+,6
 7+,7
 7+,7
 PG,8
 PG,8
-9+,9
+9,9
 12,12
 12,12
 12+,12
 12+,12
 12A,12
 12A,12
+12PG,12
 Teen,13
 Teen,13
 13+,13
 13+,13
 14+,14
 14+,14

+ 1 - 0
Emby.Server.Implementations/Localization/Ratings/ie.csv

@@ -4,6 +4,7 @@ PG,12
 12A,12
 12A,12
 12PG,12
 12PG,12
 15,15
 15,15
+15PG,15
 15A,15
 15A,15
 16,16
 16,16
 18,18
 18,18

+ 1 - 0
Emby.Server.Implementations/Localization/Ratings/no.csv

@@ -6,4 +6,5 @@ A,0
 12,12
 12,12
 15,15
 15,15
 18,18
 18,18
+C,18
 Not approved,1001
 Not approved,1001

+ 1 - 0
Emby.Server.Implementations/Localization/Ratings/nz.csv

@@ -10,6 +10,7 @@ R16,16
 RP16,16
 RP16,16
 GA,18
 GA,18
 R18,18
 R18,18
+RP18,18
 MA,1000
 MA,1000
 R,1001
 R,1001
 Objectionable,1001
 Objectionable,1001

+ 2 - 0
Emby.Server.Implementations/Localization/Ratings/us.csv

@@ -48,3 +48,5 @@ TV-MA-LS,17
 TV-MA-LV,17
 TV-MA-LV,17
 TV-MA-SV,17
 TV-MA-SV,17
 TV-MA-LSV,17
 TV-MA-LSV,17
+TV-X,18
+TV-AO,18

+ 7 - 6
Jellyfin.Api/Auth/CustomAuthenticationHandler.cs

@@ -51,20 +51,21 @@ namespace Jellyfin.Api.Auth
                 }
                 }
 
 
                 var role = UserRoles.User;
                 var role = UserRoles.User;
-                if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+                if (authorizationInfo.IsApiKey
+                    || (authorizationInfo.User?.HasPermission(PermissionKind.IsAdministrator) ?? false))
                 {
                 {
                     role = UserRoles.Administrator;
                     role = UserRoles.Administrator;
                 }
                 }
 
 
                 var claims = new[]
                 var claims = new[]
                 {
                 {
-                    new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
+                    new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
                     new Claim(ClaimTypes.Role, role),
                     new Claim(ClaimTypes.Role, role),
                     new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
                     new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
-                    new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
-                    new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
-                    new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
-                    new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
+                    new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId ?? string.Empty),
+                    new Claim(InternalClaimTypes.Device, authorizationInfo.Device ?? string.Empty),
+                    new Claim(InternalClaimTypes.Client, authorizationInfo.Client ?? string.Empty),
+                    new Claim(InternalClaimTypes.Version, authorizationInfo.Version ?? string.Empty),
                     new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
                     new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
                     new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
                     new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
                 };
                 };

+ 5 - 0
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -235,6 +235,11 @@ public static class StreamingHelpers
                     state.VideoRequest.MaxHeight = resolution.MaxHeight;
                     state.VideoRequest.MaxHeight = resolution.MaxHeight;
                 }
                 }
             }
             }
+
+            if (state.AudioStream is not null && !EncodingHelper.IsCopyCodec(state.OutputAudioCodec) && string.Equals(state.AudioStream.Codec, state.OutputAudioCodec, StringComparison.OrdinalIgnoreCase) && state.OutputAudioBitrate.HasValue)
+            {
+                state.OutputAudioCodec = state.SupportedAudioCodecs.Where(c => !EncodingHelper.LosslessAudioCodecs.Contains(c)).FirstOrDefault(mediaEncoder.CanEncodeToAudioCodec);
+            }
         }
         }
 
 
         var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
         var ext = string.IsNullOrWhiteSpace(state.OutputContainer)

+ 2 - 1
Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs

@@ -71,7 +71,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
     /// <param name="message">The message.</param>
     /// <param name="message">The message.</param>
     protected override void Start(WebSocketMessageInfo message)
     protected override void Start(WebSocketMessageInfo message)
     {
     {
-        if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+        if (message.Connection.AuthorizationInfo.User is null
+            || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
         {
         {
             throw new AuthenticationException("Only admin users can retrieve the activity log.");
             throw new AuthenticationException("Only admin users can retrieve the activity log.");
         }
         }

+ 2 - 1
Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs

@@ -80,7 +80,8 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
     /// <param name="message">The message.</param>
     /// <param name="message">The message.</param>
     protected override void Start(WebSocketMessageInfo message)
     protected override void Start(WebSocketMessageInfo message)
     {
     {
-        if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+        if (message.Connection.AuthorizationInfo.User is null
+            || !message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
         {
         {
             throw new AuthenticationException("Only admin users can subscribe to session information.");
             throw new AuthenticationException("Only admin users can subscribe to session information.");
         }
         }

+ 2 - 2
Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs

@@ -18,9 +18,9 @@ public class BaseItemEntity
 
 
     public string? Path { get; set; }
     public string? Path { get; set; }
 
 
-    public DateTime StartDate { get; set; }
+    public DateTime? StartDate { get; set; }
 
 
-    public DateTime EndDate { get; set; }
+    public DateTime? EndDate { get; set; }
 
 
     public string? ChannelId { get; set; }
     public string? ChannelId { get; set; }
 
 

+ 1623 - 0
Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs

@@ -0,0 +1,1623 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.PgSql.Migrations
+{
+    [DbContext(typeof(JellyfinDbContext))]
+    [Migration("20250205183152_MakeStartEndDateNullable")]
+    partial class MakeStartEndDateNullable
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "9.0.1")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("integer");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("double precision");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("double precision");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<long>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("text");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("text");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("text");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("text");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("real");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("real");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("text");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("EndDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("integer");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("boolean");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("real");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("text");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("real");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("text");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("text");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("text");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("text");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("text");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("text");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("text");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("integer");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("text");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("text");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("text");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("text");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("text");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "TopParentId", "StartDate");
+
+                    b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+                    b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.ToTable("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("bytea");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ProviderValue")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.HasKey("ItemId", "ProviderId");
+
+                    b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+                    b.ToTable("BaseItemProviders");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("integer");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("text");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("text");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client", "Key")
+                        .IsUnique();
+
+                    b.ToTable("CustomItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("integer");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("boolean");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("character varying(512)");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue")
+                        .IsUnique();
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("uuid");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("bigint");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("text");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("real");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("text");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("text");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("text");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("integer");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool?>("IsInterlaced")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("text");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("real");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("text");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("text");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("real");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("text");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ItemId", "StreamIndex");
+
+                    b.HasIndex("StreamIndex");
+
+                    b.HasIndex("StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType", "Language");
+
+                    b.ToTable("MediaStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("text");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("uuid");
+
+                    b.Property<long>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("boolean");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("integer");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("uuid");
+
+                    b.Property<long>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasMaxLength(65535)
+                        .HasColumnType("character varying(65535)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("AccessToken")
+                        .IsUnique();
+
+                    b.ToTable("ApiKeys");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("character varying(64)");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("boolean");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId");
+
+                    b.HasIndex("AccessToken", "DateLastActivity");
+
+                    b.HasIndex("DeviceId", "DateLastActivity");
+
+                    b.HasIndex("UserId", "DeviceId");
+
+                    b.ToTable("Devices");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("text");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("text");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("character varying(255)");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("character varying(255)");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("character varying(32)");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("boolean");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("bigint");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("integer");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("integer");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("integer");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("boolean");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("character varying(65535)");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("character varying(255)");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("boolean");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("boolean");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("integer");
+
+                    b.Property<long>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("bigint");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("character varying(255)");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("integer");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("integer");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("character varying(255)");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Username")
+                        .IsUnique();
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("uuid");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("text");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("integer");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("boolean");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("timestamp with time zone");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("boolean");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("integer");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("bigint");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("boolean");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("double precision");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("integer");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "UserId", "Played");
+
+                    b.ToTable("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .HasForeignKey("ParentItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ParentItem");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Images")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("LockedFields")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Provider")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("TrailerTypes")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("ItemValues")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+                        .WithMany("BaseItemsMap")
+                        .HasForeignKey("ItemValueId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ItemValue");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("MediaStreams")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    b.Navigation("Peoples");
+
+                    b.Navigation("Provider");
+
+                    b.Navigation("TrailerTypes");
+
+                    b.Navigation("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Navigation("BaseItemsMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.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
+        }
+    }
+}

+ 55 - 0
Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs

@@ -0,0 +1,55 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Database.Providers.PgSql.Migrations
+{
+    /// <inheritdoc />
+    public partial class MakeStartEndDateNullable : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "StartDate",
+                table: "BaseItems",
+                type: "timestamp with time zone",
+                nullable: true,
+                oldClrType: typeof(DateTime),
+                oldType: "timestamp with time zone");
+
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "EndDate",
+                table: "BaseItems",
+                type: "timestamp with time zone",
+                nullable: true,
+                oldClrType: typeof(DateTime),
+                oldType: "timestamp with time zone");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "StartDate",
+                table: "BaseItems",
+                type: "timestamp with time zone",
+                nullable: false,
+                defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+                oldClrType: typeof(DateTime),
+                oldType: "timestamp with time zone",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "EndDate",
+                table: "BaseItems",
+                type: "timestamp with time zone",
+                nullable: false,
+                defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+                oldClrType: typeof(DateTime),
+                oldType: "timestamp with time zone",
+                oldNullable: true);
+        }
+    }
+}

+ 2 - 2
Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs

@@ -194,7 +194,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations
                     b.Property<DateTime?>("DateModified")
                     b.Property<DateTime?>("DateModified")
                         .HasColumnType("timestamp with time zone");
                         .HasColumnType("timestamp with time zone");
 
 
-                    b.Property<DateTime>("EndDate")
+                    b.Property<DateTime?>("EndDate")
                         .HasColumnType("timestamp with time zone");
                         .HasColumnType("timestamp with time zone");
 
 
                     b.Property<string>("EpisodeTitle")
                     b.Property<string>("EpisodeTitle")
@@ -332,7 +332,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations
                     b.Property<string>("SortName")
                     b.Property<string>("SortName")
                         .HasColumnType("text");
                         .HasColumnType("text");
 
 
-                    b.Property<DateTime>("StartDate")
+                    b.Property<DateTime?>("StartDate")
                         .HasColumnType("timestamp with time zone");
                         .HasColumnType("timestamp with time zone");
 
 
                     b.Property<string>("Studios")
                     b.Property<string>("Studios")

+ 1595 - 0
Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs

@@ -0,0 +1,1595 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDbContext))]
+    [Migration("20250204092455_MakeStartEndDateNullable")]
+    partial class MakeStartEndDateNullable
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ParentItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ParentItemId");
+
+                    b.HasIndex("ParentItemId");
+
+                    b.ToTable("AncestorIds");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Index")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Filename")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("MimeType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "Index");
+
+                    b.ToTable("AttachmentStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Album")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AlbumArtists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Artists")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Audio")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("CommunityRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<float?>("CriticRating")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("CustomRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Data")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastMediaAdded")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastRefreshed")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateLastSaved")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("EndDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("EpisodeTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalSeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExternalServiceId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ExtraIds")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ExtraType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ForcedSortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Genres")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("InheritedParentalRatingValue")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsInMixedFolder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsLocked")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsMovie")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsRepeat")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsSeries")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsVirtualItem")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<float?>("LUFS")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("MediaType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("NormalizationGain")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("OfficialRating")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OriginalTitle")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("OwnerId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("ParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ParentIndexNumber")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataCountryCode")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PreferredMetadataLanguage")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("PremiereDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PrimaryVersionId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProductionLocations")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ProductionYear")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long?>("RunTimeTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("SeasonId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeasonName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("SeriesId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SeriesPresentationUniqueKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ShowId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Studios")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tagline")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Tags")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("TopParentId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("TotalBitrate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("UnratedType")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ParentId");
+
+                    b.HasIndex("Path");
+
+                    b.HasIndex("PresentationUniqueKey");
+
+                    b.HasIndex("TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "Id");
+
+                    b.HasIndex("Type", "TopParentId", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "TopParentId", "StartDate");
+
+                    b.HasIndex("Id", "Type", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("MediaType", "TopParentId", "IsVirtualItem", "PresentationUniqueKey");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "IsFolder", "IsVirtualItem");
+
+                    b.HasIndex("Type", "SeriesPresentationUniqueKey", "PresentationUniqueKey", "SortName");
+
+                    b.HasIndex("IsFolder", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.HasIndex("Type", "TopParentId", "IsVirtualItem", "PresentationUniqueKey", "DateCreated");
+
+                    b.ToTable("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<byte[]>("Blurhash")
+                        .HasColumnType("BLOB");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ImageType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemMetadataFields");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ProviderValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemId", "ProviderId");
+
+                    b.HasIndex("ProviderId", "ProviderValue", "ItemId");
+
+                    b.ToTable("BaseItemProviders");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+                {
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("BaseItemTrailerTypes");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ChapterIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("ImageDateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ImagePath")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "ChapterIndex");
+
+                    b.ToTable("Chapters");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client", "Key")
+                        .IsUnique();
+
+                    b.ToTable("CustomItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CleanValue")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId");
+
+                    b.HasIndex("Type", "CleanValue")
+                        .IsUnique();
+
+                    b.ToTable("ItemValues");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.Property<Guid>("ItemValueId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("ItemValueId", "ItemId");
+
+                    b.HasIndex("ItemId");
+
+                    b.ToTable("ItemValuesMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("EndTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("SegmentProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<long>("StartTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("MediaSegments");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("StreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AspectRatio")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("AverageFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("BitDepth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BitRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("BlPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ChannelLayout")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Channels")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Codec")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTag")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CodecTimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorPrimaries")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorSpace")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ColorTransfer")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Comment")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("DvBlSignalCompatibilityId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvLevel")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvProfile")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMajor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("DvVersionMinor")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("ElPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAnamorphic")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsAvc")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsDefault")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsExternal")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsForced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsHearingImpaired")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool?>("IsInterlaced")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("KeyFrames")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Language")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("Level")
+                        .HasColumnType("REAL");
+
+                    b.Property<string>("NalLengthSize")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PixelFormat")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Profile")
+                        .HasColumnType("TEXT");
+
+                    b.Property<float?>("RealFrameRate")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("RefFrames")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("Rotation")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RpuPresentFlag")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("SampleRate")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("StreamType")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TimeBase")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "StreamIndex");
+
+                    b.HasIndex("StreamIndex");
+
+                    b.HasIndex("StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType");
+
+                    b.HasIndex("StreamIndex", "StreamType", "Language");
+
+                    b.ToTable("MediaStreamInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PersonType")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Name");
+
+                    b.ToTable("Peoples");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("PeopleId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("ListOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Role")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "PeopleId");
+
+                    b.HasIndex("PeopleId");
+
+                    b.HasIndex("ItemId", "ListOrder");
+
+                    b.HasIndex("ItemId", "SortOrder");
+
+                    b.ToTable("PeopleBaseItemMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("AccessToken")
+                        .IsUnique();
+
+                    b.ToTable("ApiKeys");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId");
+
+                    b.HasIndex("AccessToken", "DateLastActivity");
+
+                    b.HasIndex("DeviceId", "DateLastActivity");
+
+                    b.HasIndex("UserId", "DeviceId");
+
+                    b.ToTable("Devices");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT")
+                        .UseCollation("NOCASE");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Username")
+                        .IsUnique();
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CustomDataKey")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("AudioStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("IsFavorite")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastPlayedDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool?>("Likes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("PlayCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("PlaybackPositionTicks")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Played")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double?>("Rating")
+                        .HasColumnType("REAL");
+
+                    b.Property<int?>("SubtitleStreamIndex")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "UserId", "CustomDataKey");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("ItemId", "UserId", "IsFavorite");
+
+                    b.HasIndex("ItemId", "UserId", "LastPlayedDate");
+
+                    b.HasIndex("ItemId", "UserId", "PlaybackPositionTicks");
+
+                    b.HasIndex("ItemId", "UserId", "Played");
+
+                    b.ToTable("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Children")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "ParentItem")
+                        .WithMany("ParentAncestors")
+                        .HasForeignKey("ParentItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ParentItem");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany()
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Images")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("LockedFields")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Provider")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("TrailerTypes")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Chapters")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("ItemValues")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.ItemValue", "ItemValue")
+                        .WithMany("BaseItemsMap")
+                        .HasForeignKey("ItemValueId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("ItemValue");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("MediaStreams")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("Peoples")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.People", "People")
+                        .WithMany("BaseItems")
+                        .HasForeignKey("PeopleId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("People");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.BaseItemEntity", "Item")
+                        .WithMany("UserData")
+                        .HasForeignKey("ItemId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Item");
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b =>
+                {
+                    b.Navigation("Chapters");
+
+                    b.Navigation("Children");
+
+                    b.Navigation("Images");
+
+                    b.Navigation("ItemValues");
+
+                    b.Navigation("LockedFields");
+
+                    b.Navigation("MediaStreams");
+
+                    b.Navigation("ParentAncestors");
+
+                    b.Navigation("Peoples");
+
+                    b.Navigation("Provider");
+
+                    b.Navigation("TrailerTypes");
+
+                    b.Navigation("UserData");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b =>
+                {
+                    b.Navigation("BaseItemsMap");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.People", b =>
+                {
+                    b.Navigation("BaseItems");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.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
+        }
+    }
+}

+ 55 - 0
Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs

@@ -0,0 +1,55 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class MakeStartEndDateNullable : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "StartDate",
+                table: "BaseItems",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(DateTime),
+                oldType: "TEXT");
+
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "EndDate",
+                table: "BaseItems",
+                type: "TEXT",
+                nullable: true,
+                oldClrType: typeof(DateTime),
+                oldType: "TEXT");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "StartDate",
+                table: "BaseItems",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+                oldClrType: typeof(DateTime),
+                oldType: "TEXT",
+                oldNullable: true);
+
+            migrationBuilder.AlterColumn<DateTime>(
+                name: "EndDate",
+                table: "BaseItems",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
+                oldClrType: typeof(DateTime),
+                oldType: "TEXT",
+                oldNullable: true);
+        }
+    }
+}

+ 2 - 2
Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs

@@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<DateTime?>("DateModified")
                     b.Property<DateTime?>("DateModified")
                         .HasColumnType("TEXT");
                         .HasColumnType("TEXT");
 
 
-                    b.Property<DateTime>("EndDate")
+                    b.Property<DateTime?>("EndDate")
                         .HasColumnType("TEXT");
                         .HasColumnType("TEXT");
 
 
                     b.Property<string>("EpisodeTitle")
                     b.Property<string>("EpisodeTitle")
@@ -323,7 +323,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<string>("SortName")
                     b.Property<string>("SortName")
                         .HasColumnType("TEXT");
                         .HasColumnType("TEXT");
 
 
-                    b.Property<DateTime>("StartDate")
+                    b.Property<DateTime?>("StartDate")
                         .HasColumnType("TEXT");
                         .HasColumnType("TEXT");
 
 
                     b.Property<string>("Studios")
                     b.Property<string>("Studios")

+ 2 - 2
Jellyfin.Server.Implementations/Item/BaseItemRepository.cs

@@ -645,7 +645,7 @@ public sealed class BaseItemRepository
         // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
         // dto.MediaType = Enum.TryParse<MediaType>(entity.MediaType);
         if (dto is IHasStartDate hasStartDate)
         if (dto is IHasStartDate hasStartDate)
         {
         {
-            hasStartDate.StartDate = entity.StartDate;
+            hasStartDate.StartDate = entity.StartDate.GetValueOrDefault();
         }
         }
 
 
         // Fields that are present in the DB but are never actually used
         // Fields that are present in the DB but are never actually used
@@ -683,7 +683,7 @@ public sealed class BaseItemRepository
 
 
         entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
         entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null;
         entity.Path = GetPathToSave(dto.Path);
         entity.Path = GetPathToSave(dto.Path);
-        entity.EndDate = dto.EndDate.GetValueOrDefault();
+        entity.EndDate = dto.EndDate;
         entity.CommunityRating = dto.CommunityRating;
         entity.CommunityRating = dto.CommunityRating;
         entity.CustomRating = dto.CustomRating;
         entity.CustomRating = dto.CustomRating;
         entity.IndexNumber = dto.IndexNumber;
         entity.IndexNumber = dto.IndexNumber;

+ 2 - 4
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -116,17 +116,15 @@ namespace Jellyfin.Server.Implementations.Security
                 DeviceId = deviceId,
                 DeviceId = deviceId,
                 Version = version,
                 Version = version,
                 Token = token,
                 Token = token,
-                IsAuthenticated = false,
-                HasToken = false
+                IsAuthenticated = false
             };
             };
 
 
-            if (string.IsNullOrWhiteSpace(token))
+            if (!authInfo.HasToken)
             {
             {
                 // Request doesn't contain a token.
                 // Request doesn't contain a token.
                 return authInfo;
                 return authInfo;
             }
             }
 
 
-            authInfo.HasToken = true;
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
             await using (dbContext.ConfigureAwait(false))
             await using (dbContext.ConfigureAwait(false))
             {
             {

+ 8 - 0
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager
                     return;
                     return;
                 }
                 }
 
 
+                // We support video backdrops, but we should not generate trickplay images for them
+                var parentDirectory = Directory.GetParent(mediaPath);
+                if (parentDirectory is not null && string.Equals(parentDirectory.Name, "backdrops", StringComparison.OrdinalIgnoreCase))
+                {
+                    _logger.LogDebug("Ignoring backdrop media found at {Path} for item {ItemID}", mediaPath, video.Id);
+                    return;
+                }
+
                 // The width has to be even, otherwise a lot of filters will not be able to sample it
                 // The width has to be even, otherwise a lot of filters will not be able to sample it
                 var actualWidth = 2 * (width / 2);
                 var actualWidth = 2 * (width / 2);
 
 

+ 13 - 4
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -310,7 +310,6 @@ namespace MediaBrowser.Controller.MediaEncoding
         private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
         private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
         {
         {
             if (state.VideoStream is null
             if (state.VideoStream is null
-                || !options.EnableTonemapping
                 || GetVideoColorBitDepth(state) < 10
                 || GetVideoColorBitDepth(state) < 10
                 || !_mediaEncoder.SupportsFilter("tonemapx"))
                 || !_mediaEncoder.SupportsFilter("tonemapx"))
             {
             {
@@ -2062,7 +2061,13 @@ namespace MediaBrowser.Controller.MediaEncoding
                 // libx265 only accept level option in -x265-params.
                 // libx265 only accept level option in -x265-params.
                 // level option may cause libx265 to fail.
                 // level option may cause libx265 to fail.
                 // libx265 cannot adjust the given level, just throw an error.
                 // libx265 cannot adjust the given level, just throw an error.
-                param += " -x265-params:0 subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1:no-scenecut=1:no-open-gop=1:no-info=1";
+                param += " -x265-params:0 no-scenecut=1:no-open-gop=1:no-info=1";
+
+                if (encodingOptions.EncoderPreset < EncoderPreset.ultrafast)
+                {
+                    // The following params are slower than the ultrafast preset, don't use when ultrafast is selected.
+                    param += ":subme=3:merange=25:rc-lookahead=10:me=star:ctu=32:max-tu-size=32:min-cu-size=16:rskip=2:rskip-edge-threshold=2:no-sao=1:no-strong-intra-smoothing=1";
+                }
             }
             }
 
 
             if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
             if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
@@ -5691,7 +5696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                     if (!string.IsNullOrEmpty(doScaling)
                     if (!string.IsNullOrEmpty(doScaling)
                         && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
                         && !IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f))
                     {
                     {
-                        var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={outFormat}:afbc=1";
+                        // Vendor provided BSP kernel has an RGA driver bug that causes the output to be corrupted for P010 format.
+                        // Use NV15 instead of P010 to avoid the issue.
+                        // SDR inputs are using BGRA formats already which is not affected.
+                        var intermediateFormat = string.Equals(outFormat, "p010", StringComparison.OrdinalIgnoreCase) ? "nv15" : outFormat;
+                        var hwScaleFilterFirstPass = $"scale_rkrga=w=iw/7.9:h=ih/7.9:format={intermediateFormat}:force_divisible_by=4:afbc=1";
                         mainFilters.Add(hwScaleFilterFirstPass);
                         mainFilters.Add(hwScaleFilterFirstPass);
                     }
                     }
 
 
@@ -7065,7 +7074,7 @@ namespace MediaBrowser.Controller.MediaEncoding
             {
             {
                 // DTS and TrueHD are not supported by HLS
                 // DTS and TrueHD are not supported by HLS
                 // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
                 // Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
-                shiftAudioCodecs.Add("dca");
+                shiftAudioCodecs.Add("dts");
                 shiftAudioCodecs.Add("truehd");
                 shiftAudioCodecs.Add("truehd");
             }
             }
             else
             else

+ 10 - 10
MediaBrowser.Controller/Net/AuthorizationInfo.cs

@@ -1,6 +1,5 @@
-#nullable disable
-
 using System;
 using System;
+using System.Diagnostics.CodeAnalysis;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 
 
 namespace MediaBrowser.Controller.Net
 namespace MediaBrowser.Controller.Net
@@ -20,31 +19,31 @@ namespace MediaBrowser.Controller.Net
         /// Gets or sets the device identifier.
         /// Gets or sets the device identifier.
         /// </summary>
         /// </summary>
         /// <value>The device identifier.</value>
         /// <value>The device identifier.</value>
-        public string DeviceId { get; set; }
+        public string? DeviceId { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the device.
         /// Gets or sets the device.
         /// </summary>
         /// </summary>
         /// <value>The device.</value>
         /// <value>The device.</value>
-        public string Device { get; set; }
+        public string? Device { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the client.
         /// Gets or sets the client.
         /// </summary>
         /// </summary>
         /// <value>The client.</value>
         /// <value>The client.</value>
-        public string Client { get; set; }
+        public string? Client { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the version.
         /// Gets or sets the version.
         /// </summary>
         /// </summary>
         /// <value>The version.</value>
         /// <value>The version.</value>
-        public string Version { get; set; }
+        public string? Version { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the token.
         /// Gets or sets the token.
         /// </summary>
         /// </summary>
         /// <value>The token.</value>
         /// <value>The token.</value>
-        public string Token { get; set; }
+        public string? Token { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether the authorization is from an api key.
         /// Gets or sets a value indicating whether the authorization is from an api key.
@@ -54,7 +53,7 @@ namespace MediaBrowser.Controller.Net
         /// <summary>
         /// <summary>
         /// Gets or sets the user making the request.
         /// Gets or sets the user making the request.
         /// </summary>
         /// </summary>
-        public User User { get; set; }
+        public User? User { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether the token is authenticated.
         /// Gets or sets a value indicating whether the token is authenticated.
@@ -62,8 +61,9 @@ namespace MediaBrowser.Controller.Net
         public bool IsAuthenticated { get; set; }
         public bool IsAuthenticated { get; set; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets or sets a value indicating whether the request has a token.
+        /// Gets a value indicating whether the request has a token.
         /// </summary>
         /// </summary>
-        public bool HasToken { get; set; }
+        [MemberNotNullWhen(true, nameof(Token))]
+        public bool HasToken => !string.IsNullOrWhiteSpace(Token);
     }
     }
 }
 }

+ 23 - 14
MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs

@@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
     public class SubtitleEditParser : ISubtitleParser
     public class SubtitleEditParser : ISubtitleParser
     {
     {
         private readonly ILogger<SubtitleEditParser> _logger;
         private readonly ILogger<SubtitleEditParser> _logger;
-        private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats;
+        private readonly Dictionary<string, List<Type>> _subtitleFormatTypes;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
         /// Initializes a new instance of the <see cref="SubtitleEditParser"/> class.
@@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
         public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
         public SubtitleEditParser(ILogger<SubtitleEditParser> logger)
         {
         {
             _logger = logger;
             _logger = logger;
-            _subtitleFormats = GetSubtitleFormats()
-                .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension))
-                .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase)
-                .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase);
+            _subtitleFormatTypes = GetSubtitleFormatTypes();
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles
             var subtitle = new Subtitle();
             var subtitle = new Subtitle();
             var lines = stream.ReadAllLines().ToList();
             var lines = stream.ReadAllLines().ToList();
 
 
-            if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats))
+            if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension))
             {
             {
                 throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
                 throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension));
             }
             }
 
 
-            foreach (var subtitleFormat in subtitleFormats)
+            foreach (var subtitleFormatType in subtitleFormatTypesForExtension)
             {
             {
+                var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!;
                 _logger.LogDebug(
                 _logger.LogDebug(
                     "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
                     "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser",
                     fileExtension,
                     fileExtension,
@@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public bool SupportsFileExtension(string fileExtension)
         public bool SupportsFileExtension(string fileExtension)
-            => _subtitleFormats.ContainsKey(fileExtension);
+            => _subtitleFormatTypes.ContainsKey(fileExtension);
 
 
-        private List<SubtitleFormat> GetSubtitleFormats()
+        private Dictionary<string, List<Type>> GetSubtitleFormatTypes()
         {
         {
-            var subtitleFormats = new List<SubtitleFormat>();
+            var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase);
             var assembly = typeof(SubtitleFormat).Assembly;
             var assembly = typeof(SubtitleFormat).Assembly;
 
 
             foreach (var type in assembly.GetTypes())
             foreach (var type in assembly.GetTypes())
@@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles
 
 
                 try
                 try
                 {
                 {
-                    // It shouldn't be null, but the exception is caught if it is
-                    var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!;
-                    subtitleFormats.Add(subtitleFormat);
+                    var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!;
+                    var extension = tempInstance.Extension.TrimStart('.');
+                    if (!string.IsNullOrEmpty(extension))
+                    {
+                        // Store only the type, we will instantiate from it later
+                        if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension))
+                        {
+                            subtitleFormatTypes[extension] = [type];
+                        }
+                        else
+                        {
+                            subtitleFormatTypesForExtension.Add(type);
+                        }
+                    }
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
@@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
                 }
                 }
             }
             }
 
 
-            return subtitleFormats;
+            return subtitleFormatTypes;
         }
         }
     }
     }
 }
 }

+ 66 - 17
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna
         private readonly ITranscoderSupport _transcoderSupport;
         private readonly ITranscoderSupport _transcoderSupport;
         private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
         private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"];
         private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
         private static readonly string[] _supportedHlsAudioCodecsTs = ["aac", "ac3", "eac3", "mp3"];
-        private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd"];
+        private static readonly string[] _supportedHlsAudioCodecsMp4 = ["aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dts", "truehd"];
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
         /// Initializes a new instance of the <see cref="StreamBuilder"/> class.
@@ -862,18 +862,37 @@ namespace MediaBrowser.Model.Dlna
 
 
                     if (options.AllowAudioStreamCopy)
                     if (options.AllowAudioStreamCopy)
                     {
                     {
-                        if (ContainerHelper.ContainsContainer(transcodingProfile.AudioCodec, audioCodec))
+                        // For Audio stream, we prefer the audio codec that can be directly copied, then the codec that can otherwise satisfies
+                        // the transcoding conditions, then the one does not satisfy the transcoding conditions.
+                        // For example: A client can support both aac and flac, but flac only supports 2 channels while aac supports 6.
+                        // When the source audio is 6 channel flac, we should transcode to 6 channel aac, instead of down-mix to 2 channel flac.
+                        var transcodingAudioCodecs = ContainerHelper.Split(transcodingProfile.AudioCodec);
+
+                        foreach (var transcodingAudioCodec in transcodingAudioCodecs)
                         {
                         {
                             var appliedVideoConditions = options.Profile.CodecProfiles
                             var appliedVideoConditions = options.Profile.CodecProfiles
                                 .Where(i => i.Type == CodecType.VideoAudio &&
                                 .Where(i => i.Type == CodecType.VideoAudio &&
-                                    i.ContainsAnyCodec(audioCodec, container) &&
+                                    i.ContainsAnyCodec(transcodingAudioCodec, container) &&
                                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
                                     i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)))
                                 .Select(i =>
                                 .Select(i =>
                                     i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
                                     i.Conditions.All(condition => ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioChannels, audioBitrate, audioSampleRate, audioBitDepth, audioProfile, false)));
 
 
                             // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
                             // An empty appliedVideoConditions means that the codec has no conditions for the current audio stream
                             var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
                             var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
-                            rank.Audio = conditionsSatisfied ? 1 : 2;
+
+                            var rankAudio = 3;
+
+                            if (conditionsSatisfied)
+                            {
+                                rankAudio = string.Equals(transcodingAudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ? 1 : 2;
+                            }
+
+                            rank.Audio = Math.Min(rank.Audio, rankAudio);
+
+                            if (rank.Audio == 1)
+                            {
+                                break;
+                            }
                         }
                         }
                     }
                     }
 
 
@@ -963,9 +982,28 @@ namespace MediaBrowser.Model.Dlna
 
 
             var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
             var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerHelper.ContainsContainer(audioCodecs, false, stream.Codec)).FirstOrDefault();
 
 
-            var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
+            var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && audioStreamWithSupportedCodec.Channels > (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue);
+
+            var directAudioStreamSatisfied = audioStreamWithSupportedCodec is not null && !channelsExceedsLimit
+                && options.Profile.CodecProfiles
+                    .Where(i => i.Type == CodecType.VideoAudio
+                        && i.ContainsAnyCodec(audioStreamWithSupportedCodec.Codec, container)
+                        && i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoAudioConditionSatisfied(applyCondition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false)))
+                    .Select(i => i.Conditions.All(condition =>
+                    {
+                        var satisfied = ConditionProcessor.IsVideoAudioConditionSatisfied(condition, audioStreamWithSupportedCodec.Channels, audioStreamWithSupportedCodec.BitRate, audioStreamWithSupportedCodec.SampleRate, audioStreamWithSupportedCodec.BitDepth, audioStreamWithSupportedCodec.Profile, false);
+                        if (!satisfied)
+                        {
+                            playlistItem.TranscodeReasons |= GetTranscodeReasonForFailedCondition(condition);
+                        }
+
+                        return satisfied;
+                    }))
+                    .All(satisfied => satisfied);
 
 
-            var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
+            directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit);
+
+            var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : null;
 
 
             if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
             if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
             {
             {
@@ -2213,7 +2251,7 @@ namespace MediaBrowser.Model.Dlna
             }
             }
         }
         }
 
 
-        private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+        private static bool IsAudioContainerSupported(DirectPlayProfile profile, MediaSourceInfo item)
         {
         {
             // Check container type
             // Check container type
             if (!profile.SupportsContainer(item.Container))
             if (!profile.SupportsContainer(item.Container))
@@ -2221,6 +2259,20 @@ namespace MediaBrowser.Model.Dlna
                 return false;
                 return false;
             }
             }
 
 
+            // Never direct play audio in matroska when the device only declare support for webm.
+            // The first check is not enough because mkv is assumed can be webm.
+            // See https://github.com/jellyfin/jellyfin/issues/13344
+            return !ContainerHelper.ContainsContainer("mkv", item.Container)
+                   || profile.SupportsContainer("mkv");
+        }
+
+        private static bool IsAudioDirectPlaySupported(DirectPlayProfile profile, MediaSourceInfo item, MediaStream audioStream)
+        {
+            if (!IsAudioContainerSupported(profile, item))
+            {
+                return false;
+            }
+
             // Check audio codec
             // Check audio codec
             string? audioCodec = audioStream?.Codec;
             string? audioCodec = audioStream?.Codec;
             if (!profile.SupportsAudioCodec(audioCodec))
             if (!profile.SupportsAudioCodec(audioCodec))
@@ -2235,19 +2287,16 @@ namespace MediaBrowser.Model.Dlna
         {
         {
             // Check container type, this should NOT be supported
             // Check container type, this should NOT be supported
             // If the container is supported, the file should be directly played
             // If the container is supported, the file should be directly played
-            if (!profile.SupportsContainer(item.Container))
+            if (IsAudioContainerSupported(profile, item))
             {
             {
-                // Check audio codec, we cannot use the SupportsAudioCodec here
-                // Because that one assumes empty container supports all codec, which is just useless
-                string? audioCodec = audioStream?.Codec;
-                if (string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase) ||
-                    string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase))
-                {
-                    return true;
-                }
+                return false;
             }
             }
 
 
-            return false;
+            // Check audio codec, we cannot use the SupportsAudioCodec here
+            // Because that one assumes empty container supports all codec, which is just useless
+            string? audioCodec = audioStream?.Codec;
+            return string.Equals(profile.AudioCodec, audioCodec, StringComparison.OrdinalIgnoreCase)
+                   || string.Equals(profile.Container, audioCodec, StringComparison.OrdinalIgnoreCase);
         }
         }
 
 
         private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)
         private int GetRank(ref TranscodeReason a, TranscodeReason[] rankings)

+ 29 - 0
MediaBrowser.Model/Dlna/TranscodingProfile.cs

@@ -1,3 +1,4 @@
+using System;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Xml.Serialization;
 using System.Xml.Serialization;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
@@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna;
 
 
 /// <summary>
 /// <summary>
 /// A class for transcoding profile information.
 /// A class for transcoding profile information.
+/// Note for client developers: Conditions defined in <see cref="CodecProfile"/> has higher priority and can override values defined here.
 /// </summary>
 /// </summary>
 public class TranscodingProfile
 public class TranscodingProfile
 {
 {
@@ -17,6 +19,33 @@ public class TranscodingProfile
         Conditions = [];
         Conditions = [];
     }
     }
 
 
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TranscodingProfile" /> class copying the values from another instance.
+    /// </summary>
+    /// <param name="other">Another instance of <see cref="TranscodingProfile" /> to be copied.</param>
+    public TranscodingProfile(TranscodingProfile other)
+    {
+        ArgumentNullException.ThrowIfNull(other);
+
+        Container = other.Container;
+        Type = other.Type;
+        VideoCodec = other.VideoCodec;
+        AudioCodec = other.AudioCodec;
+        Protocol = other.Protocol;
+        EstimateContentLength = other.EstimateContentLength;
+        EnableMpegtsM2TsMode = other.EnableMpegtsM2TsMode;
+        TranscodeSeekInfo = other.TranscodeSeekInfo;
+        CopyTimestamps = other.CopyTimestamps;
+        Context = other.Context;
+        EnableSubtitlesInManifest = other.EnableSubtitlesInManifest;
+        MaxAudioChannels = other.MaxAudioChannels;
+        MinSegments = other.MinSegments;
+        SegmentLength = other.SegmentLength;
+        BreakOnNonKeyFrames = other.BreakOnNonKeyFrames;
+        Conditions = other.Conditions;
+        EnableAudioVbrEncoding = other.EnableAudioVbrEncoding;
+    }
+
     /// <summary>
     /// <summary>
     /// Gets or sets the container.
     /// Gets or sets the container.
     /// </summary>
     /// </summary>

+ 1 - 0
MediaBrowser.Providers/Manager/ImageSaver.cs

@@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager
 
 
                 var fileStreamOptions = AsyncFile.WriteOptions;
                 var fileStreamOptions = AsyncFile.WriteOptions;
                 fileStreamOptions.Mode = FileMode.Create;
                 fileStreamOptions.Mode = FileMode.Create;
+                fileStreamOptions.Options = FileOptions.WriteThrough;
                 if (source.CanSeek)
                 if (source.CanSeek)
                 {
                 {
                     fileStreamOptions.PreallocationSize = source.Length;
                     fileStreamOptions.PreallocationSize = source.Length;

+ 3 - 3
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo
             if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
             if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
             {
             {
                 var people = new List<PersonInfo>();
                 var people = new List<PersonInfo>();
-                var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
+                var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator);
 
 
                 if (libraryOptions.UseCustomTagDelimiters)
                 if (libraryOptions.UseCustomTagDelimiters)
                 {
                 {
@@ -214,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 if (performers is null || performers.Length == 0)
                 if (performers is null || performers.Length == 0)
                 {
                 {
-                    performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
+                    performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator);
                 }
                 }
 
 
                 if (libraryOptions.UseCustomTagDelimiters)
                 if (libraryOptions.UseCustomTagDelimiters)
@@ -318,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             if (!audio.LockedFields.Contains(MetadataField.Genres))
             if (!audio.LockedFields.Contains(MetadataField.Genres))
             {
             {
-                var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+                var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 
 
                 if (libraryOptions.UseCustomTagDelimiters)
                 if (libraryOptions.UseCustomTagDelimiters)
                 {
                 {

+ 23 - 22
MediaBrowser.Providers/TV/SeriesMetadataService.cs

@@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV
 
 
         private void RemoveObsoleteEpisodes(Series series)
         private void RemoveObsoleteEpisodes(Series series)
         {
         {
-            var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
-            var numberOfEpisodes = episodes.Count;
-            // TODO: O(n^2), but can it be done faster without overcomplicating it?
-            for (var i = 0; i < numberOfEpisodes; i++)
+            var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true)
+                            .OfType<Episode>()
+                            .GroupBy(e => e.ParentIndexNumber)
+                            .ToList();
+
+            foreach (var seasonEpisodes in episodesBySeason)
             {
             {
-                var currentEpisode = episodes[i];
-                // The outer loop only examines virtual episodes
-                if (!currentEpisode.IsVirtualItem)
+                List<Episode> nonPhysicalEpisodes = [];
+                List<Episode> physicalEpisodes = [];
+                foreach (var episode in seasonEpisodes)
                 {
                 {
-                    continue;
-                }
+                    if (episode.IsVirtualItem || episode.IsMissingEpisode)
+                    {
+                        nonPhysicalEpisodes.Add(episode);
+                        continue;
+                    }
 
 
-                // Virtual episodes without an episode number are practically orphaned and should be deleted
-                if (!currentEpisode.IndexNumber.HasValue)
-                {
-                    DeleteEpisode(currentEpisode);
-                    continue;
+                    physicalEpisodes.Add(episode);
                 }
                 }
 
 
-                for (var j = i + 1; j < numberOfEpisodes; j++)
+                // Only consider non-physical episodes
+                foreach (var episode in nonPhysicalEpisodes)
                 {
                 {
-                    var comparisonEpisode = episodes[j];
-                    // The inner loop is only for "physical" episodes
-                    if (comparisonEpisode.IsVirtualItem
-                        || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber
-                        || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value))
+                    // Episodes without an episode number are practically orphaned and should be deleted
+                    // Episodes with a physical equivalent should be deleted (they are no longer missing)
+                    var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value));
+
+                    if (shouldKeep)
                     {
                     {
                         continue;
                         continue;
                     }
                     }
 
 
-                    DeleteEpisode(currentEpisode);
-                    break;
+                    DeleteEpisode(episode);
                 }
                 }
             }
             }
         }
         }

+ 16 - 22
MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs

@@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             {
             {
                 case "id":
                 case "id":
                     {
                     {
-                        // get ids from attributes
+                        // Get ids from attributes
+                        item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
+                        item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
                         string? imdbId = reader.GetAttribute("IMDB");
                         string? imdbId = reader.GetAttribute("IMDB");
-                        string? tmdbId = reader.GetAttribute("TMDB");
 
 
-                        // read id from content
+                        // Read id from content
+                        // Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
                         var contentId = reader.ReadElementContentAsString();
                         var contentId = reader.ReadElementContentAsString();
-                        if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId))
+                        if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
                         {
                         {
                             imdbId = contentId;
                             imdbId = contentId;
                         }
                         }
-                        else if (string.IsNullOrEmpty(tmdbId))
-                        {
-                            tmdbId = contentId;
-                        }
 
 
                         item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
                         item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
-                        item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
 
 
                         break;
                         break;
                     }
                     }
@@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers
 
 
                         if (!string.IsNullOrWhiteSpace(val) && movie is not null)
                         if (!string.IsNullOrWhiteSpace(val) && movie is not null)
                         {
                         {
-                            // TODO Handle this better later
-                            if (!val.Contains('<', StringComparison.Ordinal))
+                            try
                             {
                             {
-                                movie.CollectionName = val;
+                                ParseSetXml(val, movie);
                             }
                             }
-                            else
+                            catch (Exception ex)
                             {
                             {
-                                try
-                                {
-                                    ParseSetXml(val, movie);
-                                }
-                                catch (Exception ex)
-                                {
-                                    Logger.LogError(ex, "Error parsing set node");
-                                }
+                                Logger.LogError(ex, "Error parsing set node");
                             }
                             }
                         }
                         }
 
 
@@ -139,7 +128,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     // Loop through each element
                     // Loop through each element
                     while (!reader.EOF && reader.ReadState == ReadState.Interactive)
                     while (!reader.EOF && reader.ReadState == ReadState.Interactive)
                     {
                     {
-                        if (reader.NodeType == XmlNodeType.Element)
+                        if (reader.NodeType == XmlNodeType.Text && reader.Depth == 1)
+                        {
+                            movie.CollectionName = reader.Value;
+                            break;
+                        }
+                        else if (reader.NodeType == XmlNodeType.Element)
                         {
                         {
                             switch (reader.Name)
                             switch (reader.Name)
                             {
                             {

+ 10 - 5
MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Globalization;
 using System.Globalization;
 using System.Xml;
 using System.Xml;
 using Emby.Naming.TV;
 using Emby.Naming.TV;
@@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers
             {
             {
                 case "id":
                 case "id":
                     {
                     {
-                        item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
+                        // Get ids from attributes
                         item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
                         item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
+                        item.TrySetProviderId(MetadataProvider.Tvdb, reader.GetAttribute("TVDB"));
+                        string? imdbId = reader.GetAttribute("IMDB");
 
 
-                        string? tvdbId = reader.GetAttribute("TVDB");
-                        if (string.IsNullOrWhiteSpace(tvdbId))
+                        // Read id from content
+                        // Content can be arbitrary according to Kodi wiki, so only parse if we are sure it matches a provider-specific schema
+                        var contentId = reader.ReadElementContentAsString();
+                        if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal))
                         {
                         {
-                            tvdbId = reader.ReadElementContentAsString();
+                            imdbId = contentId;
                         }
                         }
 
 
-                        item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
+                        item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
 
 
                         break;
                         break;
                     }
                     }

+ 3 - 1
MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs

@@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
             {
             {
                 if (!string.IsNullOrEmpty(movie.CollectionName))
                 if (!string.IsNullOrEmpty(movie.CollectionName))
                 {
                 {
-                    writer.WriteElementString("set", movie.CollectionName);
+                    writer.WriteStartElement("set");
+                    writer.WriteElementString("name", movie.CollectionName);
+                    writer.WriteEndElement();
                 }
                 }
             }
             }
         }
         }

+ 3 - 1
src/Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder
             return string.Empty;
             return string.Empty;
         }
         }
 
 
+        // Use FileStream with FileShare.Read instead of having Skia open the file to allow concurrent read access
+        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
         // Any larger than 128x128 is too slow and there's no visually discernible difference
         // Any larger than 128x128 is too slow and there's no visually discernible difference
-        return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
+        return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128);
     }
     }
 
 
     private bool RequiresSpecialCharacterHack(string path)
     private bool RequiresSpecialCharacterHack(string path)

+ 109 - 82
src/Jellyfin.LiveTv/Guide/GuideManager.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Entities.Libraries;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using Jellyfin.LiveTv.Configuration;
 using Jellyfin.LiveTv.Configuration;
@@ -39,6 +40,11 @@ public class GuideManager : IGuideManager
     private readonly IRecordingsManager _recordingsManager;
     private readonly IRecordingsManager _recordingsManager;
     private readonly LiveTvDtoService _tvDtoService;
     private readonly LiveTvDtoService _tvDtoService;
 
 
+    /// <summary>
+    /// Amount of days images are pre-cached from external sources.
+    /// </summary>
+    public const int MaxCacheDays = 2;
+
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="GuideManager"/> class.
     /// Initializes a new instance of the <see cref="GuideManager"/> class.
     /// </summary>
     /// </summary>
@@ -204,14 +210,14 @@ public class GuideManager : IGuideManager
         progress.Report(15);
         progress.Report(15);
 
 
         numComplete = 0;
         numComplete = 0;
-        var programs = new List<Guid>();
+        var programs = new List<LiveTvProgram>();
         var channels = new List<Guid>();
         var channels = new List<Guid>();
 
 
         var guideDays = GetGuideDays();
         var guideDays = GetGuideDays();
 
 
-        _logger.LogInformation("Refreshing guide with {0} days of guide data", guideDays);
+        _logger.LogInformation("Refreshing guide with {Days} days of guide data", guideDays);
 
 
-        var maxCacheDate = DateTime.UtcNow.AddDays(2);
+        var maxCacheDate = DateTime.UtcNow.AddDays(MaxCacheDays);
         foreach (var currentChannel in list)
         foreach (var currentChannel in list)
         {
         {
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
@@ -237,22 +243,23 @@ public class GuideManager : IGuideManager
                     DtoOptions = new DtoOptions(true)
                     DtoOptions = new DtoOptions(true)
                 }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
                 }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
 
 
-                var newPrograms = new List<LiveTvProgram>();
-                var updatedPrograms = new List<BaseItem>();
+                var newPrograms = new List<Guid>();
+                var updatedPrograms = new List<Guid>();
 
 
                 foreach (var program in channelPrograms)
                 foreach (var program in channelPrograms)
                 {
                 {
                     var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
                     var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel);
+                    var id = programItem.Id;
                     if (isNew)
                     if (isNew)
                     {
                     {
-                        newPrograms.Add(programItem);
+                        newPrograms.Add(id);
                     }
                     }
                     else if (isUpdated)
                     else if (isUpdated)
                     {
                     {
-                        updatedPrograms.Add(programItem);
+                        updatedPrograms.Add(id);
                     }
                     }
 
 
-                    programs.Add(programItem.Id);
+                    programs.Add(programItem);
 
 
                     isMovie |= program.IsMovie;
                     isMovie |= program.IsMovie;
                     isSeries |= program.IsSeries;
                     isSeries |= program.IsSeries;
@@ -261,24 +268,30 @@ public class GuideManager : IGuideManager
                     isKids |= program.IsKids;
                     isKids |= program.IsKids;
                 }
                 }
 
 
-                _logger.LogDebug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
+                _logger.LogDebug(
+                    "Channel {Name} has {NewCount} new programs and {UpdatedCount} updated programs",
+                    currentChannel.Name,
+                    newPrograms.Count,
+                    updatedPrograms.Count);
 
 
                 if (newPrograms.Count > 0)
                 if (newPrograms.Count > 0)
                 {
                 {
-                    _libraryManager.CreateOrUpdateItems(newPrograms, null, cancellationToken);
-                    await PrecacheImages(newPrograms, maxCacheDate).ConfigureAwait(false);
+                    var newProgramDtos = programs.Where(b => newPrograms.Contains(b.Id)).ToList();
+                    _libraryManager.CreateOrUpdateItems(newProgramDtos, null, cancellationToken);
                 }
                 }
 
 
                 if (updatedPrograms.Count > 0)
                 if (updatedPrograms.Count > 0)
                 {
                 {
+                    var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList();
                     await _libraryManager.UpdateItemsAsync(
                     await _libraryManager.UpdateItemsAsync(
-                        updatedPrograms,
+                        updatedProgramDtos,
                         currentChannel,
                         currentChannel,
                         ItemUpdateType.MetadataImport,
                         ItemUpdateType.MetadataImport,
                         cancellationToken).ConfigureAwait(false);
                         cancellationToken).ConfigureAwait(false);
-                    await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false);
                 }
                 }
 
 
+                await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false);
+
                 currentChannel.IsMovie = isMovie;
                 currentChannel.IsMovie = isMovie;
                 currentChannel.IsNews = isNews;
                 currentChannel.IsNews = isNews;
                 currentChannel.IsSports = isSports;
                 currentChannel.IsSports = isSports;
@@ -313,7 +326,8 @@ public class GuideManager : IGuideManager
         }
         }
 
 
         progress.Report(100);
         progress.Report(100);
-        return new Tuple<List<Guid>, List<Guid>>(channels, programs);
+        var programIds = programs.Select(p => p.Id).ToList();
+        return new Tuple<List<Guid>, List<Guid>>(channels, programIds);
     }
     }
 
 
     private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
     private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
@@ -618,77 +632,17 @@ public class GuideManager : IGuideManager
         item.IndexNumber = info.EpisodeNumber;
         item.IndexNumber = info.EpisodeNumber;
         item.ParentIndexNumber = info.SeasonNumber;
         item.ParentIndexNumber = info.SeasonNumber;
 
 
-        if (!item.HasImage(ImageType.Primary))
-        {
-            if (!string.IsNullOrWhiteSpace(info.ImagePath))
-            {
-                item.SetImage(
-                    new ItemImageInfo
-                    {
-                        Path = info.ImagePath,
-                        Type = ImageType.Primary
-                    },
-                    0);
-            }
-            else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
-            {
-                item.SetImage(
-                    new ItemImageInfo
-                    {
-                        Path = info.ImageUrl,
-                        Type = ImageType.Primary
-                    },
-                    0);
-            }
-        }
+        forceUpdate = forceUpdate || UpdateImages(item, info);
 
 
-        if (!item.HasImage(ImageType.Thumb))
-        {
-            if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl))
-            {
-                item.SetImage(
-                    new ItemImageInfo
-                    {
-                        Path = info.ThumbImageUrl,
-                        Type = ImageType.Thumb
-                    },
-                    0);
-            }
-        }
-
-        if (!item.HasImage(ImageType.Logo))
+        if (isNew)
         {
         {
-            if (!string.IsNullOrWhiteSpace(info.LogoImageUrl))
-            {
-                item.SetImage(
-                    new ItemImageInfo
-                    {
-                        Path = info.LogoImageUrl,
-                        Type = ImageType.Logo
-                    },
-                    0);
-            }
-        }
+            item.OnMetadataChanged();
 
 
-        if (!item.HasImage(ImageType.Backdrop))
-        {
-            if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl))
-            {
-                item.SetImage(
-                    new ItemImageInfo
-                    {
-                        Path = info.BackdropImageUrl,
-                        Type = ImageType.Backdrop
-                    },
-                    0);
-            }
+            return (item, isNew, false);
         }
         }
 
 
         var isUpdated = false;
         var isUpdated = false;
-        if (isNew)
-        {
-        }
-        else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
+        if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
         {
         {
             isUpdated = true;
             isUpdated = true;
         }
         }
@@ -703,7 +657,7 @@ public class GuideManager : IGuideManager
             }
             }
         }
         }
 
 
-        if (isNew || isUpdated)
+        if (isUpdated)
         {
         {
             item.OnMetadataChanged();
             item.OnMetadataChanged();
         }
         }
@@ -711,7 +665,80 @@ public class GuideManager : IGuideManager
         return (item, isNew, isUpdated);
         return (item, isNew, isUpdated);
     }
     }
 
 
-    private async Task PrecacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
+    private static bool UpdateImages(BaseItem item, ProgramInfo info)
+    {
+        var updated = false;
+
+        // Primary
+        updated |= UpdateImage(ImageType.Primary, item, info);
+
+        // Thumbnail
+        updated |= UpdateImage(ImageType.Thumb, item, info);
+
+        // Logo
+        updated |= UpdateImage(ImageType.Logo, item, info);
+
+        // Backdrop
+        return updated || UpdateImage(ImageType.Backdrop, item, info);
+    }
+
+    private static bool UpdateImage(ImageType imageType, BaseItem item, ProgramInfo info)
+    {
+        var image = item.GetImages(imageType).FirstOrDefault();
+        var currentImagePath = image?.Path;
+        var newImagePath = imageType switch
+        {
+            ImageType.Primary => info.ImagePath,
+            _ => string.Empty
+        };
+        var newImageUrl = imageType switch
+        {
+            ImageType.Backdrop => info.BackdropImageUrl,
+            ImageType.Logo => info.LogoImageUrl,
+            ImageType.Primary => info.ImageUrl,
+            ImageType.Thumb => info.ThumbImageUrl,
+            _ => string.Empty
+        };
+
+        var differentImage = newImageUrl?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false
+                                || newImagePath?.Equals(currentImagePath, StringComparison.OrdinalIgnoreCase) == false;
+        if (!differentImage)
+        {
+            return false;
+        }
+
+        if (!string.IsNullOrWhiteSpace(newImagePath))
+        {
+            item.SetImage(
+                new ItemImageInfo
+                {
+                    Path = newImagePath,
+                    Type = imageType
+                },
+                0);
+
+            return true;
+        }
+
+        if (!string.IsNullOrWhiteSpace(newImageUrl))
+        {
+            item.SetImage(
+                new ItemImageInfo
+                {
+                    Path = newImageUrl,
+                    Type = imageType
+                },
+                0);
+
+            return true;
+        }
+
+        item.RemoveImage(image);
+
+        return false;
+    }
+
+    private async Task PreCacheImages(IReadOnlyList<BaseItem> programs, DateTime maxCacheDate)
     {
     {
         await Parallel.ForEachAsync(
         await Parallel.ForEachAsync(
             programs
             programs
@@ -741,7 +768,7 @@ public class GuideManager : IGuideManager
                         }
                         }
                         catch (Exception ex)
                         catch (Exception ex)
                         {
                         {
-                            _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path);
+                            _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path);
                         }
                         }
                     }
                     }
                 }
                 }

+ 15 - 16
src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs

@@ -19,6 +19,7 @@ using System.Threading.Tasks;
 using AsyncKeyedLock;
 using AsyncKeyedLock;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json;
+using Jellyfin.LiveTv.Guide;
 using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
 using Jellyfin.LiveTv.Listings.SchedulesDirectDtos;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
@@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly AsyncNonKeyedLocker _tokenLock = new(1);
         private readonly AsyncNonKeyedLocker _tokenLock = new(1);
 
 
-        private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
+        private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new();
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private DateTime _lastErrorResponse;
         private DateTime _lastErrorResponse;
         private bool _disposed = false;
         private bool _disposed = false;
@@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings
             {
             {
                 _logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
                 _logger.LogWarning("SchedulesDirect token is empty, returning empty program list");
 
 
-                return Enumerable.Empty<ProgramInfo>();
+                return [];
             }
             }
 
 
             var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
             var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
@@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings
             _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
             _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
             var requestList = new List<RequestScheduleForChannelDto>()
             var requestList = new List<RequestScheduleForChannelDto>()
                 {
                 {
-                    new RequestScheduleForChannelDto()
+                    new()
                     {
                     {
                         StationId = channelId,
                         StationId = channelId,
                         Date = dates
                         Date = dates
@@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings
             var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
             var dailySchedules = await Request<IReadOnlyList<DayDto>>(options, true, info, cancellationToken).ConfigureAwait(false);
             if (dailySchedules is null)
             if (dailySchedules is null)
             {
             {
-                return Array.Empty<ProgramInfo>();
+                return [];
             }
             }
 
 
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
             _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
@@ -120,17 +121,17 @@ namespace Jellyfin.LiveTv.Listings
             var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
             var programIds = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
             programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
             programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
 
 
-            var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken)
-                    .ConfigureAwait(false);
+            var programDetails = await Request<IReadOnlyList<ProgramDetailsDto>>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             if (programDetails is null)
             if (programDetails is null)
             {
             {
-                return Array.Empty<ProgramInfo>();
+                return [];
             }
             }
 
 
             var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
             var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y);
 
 
             var programIdsWithImages = programDetails
             var programIdsWithImages = programDetails
-                .Where(p => p.HasImageArtwork).Select(p => p.ProgramId)
+                .Where(p => p.HasImageArtwork)
+                .Select(p => p.ProgramId)
                 .ToList();
                 .ToList();
 
 
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
             var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings
             var programsInfo = new List<ProgramInfo>();
             var programsInfo = new List<ProgramInfo>();
             foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
             foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
             {
             {
-                // _logger.LogDebug("Processing Schedule for station ID " + stationID +
-                //              " which corresponds to channel " + channelNumber + " and program id " +
-                //              schedule.ProgramId + " which says it has images? " +
-                //              programDict[schedule.ProgramId].hasImageArtwork);
-
                 if (string.IsNullOrEmpty(schedule.ProgramId))
                 if (string.IsNullOrEmpty(schedule.ProgramId))
                 {
                 {
                     continue;
                     continue;
                 }
                 }
 
 
-                if (images is not null)
+                // Only add images which will be pre-cached until we can implement dynamic token fetching
+                var endDate = schedule.AirDateTime?.AddSeconds(schedule.Duration);
+                var willBeCached = endDate.HasValue && endDate.Value < DateTime.UtcNow.AddDays(GuideManager.MaxCacheDays);
+                if (willBeCached && images is not null)
                 {
                 {
                     var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
                     var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
                     if (imageIndex > -1)
                     if (imageIndex > -1)
@@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings
 
 
             if (programIds.Count == 0)
             if (programIds.Count == 0)
             {
             {
-                return Array.Empty<ShowImagesDto>();
+                return [];
             }
             }
 
 
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
@@ -483,7 +482,7 @@ namespace Jellyfin.LiveTv.Listings
             {
             {
                 _logger.LogError(ex, "Error getting image info from schedules direct");
                 _logger.LogError(ex, "Error getting image info from schedules direct");
 
 
-                return Array.Empty<ShowImagesDto>();
+                return [];
             }
             }
         }
         }
 
 

+ 7 - 2
src/Jellyfin.Networking/Manager/NetworkManager.cs

@@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable
                 return false;
                 return false;
             }
             }
         }
         }
-        else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+        else if (!IsInLocalNetwork(remoteIP))
         {
         {
             // Remote not enabled. So everyone should be LAN.
             // Remote not enabled. So everyone should be LAN.
             return false;
             return false;
@@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable
             // Get interface matching override subnet
             // Get interface matching override subnet
             var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
             var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
 
 
-            if (intf?.Address is not null)
+            if (intf?.Address is not null
+                || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any))
+                || (data.Data.AddressFamily == AddressFamily.InterNetworkV6 && data.Data.Address.Equals(IPAddress.IPv6Any)))
             {
             {
                 // If matching interface is found, use override
                 // If matching interface is found, use override
                 bindPreference = data.OverrideUri;
                 bindPreference = data.OverrideUri;
@@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable
         }
         }
 
 
         _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
         _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
+
         return true;
         return true;
     }
     }
 
 
@@ -1063,6 +1066,7 @@ public class NetworkManager : INetworkManager, IDisposable
                 // If none exists, this will select the first external interface if there is one.
                 // If none exists, this will select the first external interface if there is one.
                 bindAddress = externalInterfaces
                 bindAddress = externalInterfaces
                     .OrderByDescending(x => x.Subnet.Contains(source))
                     .OrderByDescending(x => x.Subnet.Contains(source))
+                    .ThenByDescending(x => x.Subnet.PrefixLength)
                     .ThenBy(x => x.Index)
                     .ThenBy(x => x.Index)
                     .Select(x => x.Address)
                     .Select(x => x.Address)
                     .First();
                     .First();
@@ -1080,6 +1084,7 @@ public class NetworkManager : INetworkManager, IDisposable
             // If none exists, this will select the first internal interface if there is one.
             // If none exists, this will select the first internal interface if there is one.
             bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
             bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
                 .OrderByDescending(x => x.Subnet.Contains(source))
                 .OrderByDescending(x => x.Subnet.Contains(source))
+                .ThenByDescending(x => x.Subnet.PrefixLength)
                 .ThenBy(x => x.Index)
                 .ThenBy(x => x.Index)
                 .Select(x => x.Address)
                 .Select(x => x.Address)
                 .FirstOrDefault();
                 .FirstOrDefault();

+ 2 - 1
tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs

@@ -101,6 +101,7 @@ namespace Jellyfin.Api.Tests.Auth
             var authorizationInfo = SetupUser();
             var authorizationInfo = SetupUser();
             var authenticateResult = await _sut.AuthenticateAsync();
             var authenticateResult = await _sut.AuthenticateAsync();
 
 
+            Assert.NotNull(authorizationInfo.User);
             Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
             Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username));
         }
         }
 
 
@@ -112,6 +113,7 @@ namespace Jellyfin.Api.Tests.Auth
             var authorizationInfo = SetupUser(isAdmin);
             var authorizationInfo = SetupUser(isAdmin);
             var authenticateResult = await _sut.AuthenticateAsync();
             var authenticateResult = await _sut.AuthenticateAsync();
 
 
+            Assert.NotNull(authorizationInfo.User);
             var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
             var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User;
             Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole));
             Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole));
         }
         }
@@ -133,7 +135,6 @@ namespace Jellyfin.Api.Tests.Auth
             authorizationInfo.User.AddDefaultPreferences();
             authorizationInfo.User.AddDefaultPreferences();
             authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
             authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
             authorizationInfo.IsApiKey = false;
             authorizationInfo.IsApiKey = false;
-            authorizationInfo.HasToken = true;
             authorizationInfo.Token = "fake-token";
             authorizationInfo.Token = "fake-token";
 
 
             _jellyfinAuthServiceMock.Setup(
             _jellyfinAuthServiceMock.Setup(

+ 1 - 1
tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs

@@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
             await localizationManager.LoadAll();
             await localizationManager.LoadAll();
             var ratings = localizationManager.GetParentalRatings().ToList();
             var ratings = localizationManager.GetParentalRatings().ToList();
 
 
-            Assert.Equal(54, ratings.Count);
+            Assert.Equal(56, ratings.Count);
 
 
             var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
             var tvma = ratings.FirstOrDefault(x => x.Name.Equals("TV-MA", StringComparison.Ordinal));
             Assert.NotNull(tvma);
             Assert.NotNull(tvma);

+ 18 - 0
tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs

@@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
 
 
             Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
             Assert.Throws<ArgumentException>(() => _parser.Fetch(result, string.Empty, CancellationToken.None));
         }
         }
+
+        [Fact]
+        public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success()
+        {
+            var result = new MetadataResult<Video>()
+            {
+                Item = new Movie()
+            };
+
+            _parser.Fetch(result, "Test Data/Lilo & Stitch.nfo", CancellationToken.None);
+            var item = (Movie)result.Item;
+
+            Assert.Equal("Lilo & Stitch", item.Name);
+            Assert.Equal("Lilo & Stitch", item.OriginalTitle);
+            Assert.Equal("Lilo & Stitch Collection", item.CollectionName);
+            Assert.StartsWith(">>", item.Overview, StringComparison.InvariantCulture);
+            Assert.EndsWith("<<", item.Overview, StringComparison.InvariantCulture);
+        }
     }
     }
 }
 }

+ 7 - 0
tests/Jellyfin.XbmcMetadata.Tests/Test Data/Lilo & Stitch.nfo

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<movie>
+  <title>Lilo &amp; Stitch</title>
+  <originaltitle>Lilo &amp; Stitch</originaltitle>
+  <set>Lilo &amp; Stitch Collection</set>
+  <plot>&gt;&gt;As Stitch, a runaway genetic experiment from a faraway planet, wreaks havoc on the Hawaiian Islands, he becomes the mischievous adopted alien "puppy" of an independent little girl named Lilo and learns about loyalty, friendship, and ʻohana, the Hawaiian tradition of family.&lt;&lt;</plot>
+</movie>