// // Copyright (c) PlaceholderCompany. All rights reserved. // #pragma warning disable SA1202 namespace Jellyfin.Database.Implementations; using System; using System.Data.Common; 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.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 . /// 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(); /*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) 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"); } 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(); } } /// 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); } }