diff --git a/Directory.Packages.props b/Directory.Packages.props index 5921d3c00c..7295588987 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,14 +9,14 @@ - - + + - + @@ -51,7 +51,7 @@ - + @@ -80,7 +80,7 @@ - + diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 46c128dedc..66b7839f77 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/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); } + 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 /// public virtual IEnumerable 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 enumerationOptions = GetEnumerationOptions(recursive); diff --git a/Emby.Server.Implementations/Localization/Core/ht.json b/Emby.Server.Implementations/Localization/Core/ht.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ht.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Ratings/br.csv b/Emby.Server.Implementations/Localization/Ratings/br.csv index 5ec1eb2627..f6053c88c7 100644 --- a/Emby.Server.Implementations/Localization/Ratings/br.csv +++ b/Emby.Server.Implementations/Localization/Ratings/br.csv @@ -1,8 +1,14 @@ Livre,0 L,0 -ER,9 +AL,0 +ER,10 10,10 +A10,10 12,12 +A12,12 14,14 +A14,14 16,16 +A16,16 18,18 +A18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/ca.csv b/Emby.Server.Implementations/Localization/Ratings/ca.csv index 336ee28067..41dbda1349 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ca.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ca.csv @@ -6,8 +6,6 @@ TV-Y7,7 TV-Y7-FV,7 PG,9 TV-PG,9 -PG-13,13 -13+,13 TV-14,14 14A,14 16+,16 diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv index 619e948d88..ee58660900 100644 --- a/Emby.Server.Implementations/Localization/Ratings/es.csv +++ b/Emby.Server.Implementations/Localization/Ratings/es.csv @@ -1,7 +1,7 @@ A,0 A/fig,0 A/i,0 -A/fig/i,0 +A/i/fig,0 APTA,0 ERI,0 TP,0 diff --git a/Emby.Server.Implementations/Localization/Ratings/gb.csv b/Emby.Server.Implementations/Localization/Ratings/gb.csv index 75b1c20589..858b9a32dd 100644 --- a/Emby.Server.Implementations/Localization/Ratings/gb.csv +++ b/Emby.Server.Implementations/Localization/Ratings/gb.csv @@ -6,10 +6,11 @@ U,0 6+,6 7+,7 PG,8 -9+,9 +9,9 12,12 12+,12 12A,12 +12PG,12 Teen,13 13+,13 14+,14 diff --git a/Emby.Server.Implementations/Localization/Ratings/ie.csv b/Emby.Server.Implementations/Localization/Ratings/ie.csv index 6ef2e50128..d3c634fc93 100644 --- a/Emby.Server.Implementations/Localization/Ratings/ie.csv +++ b/Emby.Server.Implementations/Localization/Ratings/ie.csv @@ -4,6 +4,7 @@ PG,12 12A,12 12PG,12 15,15 +15PG,15 15A,15 16,16 18,18 diff --git a/Emby.Server.Implementations/Localization/Ratings/no.csv b/Emby.Server.Implementations/Localization/Ratings/no.csv index c8f8e93db7..6856a2dbbb 100644 --- a/Emby.Server.Implementations/Localization/Ratings/no.csv +++ b/Emby.Server.Implementations/Localization/Ratings/no.csv @@ -6,4 +6,5 @@ A,0 12,12 15,15 18,18 +C,18 Not approved,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/nz.csv b/Emby.Server.Implementations/Localization/Ratings/nz.csv index f617f0c39d..633da78fe1 100644 --- a/Emby.Server.Implementations/Localization/Ratings/nz.csv +++ b/Emby.Server.Implementations/Localization/Ratings/nz.csv @@ -10,6 +10,7 @@ R16,16 RP16,16 GA,18 R18,18 +RP18,18 MA,1000 R,1001 Objectionable,1001 diff --git a/Emby.Server.Implementations/Localization/Ratings/us.csv b/Emby.Server.Implementations/Localization/Ratings/us.csv index fc91edecd5..9aa5c00eb3 100644 --- a/Emby.Server.Implementations/Localization/Ratings/us.csv +++ b/Emby.Server.Implementations/Localization/Ratings/us.csv @@ -48,3 +48,5 @@ TV-MA-LS,17 TV-MA-LV,17 TV-MA-SV,17 TV-MA-LSV,17 +TV-X,18 +TV-AO,18 diff --git a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs index 228413777e..0fd0149904 100644 --- a/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs +++ b/Jellyfin.Api/Auth/CustomAuthenticationHandler.cs @@ -51,20 +51,21 @@ namespace Jellyfin.Api.Auth } var role = UserRoles.User; - if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator)) + if (authorizationInfo.IsApiKey + || (authorizationInfo.User?.HasPermission(PermissionKind.IsAdministrator) ?? false)) { role = UserRoles.Administrator; } 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(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.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture)) }; diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 1923393cbc..2601fa3be8 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -235,6 +235,11 @@ public static class StreamingHelpers 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) diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs index c472abdf06..2ce1e76dec 100644 --- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs @@ -71,7 +71,8 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListenerThe 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."); } diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs index f4031be361..a793c2f39e 100644 --- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs +++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs @@ -80,7 +80,8 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListenerThe 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."); } diff --git a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs index 33b2b67413..7600ecebff 100644 --- a/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs +++ b/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs @@ -18,9 +18,9 @@ public class BaseItemEntity 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; } diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs new file mode 100644 index 0000000000..6e8aba54bf --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.Designer.cs @@ -0,0 +1,1623 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DayOfWeek") + .HasColumnType("integer"); + + b.Property("EndHour") + .HasColumnType("double precision"); + + b.Property("StartHour") + .HasColumnType("double precision"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LogSeverity") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ParentItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("integer"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("Filename") + .HasColumnType("text"); + + b.Property("MimeType") + .HasColumnType("text"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Album") + .HasColumnType("text"); + + b.Property("AlbumArtists") + .HasColumnType("text"); + + b.Property("Artists") + .HasColumnType("text"); + + b.Property("Audio") + .HasColumnType("integer"); + + b.Property("ChannelId") + .HasColumnType("text"); + + b.Property("CleanName") + .HasColumnType("text"); + + b.Property("CommunityRating") + .HasColumnType("real"); + + b.Property("CriticRating") + .HasColumnType("real"); + + b.Property("CustomRating") + .HasColumnType("text"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastMediaAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastRefreshed") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastSaved") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EpisodeTitle") + .HasColumnType("text"); + + b.Property("ExternalId") + .HasColumnType("text"); + + b.Property("ExternalSeriesId") + .HasColumnType("text"); + + b.Property("ExternalServiceId") + .HasColumnType("text"); + + b.Property("ExtraIds") + .HasColumnType("text"); + + b.Property("ExtraType") + .HasColumnType("integer"); + + b.Property("ForcedSortName") + .HasColumnType("text"); + + b.Property("Genres") + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IndexNumber") + .HasColumnType("integer"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("IsInMixedFolder") + .HasColumnType("boolean"); + + b.Property("IsLocked") + .HasColumnType("boolean"); + + b.Property("IsMovie") + .HasColumnType("boolean"); + + b.Property("IsRepeat") + .HasColumnType("boolean"); + + b.Property("IsSeries") + .HasColumnType("boolean"); + + b.Property("IsVirtualItem") + .HasColumnType("boolean"); + + b.Property("LUFS") + .HasColumnType("real"); + + b.Property("MediaType") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizationGain") + .HasColumnType("real"); + + b.Property("OfficialRating") + .HasColumnType("text"); + + b.Property("OriginalTitle") + .HasColumnType("text"); + + b.Property("Overview") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ParentIndexNumber") + .HasColumnType("integer"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("text"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("text"); + + b.Property("PremiereDate") + .HasColumnType("timestamp with time zone"); + + b.Property("PresentationUniqueKey") + .HasColumnType("text"); + + b.Property("PrimaryVersionId") + .HasColumnType("text"); + + b.Property("ProductionLocations") + .HasColumnType("text"); + + b.Property("ProductionYear") + .HasColumnType("integer"); + + b.Property("RunTimeTicks") + .HasColumnType("bigint"); + + b.Property("SeasonId") + .HasColumnType("uuid"); + + b.Property("SeasonName") + .HasColumnType("text"); + + b.Property("SeriesId") + .HasColumnType("uuid"); + + b.Property("SeriesName") + .HasColumnType("text"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("text"); + + b.Property("ShowId") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("SortName") + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Studios") + .HasColumnType("text"); + + b.Property("Tagline") + .HasColumnType("text"); + + b.Property("Tags") + .HasColumnType("text"); + + b.Property("TopParentId") + .HasColumnType("uuid"); + + b.Property("TotalBitrate") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UnratedType") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Blurhash") + .HasColumnType("bytea"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("ImageType") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("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("Id") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ChapterIndex") + .HasColumnType("integer"); + + b.Property("ImageDateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("ImagePath") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("StartPositionTicks") + .HasColumnType("bigint"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChromecastVersion") + .HasColumnType("integer"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("boolean"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("ScrollDirection") + .HasColumnType("integer"); + + b.Property("ShowBackdrop") + .HasColumnType("boolean"); + + b.Property("ShowSidebar") + .HasColumnType("boolean"); + + b.Property("SkipBackwardLength") + .HasColumnType("integer"); + + b.Property("SkipForwardLength") + .HasColumnType("integer"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayPreferencesId") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IndexBy") + .HasColumnType("integer"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("RememberIndexing") + .HasColumnType("boolean"); + + b.Property("RememberSorting") + .HasColumnType("boolean"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("ViewType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("uuid"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTicks") + .HasColumnType("bigint"); + + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTicks") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("StreamIndex") + .HasColumnType("integer"); + + b.Property("AspectRatio") + .HasColumnType("text"); + + b.Property("AverageFrameRate") + .HasColumnType("real"); + + b.Property("BitDepth") + .HasColumnType("integer"); + + b.Property("BitRate") + .HasColumnType("integer"); + + b.Property("BlPresentFlag") + .HasColumnType("integer"); + + b.Property("ChannelLayout") + .HasColumnType("text"); + + b.Property("Channels") + .HasColumnType("integer"); + + b.Property("Codec") + .HasColumnType("text"); + + b.Property("CodecTag") + .HasColumnType("text"); + + b.Property("CodecTimeBase") + .HasColumnType("text"); + + b.Property("ColorPrimaries") + .HasColumnType("text"); + + b.Property("ColorSpace") + .HasColumnType("text"); + + b.Property("ColorTransfer") + .HasColumnType("text"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("integer"); + + b.Property("DvLevel") + .HasColumnType("integer"); + + b.Property("DvProfile") + .HasColumnType("integer"); + + b.Property("DvVersionMajor") + .HasColumnType("integer"); + + b.Property("DvVersionMinor") + .HasColumnType("integer"); + + b.Property("ElPresentFlag") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("IsAnamorphic") + .HasColumnType("boolean"); + + b.Property("IsAvc") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsForced") + .HasColumnType("boolean"); + + b.Property("IsHearingImpaired") + .HasColumnType("boolean"); + + b.Property("IsInterlaced") + .HasColumnType("boolean"); + + b.Property("KeyFrames") + .HasColumnType("text"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Level") + .HasColumnType("real"); + + b.Property("NalLengthSize") + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("PixelFormat") + .HasColumnType("text"); + + b.Property("Profile") + .HasColumnType("text"); + + b.Property("RealFrameRate") + .HasColumnType("real"); + + b.Property("RefFrames") + .HasColumnType("integer"); + + b.Property("Rotation") + .HasColumnType("integer"); + + b.Property("RpuPresentFlag") + .HasColumnType("integer"); + + b.Property("SampleRate") + .HasColumnType("integer"); + + b.Property("StreamType") + .HasColumnType("integer"); + + b.Property("TimeBase") + .HasColumnType("text"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("ItemId", "StreamIndex"); + + b.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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PersonType") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("PeopleId") + .HasColumnType("uuid"); + + b.Property("ListOrder") + .HasColumnType("integer"); + + b.Property("Role") + .HasColumnType("text"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Kind") + .HasColumnType("integer"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone"); + + b.Property("DateLastActivity") + .HasColumnType("timestamp with time zone"); + + b.Property("DateModified") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomName") + .HasColumnType("text"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("uuid"); + + b.Property("Width") + .HasColumnType("integer"); + + b.Property("Bandwidth") + .HasColumnType("integer"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Interval") + .HasColumnType("integer"); + + b.Property("ThumbnailCount") + .HasColumnType("integer"); + + b.Property("TileHeight") + .HasColumnType("integer"); + + b.Property("TileWidth") + .HasColumnType("integer"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DisplayCollectionsView") + .HasColumnType("boolean"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("boolean"); + + b.Property("EnableAutoLogin") + .HasColumnType("boolean"); + + b.Property("EnableLocalPassword") + .HasColumnType("boolean"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("boolean"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("boolean"); + + b.Property("HidePlayedInLatest") + .HasColumnType("boolean"); + + b.Property("InternalId") + .HasColumnType("bigint"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("integer"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("integer"); + + b.Property("MaxActiveSessions") + .HasColumnType("integer"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("integer"); + + b.Property("MustUpdatePassword") + .HasColumnType("boolean"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("character varying(65535)"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("boolean"); + + b.Property("RememberAudioSelections") + .HasColumnType("boolean"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("boolean"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("bigint"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("SubtitleMode") + .HasColumnType("integer"); + + b.Property("SyncPlayAccess") + .HasColumnType("integer"); + + b.Property("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("ItemId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CustomDataKey") + .HasColumnType("text"); + + b.Property("AudioStreamIndex") + .HasColumnType("integer"); + + b.Property("IsFavorite") + .HasColumnType("boolean"); + + b.Property("LastPlayedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Likes") + .HasColumnType("boolean"); + + b.Property("PlayCount") + .HasColumnType("integer"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("bigint"); + + b.Property("Played") + .HasColumnType("boolean"); + + b.Property("Rating") + .HasColumnType("double precision"); + + b.Property("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 + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/20250205183152_MakeStartEndDateNullable.cs new file mode 100644 index 0000000000..6ed1ddc5aa --- /dev/null +++ b/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 +{ + /// + public partial class MakeStartEndDateNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "StartDate", + table: "BaseItems", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "BaseItems", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "timestamp with time zone"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + 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( + 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); + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs index 2d0c783be5..d67fd89d84 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.PgSql/Migrations/JellyfinDbContextModelSnapshot.cs @@ -194,7 +194,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations b.Property("DateModified") .HasColumnType("timestamp with time zone"); - b.Property("EndDate") + b.Property("EndDate") .HasColumnType("timestamp with time zone"); b.Property("EpisodeTitle") @@ -332,7 +332,7 @@ namespace Jellyfin.Database.Providers.PgSql.Migrations b.Property("SortName") .HasColumnType("text"); - b.Property("StartDate") + b.Property("StartDate") .HasColumnType("timestamp with time zone"); b.Property("Studios") diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs new file mode 100644 index 0000000000..a329f1ef16 --- /dev/null +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.Designer.cs @@ -0,0 +1,1595 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DayOfWeek") + .HasColumnType("INTEGER"); + + b.Property("EndHour") + .HasColumnType("REAL"); + + b.Property("StartHour") + .HasColumnType("REAL"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AccessSchedules"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("ShortOverview") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DateCreated"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AncestorId", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ParentItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ParentItemId"); + + b.HasIndex("ParentItemId"); + + b.ToTable("AncestorIds"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.AttachmentStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("Filename") + .HasColumnType("TEXT"); + + b.Property("MimeType") + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "Index"); + + b.ToTable("AttachmentStreamInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("AlbumArtists") + .HasColumnType("TEXT"); + + b.Property("Artists") + .HasColumnType("TEXT"); + + b.Property("Audio") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("TEXT"); + + b.Property("CleanName") + .HasColumnType("TEXT"); + + b.Property("CommunityRating") + .HasColumnType("REAL"); + + b.Property("CriticRating") + .HasColumnType("REAL"); + + b.Property("CustomRating") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastMediaAdded") + .HasColumnType("TEXT"); + + b.Property("DateLastRefreshed") + .HasColumnType("TEXT"); + + b.Property("DateLastSaved") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("EpisodeTitle") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasColumnType("TEXT"); + + b.Property("ExternalSeriesId") + .HasColumnType("TEXT"); + + b.Property("ExternalServiceId") + .HasColumnType("TEXT"); + + b.Property("ExtraIds") + .HasColumnType("TEXT"); + + b.Property("ExtraType") + .HasColumnType("INTEGER"); + + b.Property("ForcedSortName") + .HasColumnType("TEXT"); + + b.Property("Genres") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IndexNumber") + .HasColumnType("INTEGER"); + + b.Property("InheritedParentalRatingValue") + .HasColumnType("INTEGER"); + + b.Property("IsFolder") + .HasColumnType("INTEGER"); + + b.Property("IsInMixedFolder") + .HasColumnType("INTEGER"); + + b.Property("IsLocked") + .HasColumnType("INTEGER"); + + b.Property("IsMovie") + .HasColumnType("INTEGER"); + + b.Property("IsRepeat") + .HasColumnType("INTEGER"); + + b.Property("IsSeries") + .HasColumnType("INTEGER"); + + b.Property("IsVirtualItem") + .HasColumnType("INTEGER"); + + b.Property("LUFS") + .HasColumnType("REAL"); + + b.Property("MediaType") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizationGain") + .HasColumnType("REAL"); + + b.Property("OfficialRating") + .HasColumnType("TEXT"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Overview") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("TEXT"); + + b.Property("ParentIndexNumber") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataCountryCode") + .HasColumnType("TEXT"); + + b.Property("PreferredMetadataLanguage") + .HasColumnType("TEXT"); + + b.Property("PremiereDate") + .HasColumnType("TEXT"); + + b.Property("PresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("PrimaryVersionId") + .HasColumnType("TEXT"); + + b.Property("ProductionLocations") + .HasColumnType("TEXT"); + + b.Property("ProductionYear") + .HasColumnType("INTEGER"); + + b.Property("RunTimeTicks") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("TEXT"); + + b.Property("SeasonName") + .HasColumnType("TEXT"); + + b.Property("SeriesId") + .HasColumnType("TEXT"); + + b.Property("SeriesName") + .HasColumnType("TEXT"); + + b.Property("SeriesPresentationUniqueKey") + .HasColumnType("TEXT"); + + b.Property("ShowId") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("SortName") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("Studios") + .HasColumnType("TEXT"); + + b.Property("Tagline") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasColumnType("TEXT"); + + b.Property("TopParentId") + .HasColumnType("TEXT"); + + b.Property("TotalBitrate") + .HasColumnType("INTEGER"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UnratedType") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Blurhash") + .HasColumnType("BLOB"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ImageType") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemMetadataField", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemMetadataFields"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemProvider", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemId", "ProviderId"); + + b.HasIndex("ProviderId", "ProviderValue", "ItemId"); + + b.ToTable("BaseItemProviders"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.BaseItemTrailerType", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("Id", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("BaseItemTrailerTypes"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Chapter", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ChapterIndex") + .HasColumnType("INTEGER"); + + b.Property("ImageDateModified") + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("StartPositionTicks") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "ChapterIndex"); + + b.ToTable("Chapters"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client", "Key") + .IsUnique(); + + b.ToTable("CustomItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChromecastVersion") + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DashboardTheme") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("EnableNextVideoInfoOverlay") + .HasColumnType("INTEGER"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("ScrollDirection") + .HasColumnType("INTEGER"); + + b.Property("ShowBackdrop") + .HasColumnType("INTEGER"); + + b.Property("ShowSidebar") + .HasColumnType("INTEGER"); + + b.Property("SkipBackwardLength") + .HasColumnType("INTEGER"); + + b.Property("SkipForwardLength") + .HasColumnType("INTEGER"); + + b.Property("TvHome") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "ItemId", "Client") + .IsUnique(); + + b.ToTable("DisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DisplayPreferencesId") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DisplayPreferencesId"); + + b.ToTable("HomeSection"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .HasColumnType("TEXT"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("ImageInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Client") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("IndexBy") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("RememberIndexing") + .HasColumnType("INTEGER"); + + b.Property("RememberSorting") + .HasColumnType("INTEGER"); + + b.Property("SortBy") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("ViewType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ItemDisplayPreferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValue", b => + { + b.Property("ItemValueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CleanValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId"); + + b.HasIndex("Type", "CleanValue") + .IsUnique(); + + b.ToTable("ItemValues"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.ItemValueMap", b => + { + b.Property("ItemValueId") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.HasKey("ItemValueId", "ItemId"); + + b.HasIndex("ItemId"); + + b.ToTable("ItemValuesMap"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaSegment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndTicks") + .HasColumnType("INTEGER"); + + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("SegmentProviderId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StartTicks") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSegments"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.MediaStreamInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("StreamIndex") + .HasColumnType("INTEGER"); + + b.Property("AspectRatio") + .HasColumnType("TEXT"); + + b.Property("AverageFrameRate") + .HasColumnType("REAL"); + + b.Property("BitDepth") + .HasColumnType("INTEGER"); + + b.Property("BitRate") + .HasColumnType("INTEGER"); + + b.Property("BlPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("ChannelLayout") + .HasColumnType("TEXT"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("CodecTag") + .HasColumnType("TEXT"); + + b.Property("CodecTimeBase") + .HasColumnType("TEXT"); + + b.Property("ColorPrimaries") + .HasColumnType("TEXT"); + + b.Property("ColorSpace") + .HasColumnType("TEXT"); + + b.Property("ColorTransfer") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("DvBlSignalCompatibilityId") + .HasColumnType("INTEGER"); + + b.Property("DvLevel") + .HasColumnType("INTEGER"); + + b.Property("DvProfile") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMajor") + .HasColumnType("INTEGER"); + + b.Property("DvVersionMinor") + .HasColumnType("INTEGER"); + + b.Property("ElPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAnamorphic") + .HasColumnType("INTEGER"); + + b.Property("IsAvc") + .HasColumnType("INTEGER"); + + b.Property("IsDefault") + .HasColumnType("INTEGER"); + + b.Property("IsExternal") + .HasColumnType("INTEGER"); + + b.Property("IsForced") + .HasColumnType("INTEGER"); + + b.Property("IsHearingImpaired") + .HasColumnType("INTEGER"); + + b.Property("IsInterlaced") + .HasColumnType("INTEGER"); + + b.Property("KeyFrames") + .HasColumnType("TEXT"); + + b.Property("Language") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("REAL"); + + b.Property("NalLengthSize") + .HasColumnType("TEXT"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.Property("PixelFormat") + .HasColumnType("TEXT"); + + b.Property("Profile") + .HasColumnType("TEXT"); + + b.Property("RealFrameRate") + .HasColumnType("REAL"); + + b.Property("RefFrames") + .HasColumnType("INTEGER"); + + b.Property("Rotation") + .HasColumnType("INTEGER"); + + b.Property("RpuPresentFlag") + .HasColumnType("INTEGER"); + + b.Property("SampleRate") + .HasColumnType("INTEGER"); + + b.Property("StreamType") + .HasColumnType("INTEGER"); + + b.Property("TimeBase") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "StreamIndex"); + + b.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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonType") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Peoples"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.PeopleBaseItemMap", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("PeopleId") + .HasColumnType("TEXT"); + + b.Property("ListOrder") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Permission_Permissions_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("Preference_Preferences_Guid") + .HasColumnType("TEXT"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Kind") + .IsUnique() + .HasFilter("[UserId] IS NOT NULL"); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AccessToken") + .IsUnique(); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("AppName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DateCreated") + .HasColumnType("TEXT"); + + b.Property("DateLastActivity") + .HasColumnType("TEXT"); + + b.Property("DateModified") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomName") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .IsUnique(); + + b.ToTable("DeviceOptions"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.Property("Bandwidth") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailCount") + .HasColumnType("INTEGER"); + + b.Property("TileHeight") + .HasColumnType("INTEGER"); + + b.Property("TileWidth") + .HasColumnType("INTEGER"); + + b.HasKey("ItemId", "Width"); + + b.ToTable("TrickplayInfos"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AudioLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("AuthenticationProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("CastReceiverId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("DisplayCollectionsView") + .HasColumnType("INTEGER"); + + b.Property("DisplayMissingEpisodes") + .HasColumnType("INTEGER"); + + b.Property("EnableAutoLogin") + .HasColumnType("INTEGER"); + + b.Property("EnableLocalPassword") + .HasColumnType("INTEGER"); + + b.Property("EnableNextEpisodeAutoPlay") + .HasColumnType("INTEGER"); + + b.Property("EnableUserPreferenceAccess") + .HasColumnType("INTEGER"); + + b.Property("HidePlayedInLatest") + .HasColumnType("INTEGER"); + + b.Property("InternalId") + .HasColumnType("INTEGER"); + + b.Property("InvalidLoginAttemptCount") + .HasColumnType("INTEGER"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.Property("LastLoginDate") + .HasColumnType("TEXT"); + + b.Property("LoginAttemptsBeforeLockout") + .HasColumnType("INTEGER"); + + b.Property("MaxActiveSessions") + .HasColumnType("INTEGER"); + + b.Property("MaxParentalAgeRating") + .HasColumnType("INTEGER"); + + b.Property("MustUpdatePassword") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasMaxLength(65535) + .HasColumnType("TEXT"); + + b.Property("PasswordResetProviderId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("PlayDefaultAudioTrack") + .HasColumnType("INTEGER"); + + b.Property("RememberAudioSelections") + .HasColumnType("INTEGER"); + + b.Property("RememberSubtitleSelections") + .HasColumnType("INTEGER"); + + b.Property("RemoteClientBitrateLimit") + .HasColumnType("INTEGER"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguagePreference") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("SubtitleMode") + .HasColumnType("INTEGER"); + + b.Property("SyncPlayAccess") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .UseCollation("NOCASE"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Jellyfin.Data.Entities.UserData", b => + { + b.Property("ItemId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("CustomDataKey") + .HasColumnType("TEXT"); + + b.Property("AudioStreamIndex") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .HasColumnType("INTEGER"); + + b.Property("LastPlayedDate") + .HasColumnType("TEXT"); + + b.Property("Likes") + .HasColumnType("INTEGER"); + + b.Property("PlayCount") + .HasColumnType("INTEGER"); + + b.Property("PlaybackPositionTicks") + .HasColumnType("INTEGER"); + + b.Property("Played") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("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 + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/20250204092455_MakeStartEndDateNullable.cs new file mode 100644 index 0000000000..2c60dd7a62 --- /dev/null +++ b/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 +{ + /// + public partial class MakeStartEndDateNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "StartDate", + table: "BaseItems", + type: "TEXT", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "EndDate", + table: "BaseItems", + type: "TEXT", + nullable: true, + oldClrType: typeof(DateTime), + oldType: "TEXT"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + 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( + 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); + } + } +} diff --git a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs index ddcc37195d..d24555c941 100644 --- a/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs +++ b/Jellyfin.Database/Jellyfin.Database.Providers.SqLite/Migrations/JellyfinDbModelSnapshot.cs @@ -185,7 +185,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("DateModified") .HasColumnType("TEXT"); - b.Property("EndDate") + b.Property("EndDate") .HasColumnType("TEXT"); b.Property("EpisodeTitle") @@ -323,7 +323,7 @@ namespace Jellyfin.Server.Implementations.Migrations b.Property("SortName") .HasColumnType("TEXT"); - b.Property("StartDate") + b.Property("StartDate") .HasColumnType("TEXT"); b.Property("Studios") diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 80604812c2..f9a9837f14 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -645,7 +645,7 @@ public sealed class BaseItemRepository // dto.MediaType = Enum.TryParse(entity.MediaType); 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 @@ -683,7 +683,7 @@ public sealed class BaseItemRepository entity.ParentId = !dto.ParentId.IsEmpty() ? dto.ParentId : null; entity.Path = GetPathToSave(dto.Path); - entity.EndDate = dto.EndDate.GetValueOrDefault(); + entity.EndDate = dto.EndDate; entity.CommunityRating = dto.CommunityRating; entity.CustomRating = dto.CustomRating; entity.IndexNumber = dto.IndexNumber; diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs index ae90404893..9e225393c4 100644 --- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs +++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs @@ -116,17 +116,15 @@ namespace Jellyfin.Server.Implementations.Security DeviceId = deviceId, Version = version, Token = token, - IsAuthenticated = false, - HasToken = false + IsAuthenticated = false }; - if (string.IsNullOrWhiteSpace(token)) + if (!authInfo.HasToken) { // Request doesn't contain a token. return authInfo; } - authInfo.HasToken = true; var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index dfc63b63f6..5d209b0afb 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -194,6 +194,14 @@ public class TrickplayManager : ITrickplayManager 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 var actualWidth = 2 * (width / 2); diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 53a6f07692..1c5d2f4e53 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -310,7 +310,6 @@ namespace MediaBrowser.Controller.MediaEncoding private bool IsSwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream is null - || !options.EnableTonemapping || GetVideoColorBitDepth(state) < 10 || !_mediaEncoder.SupportsFilter("tonemapx")) { @@ -2062,7 +2061,13 @@ namespace MediaBrowser.Controller.MediaEncoding // libx265 only accept level option in -x265-params. // level option may cause libx265 to fail. // 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) @@ -5691,7 +5696,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(doScaling) && !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); } @@ -7065,7 +7074,7 @@ namespace MediaBrowser.Controller.MediaEncoding { // 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 - shiftAudioCodecs.Add("dca"); + shiftAudioCodecs.Add("dts"); shiftAudioCodecs.Add("truehd"); } else diff --git a/MediaBrowser.Controller/Net/AuthorizationInfo.cs b/MediaBrowser.Controller/Net/AuthorizationInfo.cs index 2452b25ab1..e452f26494 100644 --- a/MediaBrowser.Controller/Net/AuthorizationInfo.cs +++ b/MediaBrowser.Controller/Net/AuthorizationInfo.cs @@ -1,6 +1,5 @@ -#nullable disable - using System; +using System.Diagnostics.CodeAnalysis; using Jellyfin.Data.Entities; namespace MediaBrowser.Controller.Net @@ -20,31 +19,31 @@ namespace MediaBrowser.Controller.Net /// Gets or sets the device identifier. /// /// The device identifier. - public string DeviceId { get; set; } + public string? DeviceId { get; set; } /// /// Gets or sets the device. /// /// The device. - public string Device { get; set; } + public string? Device { get; set; } /// /// Gets or sets the client. /// /// The client. - public string Client { get; set; } + public string? Client { get; set; } /// /// Gets or sets the version. /// /// The version. - public string Version { get; set; } + public string? Version { get; set; } /// /// Gets or sets the token. /// /// The token. - public string Token { get; set; } + public string? Token { get; set; } /// /// Gets or sets a value indicating whether the authorization is from an api key. @@ -54,7 +53,7 @@ namespace MediaBrowser.Controller.Net /// /// Gets or sets the user making the request. /// - public User User { get; set; } + public User? User { get; set; } /// /// Gets or sets a value indicating whether the token is authenticated. @@ -62,8 +61,9 @@ namespace MediaBrowser.Controller.Net public bool IsAuthenticated { get; set; } /// - /// Gets or sets a value indicating whether the request has a token. + /// Gets a value indicating whether the request has a token. /// - public bool HasToken { get; set; } + [MemberNotNullWhen(true, nameof(Token))] + public bool HasToken => !string.IsNullOrWhiteSpace(Token); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index a79d801fb5..d060b247da 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public class SubtitleEditParser : ISubtitleParser { private readonly ILogger _logger; - private readonly Dictionary _subtitleFormats; + private readonly Dictionary> _subtitleFormatTypes; /// /// Initializes a new instance of the class. @@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public SubtitleEditParser(ILogger 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(); } /// @@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitle = new Subtitle(); 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)); } - foreach (var subtitleFormat in subtitleFormats) + foreach (var subtitleFormatType in subtitleFormatTypesForExtension) { + var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!; _logger.LogDebug( "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", fileExtension, @@ -97,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// public bool SupportsFileExtension(string fileExtension) - => _subtitleFormats.ContainsKey(fileExtension); + => _subtitleFormatTypes.ContainsKey(fileExtension); - private List GetSubtitleFormats() + private Dictionary> GetSubtitleFormatTypes() { - var subtitleFormats = new List(); + var subtitleFormatTypes = new Dictionary>(StringComparer.OrdinalIgnoreCase); var assembly = typeof(SubtitleFormat).Assembly; foreach (var type in assembly.GetTypes()) @@ -113,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles 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) { @@ -123,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - return subtitleFormats; + return subtitleFormatTypes; } } } diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 8771e4f1c8..1ed493708a 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -30,7 +30,7 @@ namespace MediaBrowser.Model.Dlna private readonly ITranscoderSupport _transcoderSupport; private static readonly string[] _supportedHlsVideoCodecs = ["h264", "hevc", "vp9", "av1"]; 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"]; /// /// Initializes a new instance of the class. @@ -862,18 +862,37 @@ namespace MediaBrowser.Model.Dlna 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 .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))) .Select(i => 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 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 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 channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null; + 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); + + directAudioStreamSatisfied = directAudioStreamSatisfied && !playlistItem.TranscodeReasons.HasFlag(TranscodeReason.ContainerBitrateExceedsLimit); + + var directAudioStream = directAudioStreamSatisfied ? audioStreamWithSupportedCodec : 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 if (!profile.SupportsContainer(item.Container)) @@ -2221,6 +2259,20 @@ namespace MediaBrowser.Model.Dlna 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 string? audioCodec = audioStream?.Codec; if (!profile.SupportsAudioCodec(audioCodec)) @@ -2235,19 +2287,16 @@ namespace MediaBrowser.Model.Dlna { // Check container type, this should NOT be supported // 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) diff --git a/MediaBrowser.Model/Dlna/TranscodingProfile.cs b/MediaBrowser.Model/Dlna/TranscodingProfile.cs index 5a9fa22ae4..5797d42506 100644 --- a/MediaBrowser.Model/Dlna/TranscodingProfile.cs +++ b/MediaBrowser.Model/Dlna/TranscodingProfile.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Xml.Serialization; using Jellyfin.Data.Enums; @@ -6,6 +7,7 @@ namespace MediaBrowser.Model.Dlna; /// /// A class for transcoding profile information. +/// Note for client developers: Conditions defined in has higher priority and can override values defined here. /// public class TranscodingProfile { @@ -17,6 +19,33 @@ public class TranscodingProfile Conditions = []; } + /// + /// Initializes a new instance of the class copying the values from another instance. + /// + /// Another instance of to be copied. + 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; + } + /// /// Gets or sets the container. /// diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 9a676cb2e7..8f6aa2db37 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager var fileStreamOptions = AsyncFile.WriteOptions; fileStreamOptions.Mode = FileMode.Create; + fileStreamOptions.Options = FileOptions.WriteThrough; if (source.CanSeek) { fileStreamOptions.PreallocationSize = source.Length; diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index b4e3a860ea..05d43acdcb 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -183,7 +183,7 @@ namespace MediaBrowser.Providers.MediaInfo if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List(); - 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) { @@ -214,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo 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) @@ -318,7 +318,7 @@ namespace MediaBrowser.Providers.MediaInfo 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) { diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index f4aede463e..284415dce6 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -140,38 +140,39 @@ namespace MediaBrowser.Providers.TV private void RemoveObsoleteEpisodes(Series series) { - var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType().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() + .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 nonPhysicalEpisodes = []; + List physicalEpisodes = []; + foreach (var episode in seasonEpisodes) { - continue; + if (episode.IsVirtualItem || episode.IsMissingEpisode) + { + nonPhysicalEpisodes.Add(episode); + continue; + } + + physicalEpisodes.Add(episode); } - // Virtual episodes without an episode number are practically orphaned and should be deleted - if (!currentEpisode.IndexNumber.HasValue) + // Only consider non-physical episodes + foreach (var episode in nonPhysicalEpisodes) { - DeleteEpisode(currentEpisode); - continue; - } + // 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)); - for (var j = i + 1; j < numberOfEpisodes; j++) - { - var comparisonEpisode = episodes[j]; - // The inner loop is only for "physical" episodes - if (comparisonEpisode.IsVirtualItem - || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber - || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value)) + if (shouldKeep) { continue; } - DeleteEpisode(currentEpisode); - break; + DeleteEpisode(episode); } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 2d65188b63..137abff478 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -50,23 +50,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers { 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? 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(); - if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId)) + if (string.IsNullOrEmpty(imdbId) && contentId.StartsWith("tt", StringComparison.Ordinal)) { imdbId = contentId; } - else if (string.IsNullOrEmpty(tmdbId)) - { - tmdbId = contentId; - } item.TrySetProviderId(MetadataProvider.Imdb, imdbId); - item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId); break; } @@ -82,21 +79,13 @@ namespace MediaBrowser.XbmcMetadata.Parsers 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 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) { diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index 59abef919e..b0944515bf 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -1,3 +1,4 @@ +using System; using System.Globalization; using System.Xml; using Emby.Naming.TV; @@ -48,16 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Parsers { case "id": { - item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB")); + // Get ids from attributes 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; } diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index bc344d87e0..e85e369d91 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -115,7 +115,9 @@ namespace MediaBrowser.XbmcMetadata.Savers { if (!string.IsNullOrEmpty(movie.CollectionName)) { - writer.WriteElementString("set", movie.CollectionName); + writer.WriteStartElement("set"); + writer.WriteElementString("name", movie.CollectionName); + writer.WriteEndElement(); } } } diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs index c5aadc8901..2dac5598f0 100644 --- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -195,8 +195,10 @@ public class SkiaEncoder : IImageEncoder 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 - return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); + return BlurHashEncoder.Encode(xComp, yComp, fileStream, 128, 128); } private bool RequiresSpecialCharacterHack(string path) diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs index ff31b71233..b75cc0fb20 100644 --- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs +++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Entities.Libraries; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using Jellyfin.LiveTv.Configuration; @@ -39,6 +40,11 @@ public class GuideManager : IGuideManager private readonly IRecordingsManager _recordingsManager; private readonly LiveTvDtoService _tvDtoService; + /// + /// Amount of days images are pre-cached from external sources. + /// + public const int MaxCacheDays = 2; + /// /// Initializes a new instance of the class. /// @@ -204,14 +210,14 @@ public class GuideManager : IGuideManager progress.Report(15); numComplete = 0; - var programs = new List(); + var programs = new List(); var channels = new List(); 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) { cancellationToken.ThrowIfCancellationRequested(); @@ -237,22 +243,23 @@ public class GuideManager : IGuideManager DtoOptions = new DtoOptions(true) }).Cast().ToDictionary(i => i.Id); - var newPrograms = new List(); - var updatedPrograms = new List(); + var newPrograms = new List(); + var updatedPrograms = new List(); foreach (var program in channelPrograms) { var (programItem, isNew, isUpdated) = GetProgram(program, existingPrograms, currentChannel); + var id = programItem.Id; if (isNew) { - newPrograms.Add(programItem); + newPrograms.Add(id); } else if (isUpdated) { - updatedPrograms.Add(programItem); + updatedPrograms.Add(id); } - programs.Add(programItem.Id); + programs.Add(programItem); isMovie |= program.IsMovie; isSeries |= program.IsSeries; @@ -261,24 +268,30 @@ public class GuideManager : IGuideManager 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) { - _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) { + var updatedProgramDtos = programs.Where(b => updatedPrograms.Contains(b.Id)).ToList(); await _libraryManager.UpdateItemsAsync( - updatedPrograms, + updatedProgramDtos, currentChannel, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); - await PrecacheImages(updatedPrograms, maxCacheDate).ConfigureAwait(false); } + await PreCacheImages(programs, maxCacheDate).ConfigureAwait(false); + currentChannel.IsMovie = isMovie; currentChannel.IsNews = isNews; currentChannel.IsSports = isSports; @@ -313,7 +326,8 @@ public class GuideManager : IGuideManager } progress.Report(100); - return new Tuple, List>(channels, programs); + var programIds = programs.Select(p => p.Id).ToList(); + return new Tuple, List>(channels, programIds); } private void CleanDatabase(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress progress, CancellationToken cancellationToken) @@ -618,77 +632,17 @@ public class GuideManager : IGuideManager item.IndexNumber = info.EpisodeNumber; 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 (isNew) { - if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.ThumbImageUrl, - Type = ImageType.Thumb - }, - 0); - } - } + item.OnMetadataChanged(); - if (!item.HasImage(ImageType.Logo)) - { - if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) - { - item.SetImage( - new ItemImageInfo - { - Path = info.LogoImageUrl, - Type = ImageType.Logo - }, - 0); - } - } - - 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; - if (isNew) - { - } - else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) + if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag)) { isUpdated = true; } @@ -703,7 +657,7 @@ public class GuideManager : IGuideManager } } - if (isNew || isUpdated) + if (isUpdated) { item.OnMetadataChanged(); } @@ -711,7 +665,80 @@ public class GuideManager : IGuideManager return (item, isNew, isUpdated); } - private async Task PrecacheImages(IReadOnlyList 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 programs, DateTime maxCacheDate) { await Parallel.ForEachAsync( programs @@ -741,7 +768,7 @@ public class GuideManager : IGuideManager } catch (Exception ex) { - _logger.LogWarning(ex, "Unable to precache {Url}", imageInfo.Path); + _logger.LogWarning(ex, "Unable to pre-cache {Url}", imageInfo.Path); } } } diff --git a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index 790f60cf09..d6f15906ef 100644 --- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using AsyncKeyedLock; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.LiveTv.Guide; using Jellyfin.LiveTv.Listings.SchedulesDirectDtos; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; @@ -38,7 +39,7 @@ namespace Jellyfin.LiveTv.Listings private readonly IHttpClientFactory _httpClientFactory; private readonly AsyncNonKeyedLocker _tokenLock = new(1); - private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _tokens = new(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private DateTime _lastErrorResponse; private bool _disposed = false; @@ -86,7 +87,7 @@ namespace Jellyfin.LiveTv.Listings { _logger.LogWarning("SchedulesDirect token is empty, returning empty program list"); - return Enumerable.Empty(); + return []; } var dates = GetScheduleRequestDates(startDateUtc, endDateUtc); @@ -94,7 +95,7 @@ namespace Jellyfin.LiveTv.Listings _logger.LogInformation("Channel Station ID is: {ChannelID}", channelId); var requestList = new List() { - new RequestScheduleForChannelDto() + new() { StationId = channelId, Date = dates @@ -109,7 +110,7 @@ namespace Jellyfin.LiveTv.Listings var dailySchedules = await Request>(options, true, info, cancellationToken).ConfigureAwait(false); if (dailySchedules is null) { - return Array.Empty(); + return []; } _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(); programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); - var programDetails = await Request>(programRequestOptions, true, info, cancellationToken) - .ConfigureAwait(false); + var programDetails = await Request>(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); if (programDetails is null) { - return Array.Empty(); + return []; } var programDict = programDetails.ToDictionary(p => p.ProgramId, y => y); var programIdsWithImages = programDetails - .Where(p => p.HasImageArtwork).Select(p => p.ProgramId) + .Where(p => p.HasImageArtwork) + .Select(p => p.ProgramId) .ToList(); var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); @@ -138,17 +139,15 @@ namespace Jellyfin.LiveTv.Listings var programsInfo = new List(); 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)) { 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]); if (imageIndex > -1) @@ -456,7 +455,7 @@ namespace Jellyfin.LiveTv.Listings if (programIds.Count == 0) { - return Array.Empty(); + return []; } 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"); - return Array.Empty(); + return []; } } diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 3f71770b52..dd01e9533b 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -702,7 +702,7 @@ public class NetworkManager : INetworkManager, IDisposable return false; } } - else if (!_lanSubnets.Any(x => x.Contains(remoteIP))) + else if (!IsInLocalNetwork(remoteIP)) { // Remote not enabled. So everyone should be LAN. return false; @@ -997,7 +997,9 @@ public class NetworkManager : INetworkManager, IDisposable // Get interface matching override subnet 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 bindPreference = data.OverrideUri; @@ -1025,6 +1027,7 @@ public class NetworkManager : INetworkManager, IDisposable } _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference); + 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. bindAddress = externalInterfaces .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) .First(); @@ -1080,6 +1084,7 @@ public class NetworkManager : INetworkManager, IDisposable // If none exists, this will select the first internal interface if there is one. bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) .OrderByDescending(x => x.Subnet.Contains(source)) + .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) .FirstOrDefault(); diff --git a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs index 99f10583c1..6b3afc9357 100644 --- a/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/CustomAuthenticationHandlerTests.cs @@ -101,6 +101,7 @@ namespace Jellyfin.Api.Tests.Auth var authorizationInfo = SetupUser(); var authenticateResult = await _sut.AuthenticateAsync(); + Assert.NotNull(authorizationInfo.User); Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Name, authorizationInfo.User.Username)); } @@ -112,6 +113,7 @@ namespace Jellyfin.Api.Tests.Auth var authorizationInfo = SetupUser(isAdmin); var authenticateResult = await _sut.AuthenticateAsync(); + Assert.NotNull(authorizationInfo.User); var expectedRole = authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User; Assert.True(authenticateResult.Principal?.HasClaim(ClaimTypes.Role, expectedRole)); } @@ -133,7 +135,6 @@ namespace Jellyfin.Api.Tests.Auth authorizationInfo.User.AddDefaultPreferences(); authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin); authorizationInfo.IsApiKey = false; - authorizationInfo.HasToken = true; authorizationInfo.Token = "fake-token"; _jellyfinAuthServiceMock.Setup( diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 0afbf7e634..65f018ee3f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -84,7 +84,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization await localizationManager.LoadAll(); 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)); Assert.NotNull(tvma); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index b9833c2250..9c2655154d 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -257,5 +257,23 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Throws(() => _parser.Fetch(result, string.Empty, CancellationToken.None)); } + + [Fact] + public void Parsing_Fields_With_Escaped_Xml_Special_Characters_Success() + { + var result = new MetadataResult