// // Copyright (c) PlaceholderCompany. All rights reserved. // #pragma warning disable SA1202 namespace Jellyfin.Database.Implementations; using System; using System.Data.Common; using System.Diagnostics.CodeAnalysis; using System.Linq; 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; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; /// /// /// Initializes a new instance of the class. /// /// The database context options. /// Logger. /// The provider for the database engine specific operations. /// The locking behavior. public class JellyfinDbContext(DbContextOptions options, ILogger logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider, IEntityFrameworkCoreLockingBehavior entityFrameworkCoreLocking) : DbContext(options) { private static readonly Action SaveChangesError = LoggerMessage.Define( LogLevel.Error, new EventId(1, nameof(SaveChanges)), "Error trying to save changes."); /// /// Gets the containing the access schedules. /// public DbSet AccessSchedules => this.Set(); /// /// Gets the containing the activity logs. /// public DbSet ActivityLogs => this.Set(); /// /// Gets the containing the API keys. /// public DbSet ApiKeys => this.Set(); /// /// Gets the containing the devices. /// public DbSet Devices => this.Set(); /// /// Gets the containing the device options. /// public DbSet DeviceOptions => this.Set(); /// /// Gets the containing the display preferences. /// public DbSet DisplayPreferences => this.Set(); /// /// Gets the containing the image infos. /// public DbSet ImageInfos => this.Set(); /// /// Gets the containing the item display preferences. /// public DbSet ItemDisplayPreferences => this.Set(); /// /// Gets the containing the custom item display preferences. /// public DbSet CustomItemDisplayPreferences => this.Set(); /// /// Gets the containing the permissions. /// public DbSet Permissions => this.Set(); /// /// Gets the containing the preferences. /// public DbSet Preferences => this.Set(); /// /// Gets the containing the users. /// public DbSet Users => this.Set(); /// /// Gets the containing the trickplay metadata. /// public DbSet TrickplayInfos => this.Set(); /// /// Gets the containing the media segments. /// public DbSet MediaSegments => this.Set(); /// /// Gets the containing the user data. /// public DbSet UserData => this.Set(); /// /// Gets the containing the user data. /// public DbSet AncestorIds => this.Set(); /// /// Gets the containing the user data. /// public DbSet AttachmentStreamInfos => this.Set(); /// /// Gets the containing the user data. /// public DbSet BaseItems => this.Set(); /// /// Gets the containing the user data. /// public DbSet Chapters => this.Set(); /// /// Gets the . /// public DbSet ItemValues => this.Set(); /// /// Gets the . /// public DbSet ItemValuesMap => this.Set(); /// /// Gets the . /// public DbSet MediaStreamInfos => this.Set(); /// /// Gets the containing audio-specific stream details. /// public DbSet AudioStreamDetails => this.Set(); /// /// Gets the containing video-specific stream details. /// public DbSet VideoStreamDetails => this.Set(); // ── Read-only view projections (PostgreSQL only) ────────────────────── /// /// Gets the flat view over MediaStreamInfos joined with audio and video detail tables. /// Maps to library."MediaStreamInfos_v". /// public DbSet MediaStreamInfoViews => this.Set(); /// /// Gets the view of video items joined with their primary stream details. /// Maps to library."VideoItems_v". /// public DbSet VideoItemViews => this.Set(); /// /// Gets the view of per-user watch history with calculated progress. /// Maps to library."UserPlaybackHistory_v". /// public DbSet UserPlaybackHistoryViews => this.Set(); /// /// Gets the view of items with pivoted external provider IDs. /// Maps to library."ItemProviders_v". /// public DbSet ItemProviderViews => this.Set(); /// /// Gets the view of cast and crew entries linked to their items. /// Maps to library."ItemPeople_v". /// public DbSet ItemPersonViews => this.Set(); /// /// Gets the aggregate library statistics view, one row per item type. /// Maps to library."LibrarySummary_v". /// public DbSet LibrarySummaryViews => this.Set(); /// /// Gets the . /// public DbSet Peoples => this.Set(); /// /// Gets the . /// public DbSet PeopleBaseItemMap => this.Set(); /// /// Gets the containing the referenced Providers with ids. /// public DbSet BaseItemProviders => this.Set(); /// /// Gets the . /// public DbSet BaseItemImageInfos => this.Set(); /// /// Gets the . /// public DbSet BaseItemMetadataFields => this.Set(); /// /// Gets the . /// public DbSet BaseItemTrailerTypes => this.Set(); /// /// Gets the . /// public DbSet KeyframeData => this.Set(); /// /// Gets the containing persisted library options. /// public DbSet LibraryOptions => this.Set(); /// /// Gets the containing TV-specific extension data for BaseItems. /// public DbSet BaseItemTvExtras => this.Set(); /// /// Gets the containing LiveTV-specific extension data for BaseItems. /// public DbSet BaseItemLiveTvExtras => this.Set(); /// /// Gets the containing music-specific extension data for BaseItems. /// public DbSet BaseItemAudioExtras => this.Set(); /*public DbSet Artwork => Set(); public DbSet Books => Set(); public DbSet BookMetadata => Set(); public DbSet Chapters => Set(); public DbSet Collections => Set(); public DbSet CollectionItems => Set(); public DbSet Companies => Set(); public DbSet CompanyMetadata => Set(); public DbSet CustomItems => Set(); public DbSet CustomItemMetadata => Set(); public DbSet Episodes => Set(); public DbSet EpisodeMetadata => Set(); public DbSet Genres => Set(); public DbSet Groups => Set(); public DbSet Libraries => Set(); public DbSet LibraryItems => Set(); public DbSet LibraryRoot => Set(); public DbSet MediaFiles => Set(); public DbSet MediaFileStream => Set(); public DbSet Metadata => Set(); public DbSet MetadataProviders => Set(); public DbSet MetadataProviderIds => Set(); public DbSet Movies => Set(); public DbSet MovieMetadata => Set(); public DbSet MusicAlbums => Set(); public DbSet MusicAlbumMetadata => Set(); public DbSet People => Set(); public DbSet PersonRoles => Set(); public DbSet Photo => Set(); public DbSet PhotoMetadata => Set(); public DbSet ProviderMappings => Set(); public DbSet Ratings => Set(); /// /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to /// store review ratings, not age ratings. /// public DbSet RatingSources => Set(); public DbSet Releases => Set(); public DbSet Seasons => Set(); public DbSet SeasonMetadata => Set(); public DbSet Series => Set(); public DbSet SeriesMetadata => Set Tracks => Set(); public DbSet TrackMetadata => Set();*/ /// public override async Task SaveChangesAsync( bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { this.HandleConcurrencyToken(); try { var result = -1; await entityFrameworkCoreLocking.OnSaveChangesAsync(this, async () => { result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); return result; } catch (DbUpdateException ex) { // Check if it's a constraint violation (works across all database providers) var isConstraintViolation = ex.InnerException?.GetType().Name.Contains("Exception", StringComparison.Ordinal) == true && (ex.Message.Contains("constraint", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) || ex.Message.Contains("unique", StringComparison.OrdinalIgnoreCase)); if (isConstraintViolation) { // Log constraint violations as warnings (expected in some concurrent scenarios) #pragma warning disable CA1848 // Use LoggerMessage delegates - exception type is dynamic logger.LogWarning( ex, "Database constraint violation: Attempted to insert or update data that violates a database constraint. " + "This may indicate a concurrency issue or a bug in the update logic. " + "Inner exception: {InnerExceptionType}", ex.InnerException?.GetType().Name ?? "unknown"); #pragma warning restore CA1848 } else { // Other DbUpdateExceptions are true errors SaveChangesError(logger, ex); } throw; } catch (Exception e) { SaveChangesError(logger, e); throw; } } /// public override int SaveChanges(bool acceptAllChangesOnSuccess) // SaveChanges(bool) is beeing called by SaveChanges() with default to false. { this.HandleConcurrencyToken(); try { var result = -1; entityFrameworkCoreLocking.OnSaveChanges(this, () => { result = base.SaveChanges(acceptAllChangesOnSuccess); }); return result; } catch (Exception e) { SaveChangesError(logger, e); throw; } } private void HandleConcurrencyToken() { foreach (var saveEntity in this.ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .Select(entry => entry.Entity) .OfType()) { saveEntity.OnSavingChanges(); } } /// /// Gets the recommended batch size for bulk operations based on the number of items to process. /// Smaller batches during library scans reduce dead tuple generation in PostgreSQL. /// /// The total number of entities being processed. /// The recommended batch size for optimal performance. public int GetBatchSize(int entityCount) { // Smaller batches during media scans = fewer dead tuples // < 100 items: batch 10 // 100-1000: batch 50 // 1000-10000: batch 100 // > 10000: batch 200 return entityCount switch { < 100 => 10, < 1000 => 50, < 10000 => 100, _ => 200 }; } /// protected override void OnModelCreating(ModelBuilder modelBuilder) { jellyfinDatabaseProvider.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder); ArgumentNullException.ThrowIfNull(modelBuilder); // Configuration for each entity is in its own class inside 'ModelConfiguration'. modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly); } /// protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder); base.ConfigureConventions(configurationBuilder); } }