Refactor database configuration and migrations for PostgreSQL support

- Updated default database configuration to use PostgreSQL instead of SQLite when no database.xml exists.
- Removed obsolete migration routines: MigrateUserDb, RemoveDuplicateExtras, ReseedFolderFlag.
- Introduced new read-only view projections for PostgreSQL: ItemPersonView, ItemProviderView, LibrarySummaryView, MediaStreamInfoView, UserPlaybackHistoryView, VideoItemView.
- Registered view mappings in PostgresDatabaseProvider to ensure proper handling of read-only views.
- Cleaned up unused SQLite cache size configuration in ConfigurationExtensions.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-30 18:47:48 -04:00
parent 9fc10226c9
commit e1f7a4bee9
15 changed files with 637 additions and 429 deletions
@@ -0,0 +1,29 @@
// <copyright file="ItemPersonView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
/// <summary>
/// Read-only projection of the library."ItemPeople_v" view.
/// All cast and crew entries linked to the items they appear in.
/// </summary>
public class ItemPersonView
{
public Guid? ItemId { get; set; }
public string? ItemType { get; set; }
public string? ItemName { get; set; }
public string? SeriesName { get; set; }
public int? ProductionYear { get; set; }
public Guid? PersonId { get; set; }
public string? PersonName { get; set; }
public string? PersonType { get; set; }
public string? Role { get; set; }
public int? SortOrder { get; set; }
public int? ListOrder { get; set; }
}
@@ -0,0 +1,34 @@
// <copyright file="ItemProviderView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
/// <summary>
/// Read-only projection of library."ItemProviders_v".
/// Items with external provider IDs pivoted into named columns.
/// </summary>
public class ItemProviderView
{
public Guid? ItemId { get; set; }
public string? Type { get; set; }
public string? Name { get; set; }
public string? OriginalTitle { get; set; }
public int? ProductionYear { get; set; }
public string? SeriesName { get; set; }
public int? EpisodeNumber { get; set; }
public int? SeasonNumber { get; set; }
public string? ImdbId { get; set; }
public string? TmdbId { get; set; }
public string? TvdbId { get; set; }
public string? TvRageId { get; set; }
public string? MusicBrainzAlbumId { get; set; }
public string? MusicBrainzArtistId { get; set; }
/// <summary>Gets or sets remaining providers as a JSON object string.</summary>
public string? OtherProviders { get; set; }
}
@@ -0,0 +1,33 @@
// <copyright file="LibrarySummaryView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
/// <summary>
/// Read-only projection of the library."LibrarySummary_v" view.
/// One row per item type with aggregate statistics across the whole library.
/// </summary>
public class LibrarySummaryView
{
public string? Type { get; set; }
public long? ItemCount { get; set; }
public long? WithYear { get; set; }
public int? OldestYear { get; set; }
public int? NewestYear { get; set; }
public decimal? AvgCommunityRating { get; set; }
public decimal? TotalRunTimeTicks { get; set; }
public decimal? TotalRuntimeHours { get; set; }
public decimal? TotalSizeBytes { get; set; }
public decimal? TotalSizeGB { get; set; }
public long? Count4K { get; set; }
public long? Count1080p { get; set; }
public long? Count720p { get; set; }
public long? CountSD { get; set; }
public long? CountDolbyVision { get; set; }
public long? CountHDR10Plus { get; set; }
public long? CountHDR10 { get; set; }
}
@@ -0,0 +1,67 @@
// <copyright file="MediaStreamInfoView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
/// <summary>
/// Read-only projection of library."MediaStreamInfos_v".
/// Flat view of all media streams with audio- and video-specific columns merged.
/// </summary>
public class MediaStreamInfoView
{
public Guid? ItemId { get; set; }
public int? StreamIndex { get; set; }
public int? StreamType { get; set; }
public string? Codec { get; set; }
public string? Language { get; set; }
public string? Profile { get; set; }
public string? Path { get; set; }
public int? BitRate { get; set; }
public bool? IsDefault { get; set; }
public bool? IsForced { get; set; }
public bool? IsExternal { get; set; }
public float? Level { get; set; }
public string? CodecTag { get; set; }
public string? Comment { get; set; }
public bool? IsAvc { get; set; }
public string? Title { get; set; }
public string? TimeBase { get; set; }
public string? CodecTimeBase { get; set; }
// audio-specific
public string? ChannelLayout { get; set; }
public int? Channels { get; set; }
public int? SampleRate { get; set; }
public bool? IsHearingImpaired { get; set; }
// video-specific
public string? AspectRatio { get; set; }
public bool? IsInterlaced { get; set; }
public int? Height { get; set; }
public int? Width { get; set; }
public float? AverageFrameRate { get; set; }
public float? RealFrameRate { get; set; }
public string? PixelFormat { get; set; }
public int? BitDepth { get; set; }
public bool? IsAnamorphic { get; set; }
public int? RefFrames { get; set; }
public string? NalLengthSize { get; set; }
public string? ColorPrimaries { get; set; }
public string? ColorSpace { get; set; }
public string? ColorTransfer { get; set; }
public int? DvVersionMajor { get; set; }
public int? DvVersionMinor { get; set; }
public int? DvProfile { get; set; }
public int? DvLevel { get; set; }
public int? RpuPresentFlag { get; set; }
public int? ElPresentFlag { get; set; }
public int? BlPresentFlag { get; set; }
public int? DvBlSignalCompatibilityId { get; set; }
public int? Rotation { get; set; }
public string? KeyFrames { get; set; }
public bool? Hdr10PlusPresentFlag { get; set; }
}
@@ -0,0 +1,39 @@
// <copyright file="UserPlaybackHistoryView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
/// <summary>
/// Read-only projection of library."UserPlaybackHistory_v".
/// Per-user watch history with calculated progress percentage.
/// </summary>
public class UserPlaybackHistoryView
{
public string? Username { get; set; }
public Guid? UserId { get; set; }
public string? ItemName { get; set; }
public string? ItemType { get; set; }
public string? SeriesName { get; set; }
public string? SeasonName { get; set; }
public int? EpisodeNumber { get; set; }
public int? SeasonNumber { get; set; }
public int? ProductionYear { get; set; }
public long? RunTimeTicks { get; set; }
public Guid? ItemId { get; set; }
public bool? Played { get; set; }
public int? PlayCount { get; set; }
public DateTimeOffset? LastPlayedDate { get; set; }
public long? PlaybackPositionTicks { get; set; }
/// <summary>Gets or sets playback progress 0100. NULL when no runtime.</summary>
public decimal? ProgressPct { get; set; }
public bool? IsFavorite { get; set; }
public double? Rating { get; set; }
public int? AudioStreamIndex { get; set; }
public int? SubtitleStreamIndex { get; set; }
}
@@ -0,0 +1,54 @@
// <copyright file="VideoItemView.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Implementations.Entities.Views;
#pragma warning disable CS1591
#pragma warning disable SA1600
using System;
/// <summary>
/// Read-only projection of library."VideoItems_v".
/// Every non-folder, non-virtual item with at least one video stream.
/// </summary>
public class VideoItemView
{
public Guid? ItemId { get; set; }
public string? Type { get; set; }
public string? Name { get; set; }
public string? OriginalTitle { get; set; }
public int? ProductionYear { get; set; }
public string? OfficialRating { get; set; }
public float? CommunityRating { get; set; }
public long? RunTimeTicks { get; set; }
public string? Path { get; set; }
public DateTimeOffset? DateCreated { get; set; }
public DateTimeOffset? DateModified { get; set; }
public string? SeriesName { get; set; }
public string? SeasonName { get; set; }
public int? EpisodeNumber { get; set; }
public int? SeasonNumber { get; set; }
public int? TotalBitrate { get; set; }
public long? Size { get; set; }
public string? VideoCodec { get; set; }
public string? VideoProfile { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public float? AverageFrameRate { get; set; }
public int? BitDepth { get; set; }
public string? PixelFormat { get; set; }
public string? ColorSpace { get; set; }
public string? ColorTransfer { get; set; }
public string? ColorPrimaries { get; set; }
public bool? IsInterlaced { get; set; }
public bool? IsAnamorphic { get; set; }
public string? AspectRatio { get; set; }
public bool? IsDolbyVision { get; set; }
public bool? IsHDR10Plus { get; set; }
/// <summary>Gets or sets derived HDR format: "Dolby Vision", "HDR10+", "HDR10", "HLG", or "SDR".</summary>
public string? HDRFormat { get; set; }
public long? AudioTrackCount { get; set; }
public long? SubtitleTrackCount { get; set; }
}
@@ -14,6 +14,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Implementations.Interfaces;
using Jellyfin.Database.Implementations.Locking;
using Microsoft.EntityFrameworkCore;
@@ -156,6 +157,44 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
/// </summary>
public DbSet<VideoStreamDetails> VideoStreamDetails => this.Set<VideoStreamDetails>();
// ── Read-only view projections (PostgreSQL only) ──────────────────────
/// <summary>
/// Gets the flat view over MediaStreamInfos joined with audio and video detail tables.
/// Maps to library."MediaStreamInfos_v".
/// </summary>
public DbSet<MediaStreamInfoView> MediaStreamInfoViews => this.Set<MediaStreamInfoView>();
/// <summary>
/// Gets the view of video items joined with their primary stream details.
/// Maps to library."VideoItems_v".
/// </summary>
public DbSet<VideoItemView> VideoItemViews => this.Set<VideoItemView>();
/// <summary>
/// Gets the view of per-user watch history with calculated progress.
/// Maps to library."UserPlaybackHistory_v".
/// </summary>
public DbSet<UserPlaybackHistoryView> UserPlaybackHistoryViews => this.Set<UserPlaybackHistoryView>();
/// <summary>
/// Gets the view of items with pivoted external provider IDs.
/// Maps to library."ItemProviders_v".
/// </summary>
public DbSet<ItemProviderView> ItemProviderViews => this.Set<ItemProviderView>();
/// <summary>
/// Gets the view of cast and crew entries linked to their items.
/// Maps to library."ItemPeople_v".
/// </summary>
public DbSet<ItemPersonView> ItemPersonViews => this.Set<ItemPersonView>();
/// <summary>
/// Gets the aggregate library statistics view, one row per item type.
/// Maps to library."LibrarySummary_v".
/// </summary>
public DbSet<LibrarySummaryView> LibrarySummaryViews => this.Set<LibrarySummaryView>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/>.
/// </summary>
@@ -19,6 +19,7 @@ using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Providers.Postgres.Exceptions;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
@@ -765,6 +766,9 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
// Assign entities to schemas based on their legacy database origin
AssignEntitiesToSchemas(modelBuilder);
// Register read-only view projections (PostgreSQL only)
RegisterViewMappings(modelBuilder);
}
/// <summary>
@@ -818,6 +822,39 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
modelBuilder.Entity<LibraryOptionsEntity>().HasIndex(e => e.DateModified);
}
/// <summary>
/// Registers read-only keyless view projections for PostgreSQL.
/// Each view lives in the <c>library</c> schema and is mapped with
/// <c>HasNoKey().ToView()</c> so EF Core never tries to create a table or migration for it.
/// </summary>
/// <param name="modelBuilder">The model builder.</param>
private static void RegisterViewMappings(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MediaStreamInfoView>()
.HasNoKey()
.ToView("MediaStreamInfos_v", Schemas.Library);
modelBuilder.Entity<VideoItemView>()
.HasNoKey()
.ToView("VideoItems_v", Schemas.Library);
modelBuilder.Entity<UserPlaybackHistoryView>()
.HasNoKey()
.ToView("UserPlaybackHistory_v", Schemas.Library);
modelBuilder.Entity<ItemProviderView>()
.HasNoKey()
.ToView("ItemProviders_v", Schemas.Library);
modelBuilder.Entity<ItemPersonView>()
.HasNoKey()
.ToView("ItemPeople_v", Schemas.Library);
modelBuilder.Entity<LibrarySummaryView>()
.HasNoKey()
.ToView("LibrarySummary_v", Schemas.Library);
}
/// <inheritdoc/>
public Task RunShutdownTask(CancellationToken cancellationToken)
{