aboutsummaryrefslogtreecommitdiff
path: root/src/Jellyfin.Database/Jellyfin.Database.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'src/Jellyfin.Database/Jellyfin.Database.Implementations')
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs19
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs25
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseLockingBehaviorTypes.cs22
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs62
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs123
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs49
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs186
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs58
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs24
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs24
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs44
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs80
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs150
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs66
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs44
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs54
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs76
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs113
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs38
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs64
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs34
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs80
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs57
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs64
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs54
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs59
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs34
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs49
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs50
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs141
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs60
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs55
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs72
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs50
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs53
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs63
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs70
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs30
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs56
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs89
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs80
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs59
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs73
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs67
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs35
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs46
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs70
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs34
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs42
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs104
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs44
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs68
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs68
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs37
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs56
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs107
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs35
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs74
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs340
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs96
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs57
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs57
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs22
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs39
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs127
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs67
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs72
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs22
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs112
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs83
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs16
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs16
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs15
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs15
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj39
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs29
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs326
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs166
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/IEntityFrameworkCoreLockingBehavior.cs32
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/NoLockBehavior.cs41
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs137
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs296
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs20
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs20
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs17
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs65
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs19
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs20
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs28
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs20
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs25
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs19
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs19
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs21
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs21
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs19
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs24
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs21
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs18
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs55
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs22
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/ProgressablePartitionReporting.cs55
-rw-r--r--src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs215
132 files changed, 7329 insertions, 0 deletions
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs
new file mode 100644
index 000000000..fcb8f41b3
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOption.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.DbConfiguration;
+
+/// <summary>
+/// The custom value option for custom database providers.
+/// </summary>
+public class CustomDatabaseOption
+{
+ /// <summary>
+ /// Gets or sets the key of the value.
+ /// </summary>
+ public required string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ public required string Value { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs
new file mode 100644
index 000000000..e2088704d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/CustomDatabaseOptions.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+
+namespace Jellyfin.Database.Implementations.DbConfiguration;
+
+/// <summary>
+/// Defines the options for a custom database connector.
+/// </summary>
+public class CustomDatabaseOptions
+{
+ /// <summary>
+ /// Gets or sets the Plugin name to search for database providers.
+ /// </summary>
+ public required string PluginName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plugin assembly to search for providers.
+ /// </summary>
+ public required string PluginAssembly { get; set; }
+
+ /// <summary>
+ /// Gets or sets the connection string for the custom database provider.
+ /// </summary>
+ public required string ConnectionString { get; set; }
+
+ /// <summary>
+ /// Gets or sets the list of extra options for the custom provider.
+ /// </summary>
+#pragma warning disable CA2227 // Collection properties should be read only
+ public Collection<CustomDatabaseOption> Options { get; set; } = [];
+#pragma warning restore CA2227 // Collection properties should be read only
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs
new file mode 100644
index 000000000..bc0cacf3c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseConfigurationOptions.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.DbConfiguration;
+
+/// <summary>
+/// Options to configure jellyfins managed database.
+/// </summary>
+public class DatabaseConfigurationOptions
+{
+ /// <summary>
+ /// Gets or Sets the type of database jellyfin should use.
+ /// </summary>
+ public required string DatabaseType { get; set; }
+
+ /// <summary>
+ /// Gets or sets the options required to use a custom database provider.
+ /// </summary>
+ public CustomDatabaseOptions? CustomProviderOptions { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the kind of locking behavior jellyfin should perform. Possible options are "NoLock", "Pessimistic", "Optimistic".
+ /// Defaults to "NoLock".
+ /// </summary>
+ public DatabaseLockingBehaviorTypes LockingBehavior { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseLockingBehaviorTypes.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseLockingBehaviorTypes.cs
new file mode 100644
index 000000000..3b2a55802
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/DbConfiguration/DatabaseLockingBehaviorTypes.cs
@@ -0,0 +1,22 @@
+namespace Jellyfin.Database.Implementations.DbConfiguration;
+
+/// <summary>
+/// Defines all possible methods for locking database access for concurrent queries.
+/// </summary>
+public enum DatabaseLockingBehaviorTypes
+{
+ /// <summary>
+ /// Defines that no explicit application level locking for reads and writes should be done and only provider specific locking should be relied on.
+ /// </summary>
+ NoLock = 0,
+
+ /// <summary>
+ /// Defines a behavior that always blocks all reads while any one write is done.
+ /// </summary>
+ Pessimistic = 1,
+
+ /// <summary>
+ /// Defines that all writes should be attempted and when fail should be retried.
+ /// </summary>
+ Optimistic = 2
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs
new file mode 100644
index 000000000..e23ac86aa
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AccessSchedule.cs
@@ -0,0 +1,62 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Xml.Serialization;
+using Jellyfin.Database.Implementations.Enums;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a user's access schedule.
+ /// </summary>
+ public class AccessSchedule
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
+ /// </summary>
+ /// <param name="dayOfWeek">The day of the week.</param>
+ /// <param name="startHour">The start hour.</param>
+ /// <param name="endHour">The end hour.</param>
+ /// <param name="userId">The associated user's id.</param>
+ public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
+ {
+ UserId = userId;
+ DayOfWeek = dayOfWeek;
+ StartHour = startHour;
+ EndHour = endHour;
+ }
+
+ /// <summary>
+ /// Gets the id of this instance.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [XmlIgnore]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the id of the associated user.
+ /// </summary>
+ [XmlIgnore]
+ public Guid UserId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the day of week.
+ /// </summary>
+ /// <value>The day of week.</value>
+ public DynamicDayOfWeek DayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start hour.
+ /// </summary>
+ /// <value>The start hour.</value>
+ public double StartHour { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end hour.
+ /// </summary>
+ /// <value>The end hour.</value>
+ public double EndHour { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs
new file mode 100644
index 000000000..bf623be7e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ActivityLog.cs
@@ -0,0 +1,123 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity referencing an activity log entry.
+ /// </summary>
+ public class ActivityLog : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ActivityLog"/> class.
+ /// Public constructor with required data.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ /// <param name="type">The type.</param>
+ /// <param name="userId">The user id.</param>
+ public ActivityLog(string name, string type, Guid userId)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ ArgumentException.ThrowIfNullOrEmpty(type);
+
+ Name = name;
+ Type = type;
+ UserId = userId;
+ DateCreated = DateTime.UtcNow;
+ LogSeverity = LogLevel.Information;
+ }
+
+ /// <summary>
+ /// Gets the identity of this instance.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 512.
+ /// </remarks>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the overview.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 512.
+ /// </remarks>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string? Overview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the short overview.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 512.
+ /// </remarks>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string? ShortOverview { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 256.
+ /// </remarks>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item id.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 256.
+ /// </remarks>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string? ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created. This should be in UTC.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the log severity. Default is <see cref="LogLevel.Trace"/>.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public LogLevel LogSeverity { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs
new file mode 100644
index 000000000..3d25ae4f4
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AncestorId.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Represents the relational information for an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AncestorId
+{
+ /// <summary>
+ /// Gets or Sets the AncestorId.
+ /// </summary>
+ public required Guid ParentItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the related BaseItem.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ParentItem.
+ /// </summary>
+ public required BaseItemEntity ParentItem { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Child item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs
new file mode 100644
index 000000000..2f27d9389
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/AttachmentStreamInfo.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Provides information about an Attachment to an <see cref="BaseItemEntity"/>.
+/// </summary>
+public class AttachmentStreamInfo
+{
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the index within the source file.
+ /// </summary>
+ public required int Index { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the codec of the attachment.
+ /// </summary>
+ public string? Codec { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the codec tag of the attachment.
+ /// </summary>
+ public string? CodecTag { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the comment of the attachment.
+ /// </summary>
+ public string? Comment { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the filename of the attachment.
+ /// </summary>
+ public string? Filename { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the attachments mimetype.
+ /// </summary>
+ public string? MimeType { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
new file mode 100644
index 000000000..a09a96317
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemEntity.cs
@@ -0,0 +1,186 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+public class BaseItemEntity
+{
+ public required Guid Id { get; set; }
+
+ public required string Type { get; set; }
+
+ public string? Data { get; set; }
+
+ public string? Path { get; set; }
+
+ public DateTime? StartDate { get; set; }
+
+ public DateTime? EndDate { get; set; }
+
+ public Guid? ChannelId { get; set; }
+
+ public bool IsMovie { get; set; }
+
+ public float? CommunityRating { get; set; }
+
+ public string? CustomRating { get; set; }
+
+ public int? IndexNumber { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ public string? Name { get; set; }
+
+ public string? OfficialRating { get; set; }
+
+ public string? MediaType { get; set; }
+
+ public string? Overview { get; set; }
+
+ public int? ParentIndexNumber { get; set; }
+
+ public DateTime? PremiereDate { get; set; }
+
+ public int? ProductionYear { get; set; }
+
+ public string? Genres { get; set; }
+
+ public string? SortName { get; set; }
+
+ public string? ForcedSortName { get; set; }
+
+ public long? RunTimeTicks { get; set; }
+
+ public DateTime? DateCreated { get; set; }
+
+ public DateTime? DateModified { get; set; }
+
+ public bool IsSeries { get; set; }
+
+ public string? EpisodeTitle { get; set; }
+
+ public bool IsRepeat { get; set; }
+
+ public string? PreferredMetadataLanguage { get; set; }
+
+ public string? PreferredMetadataCountryCode { get; set; }
+
+ public DateTime? DateLastRefreshed { get; set; }
+
+ public DateTime? DateLastSaved { get; set; }
+
+ public bool IsInMixedFolder { get; set; }
+
+ public string? Studios { get; set; }
+
+ public string? ExternalServiceId { get; set; }
+
+ public string? Tags { get; set; }
+
+ public bool IsFolder { get; set; }
+
+ public int? InheritedParentalRatingValue { get; set; }
+
+ public int? InheritedParentalRatingSubValue { get; set; }
+
+ public string? UnratedType { get; set; }
+
+ public float? CriticRating { get; set; }
+
+ public string? CleanName { get; set; }
+
+ public string? PresentationUniqueKey { get; set; }
+
+ public string? OriginalTitle { get; set; }
+
+ public string? PrimaryVersionId { get; set; }
+
+ public DateTime? DateLastMediaAdded { get; set; }
+
+ public string? Album { get; set; }
+
+ public float? LUFS { get; set; }
+
+ public float? NormalizationGain { get; set; }
+
+ public bool IsVirtualItem { get; set; }
+
+ public string? SeriesName { get; set; }
+
+ public string? SeasonName { get; set; }
+
+ public string? ExternalSeriesId { get; set; }
+
+ public string? Tagline { get; set; }
+
+ public string? ProductionLocations { get; set; }
+
+ public string? ExtraIds { get; set; }
+
+ public int? TotalBitrate { get; set; }
+
+ public BaseItemExtraType? ExtraType { get; set; }
+
+ public string? Artists { get; set; }
+
+ public string? AlbumArtists { get; set; }
+
+ public string? ExternalId { get; set; }
+
+ public string? SeriesPresentationUniqueKey { get; set; }
+
+ public string? ShowId { get; set; }
+
+ public string? OwnerId { get; set; }
+
+ public int? Width { get; set; }
+
+ public int? Height { get; set; }
+
+ public long? Size { get; set; }
+
+ public ProgramAudioEntity? Audio { get; set; }
+
+ public Guid? ParentId { get; set; }
+
+ public Guid? TopParentId { get; set; }
+
+ public Guid? SeasonId { get; set; }
+
+ public Guid? SeriesId { get; set; }
+
+ public ICollection<PeopleBaseItemMap>? Peoples { get; set; }
+
+ public ICollection<UserData>? UserData { get; set; }
+
+ public ICollection<ItemValueMap>? ItemValues { get; set; }
+
+ public ICollection<MediaStreamInfo>? MediaStreams { get; set; }
+
+ public ICollection<Chapter>? Chapters { get; set; }
+
+ public ICollection<BaseItemProvider>? Provider { get; set; }
+
+ public ICollection<AncestorId>? Parents { get; set; }
+
+ public ICollection<AncestorId>? Children { get; set; }
+
+ public ICollection<BaseItemMetadataField>? LockedFields { get; set; }
+
+ public ICollection<BaseItemTrailerType>? TrailerTypes { get; set; }
+
+ public ICollection<BaseItemImageInfo>? Images { get; set; }
+
+ // those are references to __LOCAL__ ids not DB ids ... TODO: Bring the whole folder structure into the DB
+ // public ICollection<BaseItemEntity>? SeriesEpisodes { get; set; }
+ // public BaseItemEntity? Series { get; set; }
+ // public BaseItemEntity? Season { get; set; }
+ // public BaseItemEntity? Parent { get; set; }
+ // public ICollection<BaseItemEntity>? DirectChildren { get; set; }
+ // public BaseItemEntity? TopParent { get; set; }
+ // public ICollection<BaseItemEntity>? AllChildren { get; set; }
+ // public ICollection<BaseItemEntity>? SeasonEpisodes { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs
new file mode 100644
index 000000000..46a59f790
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemExtraType.cs
@@ -0,0 +1,18 @@
+#pragma warning disable CS1591
+namespace Jellyfin.Database.Implementations.Entities;
+
+public enum BaseItemExtraType
+{
+ Unknown = 0,
+ Clip = 1,
+ Trailer = 2,
+ BehindTheScenes = 3,
+ DeletedScene = 4,
+ Interview = 5,
+ Scene = 6,
+ Sample = 7,
+ ThemeSong = 8,
+ ThemeVideo = 9,
+ Featurette = 10,
+ Short = 11
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs
new file mode 100644
index 000000000..cd14764e4
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemImageInfo.cs
@@ -0,0 +1,58 @@
+#pragma warning disable CA2227
+
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemImageInfo
+{
+ /// <summary>
+ /// Gets or Sets.
+ /// </summary>
+ public required Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the path to the original image.
+ /// </summary>
+ public required string Path { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the time the image was last modified.
+ /// </summary>
+ public DateTime? DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the imagetype.
+ /// </summary>
+ public ImageInfoImageType ImageType { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the width of the original image.
+ /// </summary>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the height of the original image.
+ /// </summary>
+ public int Height { get; set; }
+
+#pragma warning disable CA1819 // Properties should not return arrays
+ /// <summary>
+ /// Gets or Sets the blurhash.
+ /// </summary>
+ public byte[]? Blurhash { get; set; }
+#pragma warning restore CA1819
+
+ /// <summary>
+ /// Gets or Sets the reference id to the BaseItem.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced Item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs
new file mode 100644
index 000000000..e7dbc8e9f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemMetadataField.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Enum MetadataFields.
+/// </summary>
+public class BaseItemMetadataField
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumerable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs
new file mode 100644
index 000000000..73bb583e3
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemProvider.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Represents a Key-Value relation of an BaseItem's provider.
+/// </summary>
+public class BaseItemProvider
+{
+ /// <summary>
+ /// Gets or Sets the reference ItemId.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the reference BaseItem.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ProvidersId.
+ /// </summary>
+ public required string ProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Providers Value.
+ /// </summary>
+ public required string ProviderValue { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs
new file mode 100644
index 000000000..db329ad2a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/BaseItemTrailerType.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Enum TrailerTypes.
+/// </summary>
+public class BaseItemTrailerType
+{
+ /// <summary>
+ /// Gets or Sets Numerical ID of this enumerable.
+ /// </summary>
+ public required int Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs
new file mode 100644
index 000000000..f9b981328
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Chapter.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// The Chapter entity.
+/// </summary>
+public class Chapter
+{
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference id.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the <see cref="BaseItemEntity"/> reference.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the chapters index in Item.
+ /// </summary>
+ public required int ChapterIndex { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the position within the source file.
+ /// </summary>
+ public required long StartPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the common name.
+ /// </summary>
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the image path.
+ /// </summary>
+ public string? ImagePath { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the time the image was last modified.
+ /// </summary>
+ public DateTime? ImageDateModified { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs
new file mode 100644
index 000000000..b3d4b16bc
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/CustomItemDisplayPreferences.cs
@@ -0,0 +1,80 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity that represents a user's custom display preferences for a specific item.
+ /// </summary>
+ public class CustomItemDisplayPreferences
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client.</param>
+ /// <param name="key">The preference key.</param>
+ /// <param name="value">The preference value.</param>
+ public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string key, string? value)
+ {
+ UserId = userId;
+ ItemId = itemId;
+ Client = client;
+ Key = key;
+ Value = value;
+ }
+
+ /// <summary>
+ /// Gets the Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the user Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client string.
+ /// </summary>
+ /// <remarks>
+ /// Required. Max Length = 32.
+ /// </remarks>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preference key.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public string Key { get; set; }
+
+ /// <summary>
+ /// Gets or sets the preference value.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public string? Value { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs
new file mode 100644
index 000000000..ae6966e59
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/DisplayPreferences.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a user's display preferences.
+ /// </summary>
+ public class DisplayPreferences
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
+ /// </summary>
+ /// <param name="userId">The user's id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client string.</param>
+ public DisplayPreferences(Guid userId, Guid itemId, string client)
+ {
+ UserId = userId;
+ ItemId = itemId;
+ Client = client;
+ ShowSidebar = false;
+ ShowBackdrop = true;
+ SkipForwardLength = 30000;
+ SkipBackwardLength = 10000;
+ ScrollDirection = ScrollDirection.Horizontal;
+ ChromecastVersion = ChromecastVersion.Stable;
+
+ HomeSections = new HashSet<HomeSection>();
+ }
+
+ /// <summary>
+ /// Gets the Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the user Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client string.
+ /// </summary>
+ /// <remarks>
+ /// Required. Max Length = 32.
+ /// </remarks>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show the sidebar.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool ShowSidebar { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to show the backdrop.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool ShowBackdrop { get; set; }
+
+ /// <summary>
+ /// Gets or sets the scroll direction.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ScrollDirection ScrollDirection { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be indexed by.
+ /// </summary>
+ public IndexingKind? IndexBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of time to skip forwards, in milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int SkipForwardLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the length of time to skip backwards, in milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int SkipBackwardLength { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Chromecast Version.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ChromecastVersion ChromecastVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the next video info overlay should be shown.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableNextVideoInfoOverlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets the dashboard theme.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string? DashboardTheme { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tv home screen.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string? TvHome { get; set; }
+
+ /// <summary>
+ /// Gets the home sections.
+ /// </summary>
+ public virtual ICollection<HomeSection> HomeSections { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs
new file mode 100644
index 000000000..9dd248646
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Group.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a group.
+ /// </summary>
+ public class Group : IHasPermissions, IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Group"/> class.
+ /// </summary>
+ /// <param name="name">The name of the group.</param>
+ public Group(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ Name = name;
+ Id = Guid.NewGuid();
+
+ Permissions = new HashSet<Permission>();
+ Preferences = new HashSet<Preference>();
+ }
+
+ /// <summary>
+ /// Gets the id of this group.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ public Guid Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the group's name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Name { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the group's permissions.
+ /// </summary>
+ public virtual ICollection<Permission> Permissions { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the group's preferences.
+ /// </summary>
+ public virtual ICollection<Preference> Preferences { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs
new file mode 100644
index 000000000..584550ac5
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/HomeSection.cs
@@ -0,0 +1,44 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a section on the user's home page.
+ /// </summary>
+ public class HomeSection
+ {
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity. Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the Id of the associated display preferences.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int DisplayPreferencesId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the order.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Order { get; set; }
+
+ /// <summary>
+ /// Gets or sets the type.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public HomeSectionType Type { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs
new file mode 100644
index 000000000..9c0b36852
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfo.cs
@@ -0,0 +1,54 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing an image.
+ /// </summary>
+ public class ImageInfo
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ImageInfo"/> class.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ public ImageInfo(string path)
+ {
+ Path = path;
+ LastModified = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the user id.
+ /// </summary>
+ public Guid? UserId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the path of the image.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [MaxLength(512)]
+ [StringLength(512)]
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date last modified.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime LastModified { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs
new file mode 100644
index 000000000..6052a95bb
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ImageInfoImageType.cs
@@ -0,0 +1,76 @@
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Enum ImageType.
+/// </summary>
+public enum ImageInfoImageType
+{
+ /// <summary>
+ /// The primary.
+ /// </summary>
+ Primary = 0,
+
+ /// <summary>
+ /// The art.
+ /// </summary>
+ Art = 1,
+
+ /// <summary>
+ /// The backdrop.
+ /// </summary>
+ Backdrop = 2,
+
+ /// <summary>
+ /// The banner.
+ /// </summary>
+ Banner = 3,
+
+ /// <summary>
+ /// The logo.
+ /// </summary>
+ Logo = 4,
+
+ /// <summary>
+ /// The thumb.
+ /// </summary>
+ Thumb = 5,
+
+ /// <summary>
+ /// The disc.
+ /// </summary>
+ Disc = 6,
+
+ /// <summary>
+ /// The box.
+ /// </summary>
+ Box = 7,
+
+ /// <summary>
+ /// The screenshot.
+ /// </summary>
+ /// <remarks>
+ /// This enum value is obsolete.
+ /// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
+ /// </remarks>
+ Screenshot = 8,
+
+ /// <summary>
+ /// The menu.
+ /// </summary>
+ Menu = 9,
+
+ /// <summary>
+ /// The chapter image.
+ /// </summary>
+ Chapter = 10,
+
+ /// <summary>
+ /// The box rear.
+ /// </summary>
+ BoxRear = 11,
+
+ /// <summary>
+ /// The user profile image.
+ /// </summary>
+ Profile = 12
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs
new file mode 100644
index 000000000..677053114
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemDisplayPreferences.cs
@@ -0,0 +1,113 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity that represents a user's display preferences for a specific item.
+ /// </summary>
+ public class ItemDisplayPreferences
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="client">The client.</param>
+ public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
+ {
+ UserId = userId;
+ ItemId = itemId;
+ Client = client;
+
+ SortBy = "SortName";
+ SortOrder = SortOrder.Ascending;
+ RememberSorting = false;
+ RememberIndexing = false;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the user Id.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the client string.
+ /// </summary>
+ /// <remarks>
+ /// Required. Max Length = 32.
+ /// </remarks>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string Client { get; set; }
+
+ /// <summary>
+ /// Gets or sets the view type.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ViewType ViewType { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the indexing should be remembered.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberIndexing { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be indexed by.
+ /// </summary>
+ public IndexingKind? IndexBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the sorting type should be remembered.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberSorting { get; set; }
+
+ /// <summary>
+ /// Gets or sets what the view should be sorted by.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string SortBy { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort order.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public SortOrder SortOrder { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs
new file mode 100644
index 000000000..b5a31921d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValue.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Represents an ItemValue for a BaseItem.
+/// </summary>
+public class ItemValue
+{
+ /// <summary>
+ /// Gets or Sets the ItemValueId.
+ /// </summary>
+ public required Guid ItemValueId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Type.
+ /// </summary>
+ public required ItemValueType Type { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Value.
+ /// </summary>
+ public required string Value { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the sanitized Value.
+ /// </summary>
+ public required string CleanValue { get; set; }
+
+ /// <summary>
+ /// Gets or Sets all associated BaseItems.
+ /// </summary>
+#pragma warning disable CA2227 // Collection properties should be read only
+ public ICollection<ItemValueMap>? BaseItemsMap { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs
new file mode 100644
index 000000000..23f6e0f7b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueMap.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Mapping table for the ItemValue BaseItem relation.
+/// </summary>
+public class ItemValueMap
+{
+ /// <summary>
+ /// Gets or Sets the ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ItemValueId.
+ /// </summary>
+ public required Guid ItemValueId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the referenced <see cref="ItemValue"/>.
+ /// </summary>
+ public required ItemValue ItemValue { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs
new file mode 100644
index 000000000..9e2e11c00
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ItemValueType.cs
@@ -0,0 +1,38 @@
+#pragma warning disable CA1027 // Mark enums with FlagsAttribute
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Provides the Value types for an <see cref="ItemValue"/>.
+/// </summary>
+public enum ItemValueType
+{
+ /// <summary>
+ /// Artists.
+ /// </summary>
+ Artist = 0,
+
+ /// <summary>
+ /// Album.
+ /// </summary>
+ AlbumArtist = 1,
+
+ /// <summary>
+ /// Genre.
+ /// </summary>
+ Genre = 2,
+
+ /// <summary>
+ /// Studios.
+ /// </summary>
+ Studios = 3,
+
+ /// <summary>
+ /// Tags.
+ /// </summary>
+ Tags = 4,
+
+ /// <summary>
+ /// InheritedTags.
+ /// </summary>
+ InheritedTags = 6,
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs
new file mode 100644
index 000000000..c34110c4f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/KeyframeData.cs
@@ -0,0 +1,32 @@
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Keyframe information for a specific file.
+/// </summary>
+public class KeyframeData
+{
+ /// <summary>
+ /// Gets or Sets the ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the total duration of the stream in ticks.
+ /// </summary>
+ public long TotalDuration { get; set; }
+
+ /// <summary>
+ /// Gets or sets the keyframes in ticks.
+ /// </summary>
+ public ICollection<long>? KeyframeTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the item reference.
+ /// </summary>
+ public BaseItemEntity? Item { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs
new file mode 100644
index 000000000..f3083a96b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Artwork.cs
@@ -0,0 +1,64 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing artwork.
+ /// </summary>
+ public class Artwork : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Artwork"/> class.
+ /// </summary>
+ /// <param name="path">The path.</param>
+ /// <param name="kind">The kind of art.</param>
+ public Artwork(string path, ArtKind kind)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ Path = path;
+ Kind = kind;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the path.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the kind of artwork.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public ArtKind Kind { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs
new file mode 100644
index 000000000..b56c1f940
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Book.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a book.
+ /// </summary>
+ public class Book : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Book"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Book(Library library) : base(library)
+ {
+ BookMetadata = new HashSet<BookMetadata>();
+ Releases = new HashSet<Release>();
+ }
+
+ /// <summary>
+ /// Gets a collection containing the metadata for this book.
+ /// </summary>
+ public virtual ICollection<BookMetadata> BookMetadata { get; private set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs
new file mode 100644
index 000000000..a284d563a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/BookMetadata.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity containing metadata for a book.
+ /// </summary>
+ public class BookMetadata : ItemMetadata, IHasCompanies
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BookMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public BookMetadata(string title, string language) : base(title, language)
+ {
+ Publishers = new HashSet<Company>();
+ }
+
+ /// <summary>
+ /// Gets or sets the ISBN.
+ /// </summary>
+ public long? Isbn { get; set; }
+
+ /// <summary>
+ /// Gets a collection of the publishers for this book.
+ /// </summary>
+ public virtual ICollection<Company> Publishers { get; private set; }
+
+ /// <inheritdoc />
+ public ICollection<Company> Companies => Publishers;
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs
new file mode 100644
index 000000000..eac973060
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Chapter.cs
@@ -0,0 +1,80 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a chapter.
+ /// </summary>
+ public class Chapter : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Chapter"/> class.
+ /// </summary>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ /// <param name="startTime">The start time for this chapter.</param>
+ public Chapter(string language, long startTime)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(language);
+
+ Language = language;
+ StartTime = startTime;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ /// <remarks>
+ /// Required, Min length = 3, Max length = 3
+ /// ISO-639-3 3-character language codes.
+ /// </remarks>
+ [MinLength(3)]
+ [MaxLength(3)]
+ [StringLength(3)]
+ public string Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start time.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public long StartTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end time.
+ /// </summary>
+ public long? EndTime { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs
new file mode 100644
index 000000000..03b68317a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Collection.cs
@@ -0,0 +1,57 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a collection.
+ /// </summary>
+ public class Collection : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Collection"/> class.
+ /// </summary>
+ public Collection()
+ {
+ Items = new HashSet<CollectionItem>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Name { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing this collection's items.
+ /// </summary>
+ public virtual ICollection<CollectionItem> Items { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs
new file mode 100644
index 000000000..3777c705b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CollectionItem.cs
@@ -0,0 +1,64 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a collection item.
+ /// </summary>
+ public class CollectionItem : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionItem"/> class.
+ /// </summary>
+ /// <param name="libraryItem">The library item.</param>
+ public CollectionItem(LibraryItem libraryItem)
+ {
+ LibraryItem = libraryItem;
+ }
+
+ /// <summary>
+ /// Gets or sets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the library item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public virtual LibraryItem LibraryItem { get; set; }
+
+ /// <summary>
+ /// Gets or sets the next item in the collection.
+ /// </summary>
+ /// <remarks>
+ /// TODO check if this properly updated Dependent and has the proper principal relationship.
+ /// </remarks>
+ public virtual CollectionItem? Next { get; set; }
+
+ /// <summary>
+ /// Gets or sets the previous item in the collection.
+ /// </summary>
+ /// <remarks>
+ /// TODO check if this properly updated Dependent and has the proper principal relationship.
+ /// </remarks>
+ public virtual CollectionItem? Previous { get; set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs
new file mode 100644
index 000000000..c686751ab
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Company.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a company.
+ /// </summary>
+ public class Company : IHasCompanies, IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Company"/> class.
+ /// </summary>
+ public Company()
+ {
+ CompanyMetadata = new HashSet<CompanyMetadata>();
+ ChildCompanies = new HashSet<Company>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the metadata.
+ /// </summary>
+ public virtual ICollection<CompanyMetadata> CompanyMetadata { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing this company's child companies.
+ /// </summary>
+ public virtual ICollection<Company> ChildCompanies { get; private set; }
+
+ /// <inheritdoc />
+ public ICollection<Company> Companies => ChildCompanies;
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs
new file mode 100644
index 000000000..fdf1f274f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CompanyMetadata.cs
@@ -0,0 +1,59 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity holding metadata for a <see cref="Company"/>.
+ /// </summary>
+ public class CompanyMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CompanyMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public CompanyMetadata(string title, string language) : base(title, language)
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the description.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string? Description { get; set; }
+
+ /// <summary>
+ /// Gets or sets the headquarters.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string? Headquarters { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country code.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 2.
+ /// </remarks>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string? Country { get; set; }
+
+ /// <summary>
+ /// Gets or sets the homepage.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Homepage { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs
new file mode 100644
index 000000000..70e47d6bc
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItem.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a custom item.
+ /// </summary>
+ public class CustomItem : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CustomItem"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public CustomItem(Library library) : base(library)
+ {
+ CustomItemMetadata = new HashSet<CustomItemMetadata>();
+ Releases = new HashSet<Release>();
+ }
+
+ /// <summary>
+ /// Gets a collection containing the metadata for this item.
+ /// </summary>
+ public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; private set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs
new file mode 100644
index 000000000..660e535e3
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/CustomItemMetadata.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity containing metadata for a custom item.
+ /// </summary>
+ public class CustomItemMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public CustomItemMetadata(string title, string language) : base(title, language)
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs
new file mode 100644
index 000000000..7cb71f06d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Episode.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing an episode.
+ /// </summary>
+ public class Episode : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Episode"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Episode(Library library) : base(library)
+ {
+ Releases = new HashSet<Release>();
+ EpisodeMetadata = new HashSet<EpisodeMetadata>();
+ }
+
+ /// <summary>
+ /// Gets or sets the episode number.
+ /// </summary>
+ public int? EpisodeNumber { get; set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the metadata for this episode.
+ /// </summary>
+ public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs
new file mode 100644
index 000000000..b5c2c3c2a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/EpisodeMetadata.cs
@@ -0,0 +1,49 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity containing metadata for an <see cref="Episode"/>.
+ /// </summary>
+ public class EpisodeMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public EpisodeMetadata(string title, string language) : base(title, language)
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the outline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Outline { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plot.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string? Plot { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tagline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Tagline { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs
new file mode 100644
index 000000000..442dced2f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Genre.cs
@@ -0,0 +1,50 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a genre.
+ /// </summary>
+ public class Genre : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Genre"/> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public Genre(string name)
+ {
+ Name = name;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Indexed, Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Name { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs
new file mode 100644
index 000000000..e5cbab7e4
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/ItemMetadata.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An abstract class that holds metadata.
+ /// </summary>
+ public abstract class ItemMetadata : IHasArtwork, IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ protected ItemMetadata(string title, string language)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(title);
+ ArgumentException.ThrowIfNullOrEmpty(language);
+
+ Title = title;
+ Language = language;
+ DateAdded = DateTime.UtcNow;
+ DateModified = DateAdded;
+
+ PersonRoles = new HashSet<PersonRole>();
+ Genres = new HashSet<Genre>();
+ Artwork = new HashSet<Artwork>();
+ Ratings = new HashSet<Rating>();
+ Sources = new HashSet<MetadataProviderId>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the title.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Title { get; set; }
+
+ /// <summary>
+ /// Gets or sets the original title.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? OriginalTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the sort title.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? SortTitle { get; set; }
+
+ /// <summary>
+ /// Gets or sets the language.
+ /// </summary>
+ /// <remarks>
+ /// Required, Min length = 3, Max length = 3.
+ /// ISO-639-3 3-character language codes.
+ /// </remarks>
+ [MinLength(3)]
+ [MaxLength(3)]
+ [StringLength(3)]
+ public string Language { get; set; }
+
+ /// <summary>
+ /// Gets or sets the release date.
+ /// </summary>
+ public DateTimeOffset? ReleaseDate { get; set; }
+
+ /// <summary>
+ /// Gets the date added.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime DateAdded { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime DateModified { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the person roles for this item.
+ /// </summary>
+ public virtual ICollection<PersonRole> PersonRoles { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the genres for this item.
+ /// </summary>
+ public virtual ICollection<Genre> Genres { get; private set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Artwork> Artwork { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the ratings for this item.
+ /// </summary>
+ public virtual ICollection<Rating> Ratings { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the metadata sources for this item.
+ /// </summary>
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs
new file mode 100644
index 000000000..d1877ef43
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Library.cs
@@ -0,0 +1,60 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a library.
+ /// </summary>
+ public class Library : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Library"/> class.
+ /// </summary>
+ /// <param name="name">The name of the library.</param>
+ /// <param name="path">The path of the library.</param>
+ public Library(string name, string path)
+ {
+ Name = name;
+ Path = path;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 128.
+ /// </remarks>
+ [MaxLength(128)]
+ [StringLength(128)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the root path of the library.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public string Path { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs
new file mode 100644
index 000000000..4fccf6d73
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/LibraryItem.cs
@@ -0,0 +1,55 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a library item.
+ /// </summary>
+ public abstract class LibraryItem : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LibraryItem"/> class.
+ /// </summary>
+ /// <param name="library">The library of this item.</param>
+ protected LibraryItem(Library library)
+ {
+ DateAdded = DateTime.UtcNow;
+ Library = library;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the date this library item was added.
+ /// </summary>
+ public DateTime DateAdded { get; private set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the library of this item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public virtual Library Library { get; set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs
new file mode 100644
index 000000000..6e435579c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFile.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a file on disk.
+ /// </summary>
+ public class MediaFile : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaFile"/> class.
+ /// </summary>
+ /// <param name="path">The path relative to the LibraryRoot.</param>
+ /// <param name="kind">The file kind.</param>
+ public MediaFile(string path, MediaFileKind kind)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ Path = path;
+ Kind = kind;
+
+ MediaFileStreams = new HashSet<MediaFileStream>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the path relative to the library root.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Path { get; set; }
+
+ /// <summary>
+ /// Gets or sets the kind of media file.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public MediaFileKind Kind { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the streams in this file.
+ /// </summary>
+ public virtual ICollection<MediaFileStream> MediaFileStreams { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs
new file mode 100644
index 000000000..4552386fe
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MediaFileStream.cs
@@ -0,0 +1,50 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a stream in a media file.
+ /// </summary>
+ public class MediaFileStream : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MediaFileStream"/> class.
+ /// </summary>
+ /// <param name="streamNumber">The number of this stream.</param>
+ public MediaFileStream(int streamNumber)
+ {
+ StreamNumber = streamNumber;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the stream number.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int StreamNumber { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs
new file mode 100644
index 000000000..dc8f15350
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProvider.cs
@@ -0,0 +1,53 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a metadata provider.
+ /// </summary>
+ public class MetadataProvider : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MetadataProvider"/> class.
+ /// </summary>
+ /// <param name="name">The name of the metadata provider.</param>
+ public MetadataProvider(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ Name = name;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs
new file mode 100644
index 000000000..b7c9313a2
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MetadataProviderId.cs
@@ -0,0 +1,63 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a unique identifier for a metadata provider.
+ /// </summary>
+ public class MetadataProviderId : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
+ /// </summary>
+ /// <param name="providerId">The provider id.</param>
+ /// <param name="metadataProvider">The metadata provider.</param>
+ public MetadataProviderId(string providerId, MetadataProvider metadataProvider)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(providerId);
+
+ ProviderId = providerId;
+ MetadataProvider = metadataProvider;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the provider id.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string ProviderId { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the metadata provider.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public virtual MetadataProvider MetadataProvider { get; set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs
new file mode 100644
index 000000000..afc0e0f43
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Movie.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a movie.
+ /// </summary>
+ public class Movie : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Movie"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Movie(Library library) : base(library)
+ {
+ Releases = new HashSet<Release>();
+ MovieMetadata = new HashSet<MovieMetadata>();
+ }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the metadata for this movie.
+ /// </summary>
+ public virtual ICollection<MovieMetadata> MovieMetadata { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs
new file mode 100644
index 000000000..3d797d97e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MovieMetadata.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity holding the metadata for a movie.
+ /// </summary>
+ public class MovieMetadata : ItemMetadata, IHasCompanies
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MovieMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the movie.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public MovieMetadata(string title, string language) : base(title, language)
+ {
+ Studios = new HashSet<Company>();
+ }
+
+ /// <summary>
+ /// Gets or sets the outline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Outline { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tagline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Tagline { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plot.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string? Plot { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country code.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 2.
+ /// </remarks>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string? Country { get; set; }
+
+ /// <summary>
+ /// Gets the studios that produced this movie.
+ /// </summary>
+ public virtual ICollection<Company> Studios { get; private set; }
+
+ /// <inheritdoc />
+ public ICollection<Company> Companies => Studios;
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs
new file mode 100644
index 000000000..51f77ce0b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbum.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a music album.
+ /// </summary>
+ public class MusicAlbum : LibraryItem
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicAlbum"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public MusicAlbum(Library library) : base(library)
+ {
+ MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>();
+ Tracks = new HashSet<Track>();
+ }
+
+ /// <summary>
+ /// Gets a collection containing the album metadata.
+ /// </summary>
+ public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the tracks.
+ /// </summary>
+ public virtual ICollection<Track> Tracks { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs
new file mode 100644
index 000000000..bfb94c44d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/MusicAlbumMetadata.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity holding the metadata for a music album.
+ /// </summary>
+ public class MusicAlbumMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the album.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public MusicAlbumMetadata(string title, string language) : base(title, language)
+ {
+ Labels = new HashSet<Company>();
+ }
+
+ /// <summary>
+ /// Gets or sets the barcode.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string? Barcode { get; set; }
+
+ /// <summary>
+ /// Gets or sets the label number.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string? LabelNumber { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country code.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 2.
+ /// </remarks>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string? Country { get; set; }
+
+ /// <summary>
+ /// Gets a collection containing the labels.
+ /// </summary>
+ public virtual ICollection<Company> Labels { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs
new file mode 100644
index 000000000..25cdfdc2e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Person.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a person.
+ /// </summary>
+ public class Person : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Person"/> class.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ public Person(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ Name = name;
+ DateAdded = DateTime.UtcNow;
+ DateModified = DateAdded;
+
+ Sources = new HashSet<MetadataProviderId>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the source id.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string? SourceId { get; set; }
+
+ /// <summary>
+ /// Gets the date added.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime DateAdded { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public DateTime DateModified { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a list of metadata sources for this person.
+ /// </summary>
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs
new file mode 100644
index 000000000..e1c211390
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PersonRole.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a person's role in media.
+ /// </summary>
+ public class PersonRole : IHasArtwork, IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PersonRole"/> class.
+ /// </summary>
+ /// <param name="type">The role type.</param>
+ /// <param name="person">The person.</param>
+ public PersonRole(PersonRoleType type, Person person)
+ {
+ Type = type;
+ Person = person;
+ Artwork = new HashSet<Artwork>();
+ Sources = new HashSet<MetadataProviderId>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name of the person's role.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or sets the person's role type.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public PersonRoleType Type { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the person.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public virtual Person Person { get; set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Artwork> Artwork { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the metadata sources for this person role.
+ /// </summary>
+ public virtual ICollection<MetadataProviderId> Sources { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs
new file mode 100644
index 000000000..b113170e1
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Photo.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a photo.
+ /// </summary>
+ public class Photo : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Photo"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Photo(Library library) : base(library)
+ {
+ PhotoMetadata = new HashSet<PhotoMetadata>();
+ Releases = new HashSet<Release>();
+ }
+
+ /// <summary>
+ /// Gets a collection containing the photo metadata.
+ /// </summary>
+ public virtual ICollection<PhotoMetadata> PhotoMetadata { get; private set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs
new file mode 100644
index 000000000..6fae4a024
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/PhotoMetadata.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity that holds metadata for a photo.
+ /// </summary>
+ public class PhotoMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PhotoMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the photo.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public PhotoMetadata(string title, string language) : base(title, language)
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs
new file mode 100644
index 000000000..627575024
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Rating.cs
@@ -0,0 +1,59 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a rating for an entity.
+ /// </summary>
+ public class Rating : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Rating"/> class.
+ /// </summary>
+ /// <param name="value">The value.</param>
+ public Rating(double value)
+ {
+ Value = value;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the value.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public double Value { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of votes.
+ /// </summary>
+ public int? Votes { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the rating type.
+ /// If this is <c>null</c> it's the internal user rating.
+ /// </summary>
+ public virtual RatingSource? RatingType { get; set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs
new file mode 100644
index 000000000..832285599
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/RatingSource.cs
@@ -0,0 +1,73 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// This is the entity to store review ratings, not age ratings.
+ /// </summary>
+ public class RatingSource : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RatingSource"/> class.
+ /// </summary>
+ /// <param name="minimumValue">The minimum value.</param>
+ /// <param name="maximumValue">The maximum value.</param>
+ public RatingSource(double minimumValue, double maximumValue)
+ {
+ MinimumValue = minimumValue;
+ MaximumValue = maximumValue;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the minimum value.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public double MinimumValue { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum value.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public double MaximumValue { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the metadata source.
+ /// </summary>
+ public virtual MetadataProviderId? Source { get; set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs
new file mode 100644
index 000000000..db148338e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Release.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a release for a library item, eg. Director's cut vs. standard.
+ /// </summary>
+ public class Release : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Release"/> class.
+ /// </summary>
+ /// <param name="name">The name of this release.</param>
+ public Release(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ Name = name;
+
+ MediaFiles = new HashSet<MediaFile>();
+ Chapters = new HashSet<Chapter>();
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string Name { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the media files for this release.
+ /// </summary>
+ public virtual ICollection<MediaFile> MediaFiles { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the chapters for this release.
+ /// </summary>
+ public virtual ICollection<Chapter> Chapters { get; private set; }
+
+ /// <inheritdoc />
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs
new file mode 100644
index 000000000..dc9f695d9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Season.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a season.
+ /// </summary>
+ public class Season : LibraryItem
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Season"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Season(Library library) : base(library)
+ {
+ Episodes = new HashSet<Episode>();
+ SeasonMetadata = new HashSet<SeasonMetadata>();
+ }
+
+ /// <summary>
+ /// Gets or sets the season number.
+ /// </summary>
+ public int? SeasonNumber { get; set; }
+
+ /// <summary>
+ /// Gets the season metadata.
+ /// </summary>
+ public virtual ICollection<SeasonMetadata> SeasonMetadata { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the number of episodes.
+ /// </summary>
+ public virtual ICollection<Episode> Episodes { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs
new file mode 100644
index 000000000..af1e9fa2b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeasonMetadata.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity that holds metadata for seasons.
+ /// </summary>
+ public class SeasonMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeasonMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public SeasonMetadata(string title, string language) : base(title, language)
+ {
+ }
+
+ /// <summary>
+ /// Gets or sets the outline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Outline { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs
new file mode 100644
index 000000000..1e1633248
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Series.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a series.
+ /// </summary>
+ public class Series : LibraryItem
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Series"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Series(Library library) : base(library)
+ {
+ Seasons = new HashSet<Season>();
+ SeriesMetadata = new HashSet<SeriesMetadata>();
+ }
+
+ /// <summary>
+ /// Gets or sets the days of week.
+ /// </summary>
+ public DayOfWeek? AirsDayOfWeek { get; set; }
+
+ /// <summary>
+ /// Gets or sets the time the show airs, ignore the date portion.
+ /// </summary>
+ public DateTimeOffset? AirsTime { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date the series first aired.
+ /// </summary>
+ public DateTime? FirstAired { get; set; }
+
+ /// <summary>
+ /// Gets a collection containing the series metadata.
+ /// </summary>
+ public virtual ICollection<SeriesMetadata> SeriesMetadata { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the seasons.
+ /// </summary>
+ public virtual ICollection<Season> Seasons { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs
new file mode 100644
index 000000000..b1b2b10be
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/SeriesMetadata.cs
@@ -0,0 +1,70 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing series metadata.
+ /// </summary>
+ public class SeriesMetadata : ItemMetadata, IHasCompanies
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SeriesMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public SeriesMetadata(string title, string language) : base(title, language)
+ {
+ Networks = new HashSet<Company>();
+ }
+
+ /// <summary>
+ /// Gets or sets the outline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Outline { get; set; }
+
+ /// <summary>
+ /// Gets or sets the plot.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string? Plot { get; set; }
+
+ /// <summary>
+ /// Gets or sets the tagline.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 1024.
+ /// </remarks>
+ [MaxLength(1024)]
+ [StringLength(1024)]
+ public string? Tagline { get; set; }
+
+ /// <summary>
+ /// Gets or sets the country code.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 2.
+ /// </remarks>
+ [MaxLength(2)]
+ [StringLength(2)]
+ public string? Country { get; set; }
+
+ /// <summary>
+ /// Gets a collection containing the networks.
+ /// </summary>
+ public virtual ICollection<Company> Networks { get; private set; }
+
+ /// <inheritdoc />
+ public ICollection<Company> Companies => Networks;
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs
new file mode 100644
index 000000000..f0bd88963
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/Track.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity representing a track.
+ /// </summary>
+ public class Track : LibraryItem, IHasReleases
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Track"/> class.
+ /// </summary>
+ /// <param name="library">The library.</param>
+ public Track(Library library) : base(library)
+ {
+ Releases = new HashSet<Release>();
+ TrackMetadata = new HashSet<TrackMetadata>();
+ }
+
+ /// <summary>
+ /// Gets or sets the track number.
+ /// </summary>
+ public int? TrackNumber { get; set; }
+
+ /// <inheritdoc />
+ public virtual ICollection<Release> Releases { get; private set; }
+
+ /// <summary>
+ /// Gets a collection containing the track metadata.
+ /// </summary>
+ public virtual ICollection<TrackMetadata> TrackMetadata { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs
new file mode 100644
index 000000000..d9b4736a7
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Libraries/TrackMetadata.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Entities.Libraries
+{
+ /// <summary>
+ /// An entity holding metadata for a track.
+ /// </summary>
+ public class TrackMetadata : ItemMetadata
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TrackMetadata"/> class.
+ /// </summary>
+ /// <param name="title">The title or name of the object.</param>
+ /// <param name="language">ISO-639-3 3-character language codes.</param>
+ public TrackMetadata(string title, string language) : base(title, language)
+ {
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs
new file mode 100644
index 000000000..c34369d88
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaSegment.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class MediaSegment
+{
+ /// <summary>
+ /// Gets or sets the id of the media segment.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the Type of content this segment defines.
+ /// </summary>
+ public MediaSegmentType Type { get; set; }
+
+ /// <summary>
+ /// Gets or sets the end of the segment.
+ /// </summary>
+ public long EndTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the start of the segment.
+ /// </summary>
+ public long StartTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets Id of the media segment provider this entry originates from.
+ /// </summary>
+ public required string SegmentProviderId { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs
new file mode 100644
index 000000000..b80b764ba
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamInfo.cs
@@ -0,0 +1,104 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+public class MediaStreamInfo
+{
+ public required Guid ItemId { get; set; }
+
+ public required BaseItemEntity Item { get; set; }
+
+ public int StreamIndex { get; set; }
+
+ public required MediaStreamTypeEntity StreamType { get; set; }
+
+ public string? Codec { get; set; }
+
+ public string? Language { get; set; }
+
+ public string? ChannelLayout { get; set; }
+
+ public string? Profile { get; set; }
+
+ public string? AspectRatio { get; set; }
+
+ public string? Path { get; set; }
+
+ public bool? IsInterlaced { get; set; }
+
+ public int? BitRate { get; set; }
+
+ public int? Channels { get; set; }
+
+ public int? SampleRate { get; set; }
+
+ public bool IsDefault { get; set; }
+
+ public bool IsForced { get; set; }
+
+ public bool IsExternal { get; set; }
+
+ public int? Height { get; set; }
+
+ public int? Width { get; set; }
+
+ public float? AverageFrameRate { get; set; }
+
+ public float? RealFrameRate { get; set; }
+
+ public float? Level { get; set; }
+
+ public string? PixelFormat { get; set; }
+
+ public int? BitDepth { get; set; }
+
+ public bool? IsAnamorphic { get; set; }
+
+ public int? RefFrames { get; set; }
+
+ public string? CodecTag { get; set; }
+
+ public string? Comment { get; set; }
+
+ public string? NalLengthSize { get; set; }
+
+ public bool? IsAvc { get; set; }
+
+ public string? Title { get; set; }
+
+ public string? TimeBase { get; set; }
+
+ public string? CodecTimeBase { 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 bool? IsHearingImpaired { get; set; }
+
+ public int? Rotation { get; set; }
+
+ public string? KeyFrames { get; set; }
+
+ public bool? Hdr10PlusPresentFlag { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs
new file mode 100644
index 000000000..33dd81bdd
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/MediaStreamTypeEntity.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Enum MediaStreamType.
+/// </summary>
+public enum MediaStreamTypeEntity
+{
+ /// <summary>
+ /// The audio.
+ /// </summary>
+ Audio = 0,
+
+ /// <summary>
+ /// The video.
+ /// </summary>
+ Video = 1,
+
+ /// <summary>
+ /// The subtitle.
+ /// </summary>
+ Subtitle = 2,
+
+ /// <summary>
+ /// The embedded image.
+ /// </summary>
+ EmbeddedImage = 3,
+
+ /// <summary>
+ /// The data.
+ /// </summary>
+ Data = 4,
+
+ /// <summary>
+ /// The lyric.
+ /// </summary>
+ Lyric = 5
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs
new file mode 100644
index 000000000..20cf3e2d9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/People.cs
@@ -0,0 +1,32 @@
+#pragma warning disable CA2227 // Collection properties should be read only
+
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// People entity.
+/// </summary>
+public class People
+{
+ /// <summary>
+ /// Gets or Sets the PeopleId.
+ /// </summary>
+ public required Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Persons Name.
+ /// </summary>
+ public required string Name { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Type.
+ /// </summary>
+ public string? PersonType { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the mapping of People to BaseItems.
+ /// </summary>
+ public ICollection<PeopleBaseItemMap>? BaseItems { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs
new file mode 100644
index 000000000..c719a185c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/PeopleBaseItemMap.cs
@@ -0,0 +1,44 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Mapping table for People to BaseItems.
+/// </summary>
+public class PeopleBaseItemMap
+{
+ /// <summary>
+ /// Gets or Sets the SortOrder.
+ /// </summary>
+ public int? SortOrder { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the ListOrder.
+ /// </summary>
+ public int? ListOrder { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the Role name the associated actor played in the <see cref="BaseItemEntity"/>.
+ /// </summary>
+ public string? Role { get; set; }
+
+ /// <summary>
+ /// Gets or Sets The ItemId.
+ /// </summary>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets Reference Item.
+ /// </summary>
+ public required BaseItemEntity Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets The PeopleId.
+ /// </summary>
+ public required Guid PeopleId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets Reference People.
+ /// </summary>
+ public required People People { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs
new file mode 100644
index 000000000..84b86574c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Permission.cs
@@ -0,0 +1,68 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing whether the associated user has a specific permission.
+ /// </summary>
+ public class Permission : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Permission"/> class.
+ /// Public constructor with required data.
+ /// </summary>
+ /// <param name="kind">The permission kind.</param>
+ /// <param name="value">The value of this permission.</param>
+ public Permission(PermissionKind kind, bool value)
+ {
+ Kind = kind;
+ Value = value;
+ }
+
+ /// <summary>
+ /// Gets the id of this permission.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated user.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets the type of this permission.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public PermissionKind Kind { get; private set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the associated user has this permission.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool Value { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc/>
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs
new file mode 100644
index 000000000..c02ea7375
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Preference.cs
@@ -0,0 +1,68 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a preference attached to a user or group.
+ /// </summary>
+ public class Preference : IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Preference"/> class.
+ /// Public constructor with required data.
+ /// </summary>
+ /// <param name="kind">The preference kind.</param>
+ /// <param name="value">The value.</param>
+ public Preference(PreferenceKind kind, string value)
+ {
+ Kind = kind;
+ Value = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// Gets the id of this preference.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the id of the associated user.
+ /// </summary>
+ public Guid? UserId { get; set; }
+
+ /// <summary>
+ /// Gets the type of this preference.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public PreferenceKind Kind { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the value of this preference.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string Value { get; set; }
+
+ /// <inheritdoc/>
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <inheritdoc/>
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs
new file mode 100644
index 000000000..cb7255c19
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/ProgramAudioEntity.cs
@@ -0,0 +1,37 @@
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Lists types of Audio.
+/// </summary>
+public enum ProgramAudioEntity
+{
+ /// <summary>
+ /// Mono.
+ /// </summary>
+ Mono = 0,
+
+ /// <summary>
+ /// Stereo.
+ /// </summary>
+ Stereo = 1,
+
+ /// <summary>
+ /// Dolby.
+ /// </summary>
+ Dolby = 2,
+
+ /// <summary>
+ /// DolbyDigital.
+ /// </summary>
+ DolbyDigital = 3,
+
+ /// <summary>
+ /// Thx.
+ /// </summary>
+ Thx = 4,
+
+ /// <summary>
+ /// Atmos.
+ /// </summary>
+ Atmos = 5
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs
new file mode 100644
index 000000000..25a1d5ce9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/ApiKey.cs
@@ -0,0 +1,56 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+
+namespace Jellyfin.Database.Implementations.Entities.Security
+{
+ /// <summary>
+ /// An entity representing an API key.
+ /// </summary>
+ public class ApiKey
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ApiKey"/> class.
+ /// </summary>
+ /// <param name="name">The name.</param>
+ public ApiKey(string name)
+ {
+ Name = name;
+
+ AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ DateCreated = DateTime.UtcNow;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date of last activity.
+ /// </summary>
+ public DateTime DateLastActivity { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs
new file mode 100644
index 000000000..b0f9b2d56
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/Device.cs
@@ -0,0 +1,107 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Globalization;
+
+namespace Jellyfin.Database.Implementations.Entities.Security
+{
+ /// <summary>
+ /// An entity representing a device.
+ /// </summary>
+ public class Device
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Device"/> class.
+ /// </summary>
+ /// <param name="userId">The user id.</param>
+ /// <param name="appName">The app name.</param>
+ /// <param name="appVersion">The app version.</param>
+ /// <param name="deviceName">The device name.</param>
+ /// <param name="deviceId">The device id.</param>
+ public Device(Guid userId, string appName, string appVersion, string deviceName, string deviceId)
+ {
+ UserId = userId;
+ AppName = appName;
+ AppVersion = appVersion;
+ DeviceName = deviceName;
+ DeviceId = deviceId;
+
+ AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ DateCreated = DateTime.UtcNow;
+ DateModified = DateCreated;
+ DateLastActivity = DateCreated;
+
+ // Non-nullable for EF Core, as this is a required relationship.
+ User = null!;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the user id.
+ /// </summary>
+ public Guid UserId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the access token.
+ /// </summary>
+ public string AccessToken { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string AppName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the app version.
+ /// </summary>
+ [MaxLength(32)]
+ [StringLength(32)]
+ public string AppVersion { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device name.
+ /// </summary>
+ [MaxLength(64)]
+ [StringLength(64)]
+ public string DeviceName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the device id.
+ /// </summary>
+ [MaxLength(256)]
+ [StringLength(256)]
+ public string DeviceId { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this device is active.
+ /// </summary>
+ public bool IsActive { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date created.
+ /// </summary>
+ public DateTime DateCreated { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date modified.
+ /// </summary>
+ public DateTime DateModified { get; set; }
+
+ /// <summary>
+ /// Gets or sets the date of last activity.
+ /// </summary>
+ public DateTime DateLastActivity { get; set; }
+
+ /// <summary>
+ /// Gets the user.
+ /// </summary>
+ public User User { get; private set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs
new file mode 100644
index 000000000..8ac3e364c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Security/DeviceOptions.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Database.Implementations.Entities.Security
+{
+ /// <summary>
+ /// An entity representing custom options for a device.
+ /// </summary>
+ public class DeviceOptions
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeviceOptions"/> class.
+ /// </summary>
+ /// <param name="deviceId">The device id.</param>
+ public DeviceOptions(string deviceId)
+ {
+ DeviceId = deviceId;
+ }
+
+ /// <summary>
+ /// Gets the id.
+ /// </summary>
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public int Id { get; private set; }
+
+ /// <summary>
+ /// Gets the device id.
+ /// </summary>
+ public string DeviceId { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the custom name.
+ /// </summary>
+ public string? CustomName { get; set; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
new file mode 100644
index 000000000..39b449553
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/TrickplayInfo.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class TrickplayInfo
+{
+ /// <summary>
+ /// Gets or sets the id of the associated item.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or sets width of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Width { get; set; }
+
+ /// <summary>
+ /// Gets or sets height of an individual thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Height { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per row.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileWidth { get; set; }
+
+ /// <summary>
+ /// Gets or sets amount of thumbnails per column.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int TileHeight { get; set; }
+
+ /// <summary>
+ /// Gets or sets total amount of non-black thumbnails.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int ThumbnailCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Interval { get; set; }
+
+ /// <summary>
+ /// Gets or sets peak bandwidth usage in bits per second.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int Bandwidth { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
new file mode 100644
index 000000000..6c81fa729
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/User.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
+using Jellyfin.Database.Implementations.Enums;
+using Jellyfin.Database.Implementations.Interfaces;
+
+namespace Jellyfin.Database.Implementations.Entities
+{
+ /// <summary>
+ /// An entity representing a user.
+ /// </summary>
+ public class User : IHasPermissions, IHasConcurrencyToken
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="User"/> class.
+ /// Public constructor with required data.
+ /// </summary>
+ /// <param name="username">The username for the new user.</param>
+ /// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
+ /// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
+ public User(string username, string authenticationProviderId, string passwordResetProviderId)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(username);
+ ArgumentException.ThrowIfNullOrEmpty(authenticationProviderId);
+ ArgumentException.ThrowIfNullOrEmpty(passwordResetProviderId);
+
+ Username = username;
+ AuthenticationProviderId = authenticationProviderId;
+ PasswordResetProviderId = passwordResetProviderId;
+
+ AccessSchedules = new HashSet<AccessSchedule>();
+ DisplayPreferences = new HashSet<DisplayPreferences>();
+ ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
+ // Groups = new HashSet<Group>();
+ Permissions = new HashSet<Permission>();
+ Preferences = new HashSet<Preference>();
+ // ProviderMappings = new HashSet<ProviderMapping>();
+
+ // Set default values
+ Id = Guid.NewGuid();
+ InvalidLoginAttemptCount = 0;
+ EnableUserPreferenceAccess = true;
+ MustUpdatePassword = false;
+ DisplayMissingEpisodes = false;
+ DisplayCollectionsView = false;
+ HidePlayedInLatest = true;
+ RememberAudioSelections = true;
+ RememberSubtitleSelections = true;
+ EnableNextEpisodeAutoPlay = true;
+ EnableAutoLogin = false;
+ PlayDefaultAudioTrack = true;
+ SubtitleMode = SubtitlePlaybackMode.Default;
+ SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
+ }
+
+ /// <summary>
+ /// Gets or sets the Id of the user.
+ /// </summary>
+ /// <remarks>
+ /// Identity, Indexed, Required.
+ /// </remarks>
+ public Guid Id { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's name.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string Username { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's password, or <c>null</c> if none is set.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 65535.
+ /// </remarks>
+ [MaxLength(65535)]
+ [StringLength(65535)]
+ public string? Password { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user must update their password.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool MustUpdatePassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets the audio language preference.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string? AudioLanguagePreference { get; set; }
+
+ /// <summary>
+ /// Gets or sets the authentication provider id.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string AuthenticationProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the password reset provider id.
+ /// </summary>
+ /// <remarks>
+ /// Required, Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string PasswordResetProviderId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the invalid login attempt count.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public int InvalidLoginAttemptCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last activity date.
+ /// </summary>
+ public DateTime? LastActivityDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last login date.
+ /// </summary>
+ public DateTime? LastLoginDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the number of login attempts the user can make before they are locked out.
+ /// </summary>
+ public int? LoginAttemptsBeforeLockout { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum number of active sessions the user can have at once.
+ /// </summary>
+ public int MaxActiveSessions { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle mode.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public SubtitlePlaybackMode SubtitleMode { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the default audio track should be played.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool PlayDefaultAudioTrack { get; set; }
+
+ /// <summary>
+ /// Gets or sets the subtitle language preference.
+ /// </summary>
+ /// <remarks>
+ /// Max length = 255.
+ /// </remarks>
+ [MaxLength(255)]
+ [StringLength(255)]
+ public string? SubtitleLanguagePreference { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether missing episodes should be displayed.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool DisplayMissingEpisodes { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to display the collections view.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool DisplayCollectionsView { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user has a local password.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableLocalPassword { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the server should hide played content in "Latest".
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool HidePlayedInLatest { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to remember audio selections on played content.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberAudioSelections { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to remember subtitle selections on played content.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool RememberSubtitleSelections { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to enable auto-play for the next episode.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableNextEpisodeAutoPlay { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user should auto-login.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableAutoLogin { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the user can change their preferences.
+ /// </summary>
+ /// <remarks>
+ /// Required.
+ /// </remarks>
+ public bool EnableUserPreferenceAccess { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum parental rating score.
+ /// </summary>
+ public int? MaxParentalRatingScore { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum parental rating sub score.
+ /// </summary>
+ public int? MaxParentalRatingSubScore { get; set; }
+
+ /// <summary>
+ /// Gets or sets the remote client bitrate limit.
+ /// </summary>
+ public int? RemoteClientBitrateLimit { get; set; }
+
+ /// <summary>
+ /// Gets or sets the internal id.
+ /// This is a temporary stopgap for until the library db is migrated.
+ /// This corresponds to the value of the index of this user in the library db.
+ /// </summary>
+ public long InternalId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the user's profile image. Can be <c>null</c>.
+ /// </summary>
+ // [ForeignKey("UserId")]
+ public virtual ImageInfo? ProfileImage { get; set; }
+
+ /// <summary>
+ /// Gets the user's display preferences.
+ /// </summary>
+ public virtual ICollection<DisplayPreferences> DisplayPreferences { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the level of sync play permissions this user has.
+ /// </summary>
+ public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+
+ /// <summary>
+ /// Gets or sets the cast receiver id.
+ /// </summary>
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
+ /// <inheritdoc />
+ [ConcurrencyCheck]
+ public uint RowVersion { get; private set; }
+
+ /// <summary>
+ /// Gets the list of access schedules this user has.
+ /// </summary>
+ public virtual ICollection<AccessSchedule> AccessSchedules { get; private set; }
+
+ /// <summary>
+ /// Gets the list of item display preferences.
+ /// </summary>
+ public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; private set; }
+
+ /*
+ /// <summary>
+ /// Gets the list of groups this user is a member of.
+ /// </summary>
+ public virtual ICollection<Group> Groups { get; private set; }
+ */
+
+ /// <summary>
+ /// Gets the list of permissions this user has.
+ /// </summary>
+ [ForeignKey("Permission_Permissions_Guid")]
+ public virtual ICollection<Permission> Permissions { get; private set; }
+
+ /*
+ /// <summary>
+ /// Gets the list of provider mappings this user has.
+ /// </summary>
+ public virtual ICollection<ProviderMapping> ProviderMappings { get; private set; }
+ */
+
+ /// <summary>
+ /// Gets the list of preferences this user has.
+ /// </summary>
+ [ForeignKey("Preference_Preferences_Guid")]
+ public virtual ICollection<Preference> Preferences { get; private set; }
+
+ /// <inheritdoc/>
+ public void OnSavingChanges()
+ {
+ RowVersion++;
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs
new file mode 100644
index 000000000..3d8b01c2b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/UserData.cs
@@ -0,0 +1,96 @@
+using System;
+
+namespace Jellyfin.Database.Implementations.Entities;
+
+/// <summary>
+/// Provides <see cref="BaseItemEntity"/> and <see cref="User"/> related data.
+/// </summary>
+public class UserData
+{
+ /// <summary>
+ /// Gets or sets the custom data key.
+ /// </summary>
+ /// <value>The rating.</value>
+ public required string CustomDataKey { get; set; }
+
+ /// <summary>
+ /// Gets or sets the users 0-10 rating.
+ /// </summary>
+ /// <value>The rating.</value>
+ public double? Rating { get; set; }
+
+ /// <summary>
+ /// Gets or sets the playback position ticks.
+ /// </summary>
+ /// <value>The playback position ticks.</value>
+ public long PlaybackPositionTicks { get; set; }
+
+ /// <summary>
+ /// Gets or sets the play count.
+ /// </summary>
+ /// <value>The play count.</value>
+ public int PlayCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this instance is favorite.
+ /// </summary>
+ /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
+ public bool IsFavorite { get; set; }
+
+ /// <summary>
+ /// Gets or sets the last played date.
+ /// </summary>
+ /// <value>The last played date.</value>
+ public DateTime? LastPlayedDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether this <see cref="UserData" /> is played.
+ /// </summary>
+ /// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
+ public bool Played { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the audio stream.
+ /// </summary>
+ /// <value>The index of the audio stream.</value>
+ public int? AudioStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets the index of the subtitle stream.
+ /// </summary>
+ /// <value>The index of the subtitle stream.</value>
+ public int? SubtitleStreamIndex { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether the item is liked or not.
+ /// This should never be serialized.
+ /// </summary>
+ /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
+ public bool? Likes { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the date the referenced <see cref="Item"/> has been deleted.
+ /// </summary>
+ public DateTime? RetentionDate { get; set; }
+
+ /// <summary>
+ /// Gets or sets the key.
+ /// </summary>
+ /// <value>The key.</value>
+ public required Guid ItemId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the BaseItem.
+ /// </summary>
+ public required BaseItemEntity? Item { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the UserId.
+ /// </summary>
+ public required Guid UserId { get; set; }
+
+ /// <summary>
+ /// Gets or Sets the User.
+ /// </summary>
+ public required User? User { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs
new file mode 100644
index 000000000..218e97bcc
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ArtKind.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing types of art.
+/// </summary>
+public enum ArtKind
+{
+ /// <summary>
+ /// Another type of art, not covered by the other members.
+ /// </summary>
+ Other = 0,
+
+ /// <summary>
+ /// A poster.
+ /// </summary>
+ Poster = 1,
+
+ /// <summary>
+ /// A banner.
+ /// </summary>
+ Banner = 2,
+
+ /// <summary>
+ /// A thumbnail.
+ /// </summary>
+ Thumbnail = 3,
+
+ /// <summary>
+ /// A logo.
+ /// </summary>
+ Logo = 4
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs
new file mode 100644
index 000000000..123f2fe43
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ChromecastVersion.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the version of Chromecast to be used by clients.
+/// </summary>
+public enum ChromecastVersion
+{
+ /// <summary>
+ /// Stable Chromecast version.
+ /// </summary>
+ Stable = 0,
+
+ /// <summary>
+ /// Unstable Chromecast version.
+ /// </summary>
+ Unstable = 1
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs
new file mode 100644
index 000000000..69a9b5816
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/DynamicDayOfWeek.cs
@@ -0,0 +1,57 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum that represents a day of the week, weekdays, weekends, or all days.
+/// </summary>
+public enum DynamicDayOfWeek
+{
+ /// <summary>
+ /// Sunday.
+ /// </summary>
+ Sunday = 0,
+
+ /// <summary>
+ /// Monday.
+ /// </summary>
+ Monday = 1,
+
+ /// <summary>
+ /// Tuesday.
+ /// </summary>
+ Tuesday = 2,
+
+ /// <summary>
+ /// Wednesday.
+ /// </summary>
+ Wednesday = 3,
+
+ /// <summary>
+ /// Thursday.
+ /// </summary>
+ Thursday = 4,
+
+ /// <summary>
+ /// Friday.
+ /// </summary>
+ Friday = 5,
+
+ /// <summary>
+ /// Saturday.
+ /// </summary>
+ Saturday = 6,
+
+ /// <summary>
+ /// All days of the week.
+ /// </summary>
+ Everyday = 7,
+
+ /// <summary>
+ /// A week day, or Monday-Friday.
+ /// </summary>
+ Weekday = 8,
+
+ /// <summary>
+ /// Saturday and Sunday.
+ /// </summary>
+ Weekend = 9
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs
new file mode 100644
index 000000000..6ba57e74d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/HomeSectionType.cs
@@ -0,0 +1,57 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the different options for the home screen sections.
+/// </summary>
+public enum HomeSectionType
+{
+ /// <summary>
+ /// None.
+ /// </summary>
+ None = 0,
+
+ /// <summary>
+ /// My Media.
+ /// </summary>
+ SmallLibraryTiles = 1,
+
+ /// <summary>
+ /// My Media Small.
+ /// </summary>
+ LibraryButtons = 2,
+
+ /// <summary>
+ /// Active Recordings.
+ /// </summary>
+ ActiveRecordings = 3,
+
+ /// <summary>
+ /// Continue Watching.
+ /// </summary>
+ Resume = 4,
+
+ /// <summary>
+ /// Continue Listening.
+ /// </summary>
+ ResumeAudio = 5,
+
+ /// <summary>
+ /// Latest Media.
+ /// </summary>
+ LatestMedia = 6,
+
+ /// <summary>
+ /// Next Up.
+ /// </summary>
+ NextUp = 7,
+
+ /// <summary>
+ /// Live TV.
+ /// </summary>
+ LiveTv = 8,
+
+ /// <summary>
+ /// Continue Reading.
+ /// </summary>
+ ResumeBook = 9
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs
new file mode 100644
index 000000000..72ac1140c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/IndexingKind.cs
@@ -0,0 +1,22 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing a type of indexing in a user's display preferences.
+/// </summary>
+public enum IndexingKind
+{
+ /// <summary>
+ /// Index by the premiere date.
+ /// </summary>
+ PremiereDate = 0,
+
+ /// <summary>
+ /// Index by the production year.
+ /// </summary>
+ ProductionYear = 1,
+
+ /// <summary>
+ /// Index by the community rating.
+ /// </summary>
+ CommunityRating = 2
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs
new file mode 100644
index 000000000..8e6f677dc
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaFileKind.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the type of media file.
+/// </summary>
+public enum MediaFileKind
+{
+ /// <summary>
+ /// The main file.
+ /// </summary>
+ Main = 0,
+
+ /// <summary>
+ /// A sidecar file.
+ /// </summary>
+ Sidecar = 1,
+
+ /// <summary>
+ /// An additional part to the main file.
+ /// </summary>
+ AdditionalPart = 2,
+
+ /// <summary>
+ /// An alternative format to the main file.
+ /// </summary>
+ AlternativeFormat = 3,
+
+ /// <summary>
+ /// An additional stream for the main file.
+ /// </summary>
+ AdditionalStream = 4
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs
new file mode 100644
index 000000000..a6e8732ff
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/MediaSegmentType.cs
@@ -0,0 +1,39 @@
+using Jellyfin.Database.Implementations.Entities;
+
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// Defines the types of content an individual <see cref="MediaSegment"/> represents.
+/// </summary>
+public enum MediaSegmentType
+{
+ /// <summary>
+ /// Default media type or custom one.
+ /// </summary>
+ Unknown = 0,
+
+ /// <summary>
+ /// Commercial.
+ /// </summary>
+ Commercial = 1,
+
+ /// <summary>
+ /// Preview.
+ /// </summary>
+ Preview = 2,
+
+ /// <summary>
+ /// Recap.
+ /// </summary>
+ Recap = 3,
+
+ /// <summary>
+ /// Outro.
+ /// </summary>
+ Outro = 4,
+
+ /// <summary>
+ /// Intro.
+ /// </summary>
+ Intro = 5
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs
new file mode 100644
index 000000000..081863963
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PermissionKind.cs
@@ -0,0 +1,127 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// The types of user permissions.
+/// </summary>
+public enum PermissionKind
+{
+ /// <summary>
+ /// Whether the user is an administrator.
+ /// </summary>
+ IsAdministrator = 0,
+
+ /// <summary>
+ /// Whether the user is hidden.
+ /// </summary>
+ IsHidden = 1,
+
+ /// <summary>
+ /// Whether the user is disabled.
+ /// </summary>
+ IsDisabled = 2,
+
+ /// <summary>
+ /// Whether the user can control shared devices.
+ /// </summary>
+ EnableSharedDeviceControl = 3,
+
+ /// <summary>
+ /// Whether the user can access the server remotely.
+ /// </summary>
+ EnableRemoteAccess = 4,
+
+ /// <summary>
+ /// Whether the user can manage live tv.
+ /// </summary>
+ EnableLiveTvManagement = 5,
+
+ /// <summary>
+ /// Whether the user can access live tv.
+ /// </summary>
+ EnableLiveTvAccess = 6,
+
+ /// <summary>
+ /// Whether the user can play media.
+ /// </summary>
+ EnableMediaPlayback = 7,
+
+ /// <summary>
+ /// Whether the server should transcode audio for the user if requested.
+ /// </summary>
+ EnableAudioPlaybackTranscoding = 8,
+
+ /// <summary>
+ /// Whether the server should transcode video for the user if requested.
+ /// </summary>
+ EnableVideoPlaybackTranscoding = 9,
+
+ /// <summary>
+ /// Whether the user can delete content.
+ /// </summary>
+ EnableContentDeletion = 10,
+
+ /// <summary>
+ /// Whether the user can download content.
+ /// </summary>
+ EnableContentDownloading = 11,
+
+ /// <summary>
+ /// Whether to enable sync transcoding for the user.
+ /// </summary>
+ EnableSyncTranscoding = 12,
+
+ /// <summary>
+ /// Whether the user can do media conversion.
+ /// </summary>
+ EnableMediaConversion = 13,
+
+ /// <summary>
+ /// Whether the user has access to all devices.
+ /// </summary>
+ EnableAllDevices = 14,
+
+ /// <summary>
+ /// Whether the user has access to all channels.
+ /// </summary>
+ EnableAllChannels = 15,
+
+ /// <summary>
+ /// Whether the user has access to all folders.
+ /// </summary>
+ EnableAllFolders = 16,
+
+ /// <summary>
+ /// Whether to enable public sharing for the user.
+ /// </summary>
+ EnablePublicSharing = 17,
+
+ /// <summary>
+ /// Whether the user can remotely control other users.
+ /// </summary>
+ EnableRemoteControlOfOtherUsers = 18,
+
+ /// <summary>
+ /// Whether the user is permitted to do playback remuxing.
+ /// </summary>
+ EnablePlaybackRemuxing = 19,
+
+ /// <summary>
+ /// Whether the server should force transcoding on remote connections for the user.
+ /// </summary>
+ ForceRemoteSourceTranscoding = 20,
+
+ /// <summary>
+ /// Whether the user can create, modify and delete collections.
+ /// </summary>
+ EnableCollectionManagement = 21,
+
+ /// <summary>
+ /// Whether the user can edit subtitles.
+ /// </summary>
+ EnableSubtitleManagement = 22,
+
+ /// <summary>
+ /// Whether the user can edit lyrics.
+ /// </summary>
+ EnableLyricManagement = 23,
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs
new file mode 100644
index 000000000..5b913385e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PersonRoleType.cs
@@ -0,0 +1,67 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing a person's role in a specific media item.
+/// </summary>
+public enum PersonRoleType
+{
+ /// <summary>
+ /// Another role, not covered by the other types.
+ /// </summary>
+ Other = 0,
+
+ /// <summary>
+ /// The director of the media.
+ /// </summary>
+ Director = 1,
+
+ /// <summary>
+ /// An artist.
+ /// </summary>
+ Artist = 2,
+
+ /// <summary>
+ /// The original artist.
+ /// </summary>
+ OriginalArtist = 3,
+
+ /// <summary>
+ /// An actor.
+ /// </summary>
+ Actor = 4,
+
+ /// <summary>
+ /// A voice actor.
+ /// </summary>
+ VoiceActor = 5,
+
+ /// <summary>
+ /// A producer.
+ /// </summary>
+ Producer = 6,
+
+ /// <summary>
+ /// A remixer.
+ /// </summary>
+ Remixer = 7,
+
+ /// <summary>
+ /// A conductor.
+ /// </summary>
+ Conductor = 8,
+
+ /// <summary>
+ /// A composer.
+ /// </summary>
+ Composer = 9,
+
+ /// <summary>
+ /// An author.
+ /// </summary>
+ Author = 10,
+
+ /// <summary>
+ /// An editor.
+ /// </summary>
+ Editor = 11
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs
new file mode 100644
index 000000000..f70e3e2c2
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/PreferenceKind.cs
@@ -0,0 +1,72 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// The types of user preferences.
+/// </summary>
+public enum PreferenceKind
+{
+ /// <summary>
+ /// A list of blocked tags.
+ /// </summary>
+ BlockedTags = 0,
+
+ /// <summary>
+ /// A list of blocked channels.
+ /// </summary>
+ BlockedChannels = 1,
+
+ /// <summary>
+ /// A list of blocked media folders.
+ /// </summary>
+ BlockedMediaFolders = 2,
+
+ /// <summary>
+ /// A list of enabled devices.
+ /// </summary>
+ EnabledDevices = 3,
+
+ /// <summary>
+ /// A list of enabled channels.
+ /// </summary>
+ EnabledChannels = 4,
+
+ /// <summary>
+ /// A list of enabled folders.
+ /// </summary>
+ EnabledFolders = 5,
+
+ /// <summary>
+ /// A list of folders to allow content deletion from.
+ /// </summary>
+ EnableContentDeletionFromFolders = 6,
+
+ /// <summary>
+ /// A list of latest items to exclude.
+ /// </summary>
+ LatestItemExcludes = 7,
+
+ /// <summary>
+ /// A list of media to exclude.
+ /// </summary>
+ MyMediaExcludes = 8,
+
+ /// <summary>
+ /// A list of grouped folders.
+ /// </summary>
+ GroupedFolders = 9,
+
+ /// <summary>
+ /// A list of unrated items to block.
+ /// </summary>
+ BlockUnratedItems = 10,
+
+ /// <summary>
+ /// A list of ordered views.
+ /// </summary>
+ OrderedViews = 11,
+
+ /// <summary>
+ /// A list of allowed tags.
+ /// </summary>
+ AllowedTags = 12
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs
new file mode 100644
index 000000000..3ff3c45fa
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ScrollDirection.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the axis that should be scrolled.
+/// </summary>
+public enum ScrollDirection
+{
+ /// <summary>
+ /// Horizontal scrolling direction.
+ /// </summary>
+ Horizontal = 0,
+
+ /// <summary>
+ /// Vertical scrolling direction.
+ /// </summary>
+ Vertical = 1
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs
new file mode 100644
index 000000000..c865b75f1
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SortOrder.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the sorting order.
+/// </summary>
+public enum SortOrder
+{
+ /// <summary>
+ /// Sort in increasing order.
+ /// </summary>
+ Ascending = 0,
+
+ /// <summary>
+ /// Sort in decreasing order.
+ /// </summary>
+ Descending = 1
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs
new file mode 100644
index 000000000..c394c209b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SubtitlePlaybackMode.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing a subtitle playback mode.
+/// </summary>
+public enum SubtitlePlaybackMode
+{
+ /// <summary>
+ /// The default subtitle playback mode.
+ /// </summary>
+ Default = 0,
+
+ /// <summary>
+ /// Always show subtitles.
+ /// </summary>
+ Always = 1,
+
+ /// <summary>
+ /// Only show forced subtitles.
+ /// </summary>
+ OnlyForced = 2,
+
+ /// <summary>
+ /// Don't show subtitles.
+ /// </summary>
+ None = 3,
+
+ /// <summary>
+ /// Only show subtitles when the current audio stream is in a different language.
+ /// </summary>
+ Smart = 4
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs
new file mode 100644
index 000000000..311642e0d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/SyncPlayUserAccessType.cs
@@ -0,0 +1,22 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// Enum SyncPlayUserAccessType.
+/// </summary>
+public enum SyncPlayUserAccessType
+{
+ /// <summary>
+ /// User can create groups and join them.
+ /// </summary>
+ CreateAndJoinGroups = 0,
+
+ /// <summary>
+ /// User can only join already existing groups.
+ /// </summary>
+ JoinGroups = 1,
+
+ /// <summary>
+ /// SyncPlay is disabled for the user.
+ /// </summary>
+ None = 2
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs
new file mode 100644
index 000000000..b2bcbf2bb
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Enums/ViewType.cs
@@ -0,0 +1,112 @@
+namespace Jellyfin.Database.Implementations.Enums;
+
+/// <summary>
+/// An enum representing the type of view for a library or collection.
+/// </summary>
+public enum ViewType
+{
+ /// <summary>
+ /// Shows albums.
+ /// </summary>
+ Albums = 0,
+
+ /// <summary>
+ /// Shows album artists.
+ /// </summary>
+ AlbumArtists = 1,
+
+ /// <summary>
+ /// Shows artists.
+ /// </summary>
+ Artists = 2,
+
+ /// <summary>
+ /// Shows channels.
+ /// </summary>
+ Channels = 3,
+
+ /// <summary>
+ /// Shows collections.
+ /// </summary>
+ Collections = 4,
+
+ /// <summary>
+ /// Shows episodes.
+ /// </summary>
+ Episodes = 5,
+
+ /// <summary>
+ /// Shows favorites.
+ /// </summary>
+ Favorites = 6,
+
+ /// <summary>
+ /// Shows genres.
+ /// </summary>
+ Genres = 7,
+
+ /// <summary>
+ /// Shows guide.
+ /// </summary>
+ Guide = 8,
+
+ /// <summary>
+ /// Shows movies.
+ /// </summary>
+ Movies = 9,
+
+ /// <summary>
+ /// Shows networks.
+ /// </summary>
+ Networks = 10,
+
+ /// <summary>
+ /// Shows playlists.
+ /// </summary>
+ Playlists = 11,
+
+ /// <summary>
+ /// Shows programs.
+ /// </summary>
+ Programs = 12,
+
+ /// <summary>
+ /// Shows recordings.
+ /// </summary>
+ Recordings = 13,
+
+ /// <summary>
+ /// Shows schedule.
+ /// </summary>
+ Schedule = 14,
+
+ /// <summary>
+ /// Shows series.
+ /// </summary>
+ Series = 15,
+
+ /// <summary>
+ /// Shows shows.
+ /// </summary>
+ Shows = 16,
+
+ /// <summary>
+ /// Shows songs.
+ /// </summary>
+ Songs = 17,
+
+ /// <summary>
+ /// Shows songs.
+ /// </summary>
+ Suggestions = 18,
+
+ /// <summary>
+ /// Shows trailers.
+ /// </summary>
+ Trailers = 19,
+
+ /// <summary>
+ /// Shows upcoming.
+ /// </summary>
+ Upcoming = 20
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
new file mode 100644
index 000000000..27dbeaba6
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/IJellyfinDatabaseProvider.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations.DbConfiguration;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// Defines the type and extension points for multi database support.
+/// </summary>
+public interface IJellyfinDatabaseProvider
+{
+ /// <summary>
+ /// Gets or Sets the Database Factory when initialisaition is done.
+ /// </summary>
+ IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }
+
+ /// <summary>
+ /// Initialises jellyfins EFCore database access.
+ /// </summary>
+ /// <param name="options">The EFCore database options.</param>
+ /// <param name="databaseConfiguration">The Jellyfin database options.</param>
+ void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration);
+
+ /// <summary>
+ /// Will be invoked when EFCore wants to build its model.
+ /// </summary>
+ /// <param name="modelBuilder">The ModelBuilder from EFCore.</param>
+ void OnModelCreating(ModelBuilder modelBuilder);
+
+ /// <summary>
+ /// Will be invoked when EFCore wants to configure its model.
+ /// </summary>
+ /// <param name="configurationBuilder">The ModelConfigurationBuilder from EFCore.</param>
+ void ConfigureConventions(ModelConfigurationBuilder configurationBuilder);
+
+ /// <summary>
+ /// If supported this should run any periodic maintaince tasks.
+ /// </summary>
+ /// <param name="cancellationToken">The token to abort the operation.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task RunScheduledOptimisation(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// If supported this should perform any actions that are required on stopping the jellyfin server.
+ /// </summary>
+ /// <param name="cancellationToken">The token that will be used to abort the operation.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task RunShutdownTask(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Runs a full Database backup that can later be restored to.
+ /// </summary>
+ /// <param name="cancellationToken">A cancellation token.</param>
+ /// <returns>A key to identify the backup.</returns>
+ /// <exception cref="NotImplementedException">May throw an NotImplementException if this operation is not supported for this database.</exception>
+ Task<string> MigrationBackupFast(CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Restores a backup that has been previously created by <see cref="MigrationBackupFast(CancellationToken)"/>.
+ /// </summary>
+ /// <param name="key">The key to the backup from which the current database should be restored from.</param>
+ /// <param name="cancellationToken">A cancellation token.</param>
+ /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
+ Task RestoreBackupFast(string key, CancellationToken cancellationToken);
+
+ /// <summary>
+ /// Deletes a backup that has been previously created by <see cref="MigrationBackupFast(CancellationToken)"/>.
+ /// </summary>
+ /// <param name="key">The key to the backup which should be cleaned up.</param>
+ /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns>
+ Task DeleteBackup(string key);
+
+ /// <summary>
+ /// Removes all contents from the database.
+ /// </summary>
+ /// <param name="dbContext">The Database context.</param>
+ /// <param name="tableNames">The names of the tables to purge or null for all tables to be purged.</param>
+ /// <returns>A Task.</returns>
+ Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames);
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs
new file mode 100644
index 000000000..46007472a
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasArtwork.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities.Libraries;
+
+namespace Jellyfin.Database.Implementations.Interfaces
+{
+ /// <summary>
+ /// An interface abstracting an entity that has artwork.
+ /// </summary>
+ public interface IHasArtwork
+ {
+ /// <summary>
+ /// Gets a collection containing this entity's artwork.
+ /// </summary>
+ ICollection<Artwork> Artwork { get; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs
new file mode 100644
index 000000000..5cfefa456
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasCompanies.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities.Libraries;
+
+namespace Jellyfin.Database.Implementations.Interfaces
+{
+ /// <summary>
+ /// An abstraction representing an entity that has companies.
+ /// </summary>
+ public interface IHasCompanies
+ {
+ /// <summary>
+ /// Gets a collection containing this entity's companies.
+ /// </summary>
+ ICollection<Company> Companies { get; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs
new file mode 100644
index 000000000..196d2680d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasConcurrencyToken.cs
@@ -0,0 +1,17 @@
+namespace Jellyfin.Database.Implementations.Interfaces;
+
+/// <summary>
+/// An interface abstracting an entity that has a concurrency token.
+/// </summary>
+public interface IHasConcurrencyToken
+{
+ /// <summary>
+ /// Gets the version of this row. Acts as a concurrency token.
+ /// </summary>
+ uint RowVersion { get; }
+
+ /// <summary>
+ /// Called when saving changes to this entity.
+ /// </summary>
+ void OnSavingChanges();
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs
new file mode 100644
index 000000000..99b29e6d3
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasPermissions.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities;
+
+namespace Jellyfin.Database.Implementations.Interfaces;
+
+/// <summary>
+/// An abstraction representing an entity that has permissions.
+/// </summary>
+public interface IHasPermissions
+{
+ /// <summary>
+ /// Gets a collection containing this entity's permissions.
+ /// </summary>
+ ICollection<Permission> Permissions { get; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs
new file mode 100644
index 000000000..742a6a386
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Interfaces/IHasReleases.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using Jellyfin.Database.Implementations.Entities.Libraries;
+
+namespace Jellyfin.Database.Implementations.Interfaces;
+
+/// <summary>
+/// An abstraction representing an entity that has releases.
+/// </summary>
+public interface IHasReleases
+{
+ /// <summary>
+ /// Gets a collection containing this entity's releases.
+ /// </summary>
+ ICollection<Release> Releases { get; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
new file mode 100644
index 000000000..28c4972d2
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Jellyfin.Database.Implementations.csproj
@@ -0,0 +1,39 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="..\..\..\SharedVersion.cs" />
+ </ItemGroup>
+
+ <PropertyGroup>
+ <Authors>Jellyfin Contributors</Authors>
+ <PackageId>Jellyfin.Database.Implementations</PackageId>
+ <VersionPrefix>10.11.0</VersionPrefix>
+ <RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
+ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
+ </PropertyGroup>
+
+ <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
+ <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
+ <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Polly" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+</Project>
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs
new file mode 100644
index 000000000..778aca373
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDatabaseProviderKeyAttribute.cs
@@ -0,0 +1,29 @@
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// Defines the key of the database provider.
+/// </summary>
+[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
+public sealed class JellyfinDatabaseProviderKeyAttribute : System.Attribute
+{
+ // See the attribute guidelines at
+ // http://go.microsoft.com/fwlink/?LinkId=85236
+ private readonly string _databaseProviderKey;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="JellyfinDatabaseProviderKeyAttribute"/> class.
+ /// </summary>
+ /// <param name="databaseProviderKey">The key on which to identify the annotated provider.</param>
+ public JellyfinDatabaseProviderKeyAttribute(string databaseProviderKey)
+ {
+ _databaseProviderKey = databaseProviderKey;
+ }
+
+ /// <summary>
+ /// Gets the key on which to identify the annotated provider.
+ /// </summary>
+ public string DatabaseProviderKey
+ {
+ get { return _databaseProviderKey; }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
new file mode 100644
index 000000000..5163bff8b
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
@@ -0,0 +1,326 @@
+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;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <inheritdoc/>
+/// <summary>
+/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
+/// </summary>
+/// <param name="options">The database context options.</param>
+/// <param name="logger">Logger.</param>
+/// <param name="jellyfinDatabaseProvider">The provider for the database engine specific operations.</param>
+/// <param name="entityFrameworkCoreLocking">The locking behavior.</param>
+public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider, IEntityFrameworkCoreLockingBehavior entityFrameworkCoreLocking) : DbContext(options)
+{
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
+ /// </summary>
+ public DbSet<AccessSchedule> AccessSchedules => Set<AccessSchedule>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
+ /// </summary>
+ public DbSet<ActivityLog> ActivityLogs => Set<ActivityLog>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
+ /// </summary>
+ public DbSet<ApiKey> ApiKeys => Set<ApiKey>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
+ /// </summary>
+ public DbSet<Device> Devices => Set<Device>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
+ /// </summary>
+ public DbSet<DeviceOptions> DeviceOptions => Set<DeviceOptions>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
+ /// </summary>
+ public DbSet<DisplayPreferences> DisplayPreferences => Set<DisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
+ /// </summary>
+ public DbSet<ImageInfo> ImageInfos => Set<ImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
+ /// </summary>
+ public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => Set<ItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
+ /// </summary>
+ public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => Set<CustomItemDisplayPreferences>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
+ /// </summary>
+ public DbSet<Permission> Permissions => Set<Permission>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
+ /// </summary>
+ public DbSet<Preference> Preferences => Set<Preference>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the users.
+ /// </summary>
+ public DbSet<User> Users => Set<User>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
+ /// </summary>
+ public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
+ /// </summary>
+ public DbSet<MediaSegment> MediaSegments => Set<MediaSegment>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<UserData> UserData => Set<UserData>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<AncestorId> AncestorIds => Set<AncestorId>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => Set<AttachmentStreamInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<BaseItemEntity> BaseItems => Set<BaseItemEntity>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
+ /// </summary>
+ public DbSet<Chapter> Chapters => Set<Chapter>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<ItemValue> ItemValues => Set<ItemValue>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<ItemValueMap> ItemValuesMap => Set<ItemValueMap>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<MediaStreamInfo> MediaStreamInfos => Set<MediaStreamInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<People> Peoples => Set<People>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => Set<PeopleBaseItemMap>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
+ /// </summary>
+ public DbSet<BaseItemProvider> BaseItemProviders => Set<BaseItemProvider>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemImageInfo> BaseItemImageInfos => Set<BaseItemImageInfo>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemMetadataField> BaseItemMetadataFields => Set<BaseItemMetadataField>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => Set<BaseItemTrailerType>();
+
+ /// <summary>
+ /// Gets the <see cref="DbSet{TEntity}"/>.
+ /// </summary>
+ public DbSet<KeyframeData> KeyframeData => Set<KeyframeData>();
+
+ /*public DbSet<Artwork> Artwork => Set<Artwork>();
+
+ public DbSet<Book> Books => Set<Book>();
+
+ public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();
+
+ public DbSet<Chapter> Chapters => Set<Chapter>();
+
+ public DbSet<Collection> Collections => Set<Collection>();
+
+ public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
+
+ public DbSet<Company> Companies => Set<Company>();
+
+ public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();
+
+ public DbSet<CustomItem> CustomItems => Set<CustomItem>();
+
+ public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();
+
+ public DbSet<Episode> Episodes => Set<Episode>();
+
+ public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();
+
+ public DbSet<Genre> Genres => Set<Genre>();
+
+ public DbSet<Group> Groups => Set<Groups>();
+
+ public DbSet<Library> Libraries => Set<Library>();
+
+ public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();
+
+ public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();
+
+ public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();
+
+ public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();
+
+ public DbSet<Metadata> Metadata => Set<Metadata>();
+
+ public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();
+
+ public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();
+
+ public DbSet<Movie> Movies => Set<Movie>();
+
+ public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();
+
+ public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();
+
+ public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();
+
+ public DbSet<Person> People => Set<Person>();
+
+ public DbSet<PersonRole> PersonRoles => Set<PersonRole>();
+
+ public DbSet<Photo> Photo => Set<Photo>();
+
+ public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();
+
+ public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();
+
+ public DbSet<Rating> Ratings => Set<Rating>();
+
+ /// <summary>
+ /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
+ /// store review ratings, not age ratings.
+ /// </summary>
+ public DbSet<RatingSource> RatingSources => Set<RatingSource>();
+
+ public DbSet<Release> Releases => Set<Release>();
+
+ public DbSet<Season> Seasons => Set<Season>();
+
+ public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();
+
+ public DbSet<Series> Series => Set<Series>();
+
+ public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();
+
+ public DbSet<Track> Tracks => Set<Track>();
+
+ public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/
+
+ /// <inheritdoc/>
+ public override async Task<int> SaveChangesAsync(
+ bool acceptAllChangesOnSuccess,
+ CancellationToken cancellationToken = default)
+ {
+ HandleConcurrencyToken();
+
+ try
+ {
+ var result = -1;
+ await entityFrameworkCoreLocking.OnSaveChangesAsync(this, async () =>
+ {
+ result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
+ }).ConfigureAwait(false);
+ return result;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error trying to save changes.");
+ throw;
+ }
+ }
+
+ /// <inheritdoc/>
+ public override int SaveChanges(bool acceptAllChangesOnSuccess) // SaveChanges(bool) is beeing called by SaveChanges() with default to false.
+ {
+ HandleConcurrencyToken();
+
+ try
+ {
+ var result = -1;
+ entityFrameworkCoreLocking.OnSaveChanges(this, () =>
+ {
+ result = base.SaveChanges(acceptAllChangesOnSuccess);
+ });
+ return result;
+ }
+ catch (Exception e)
+ {
+ logger.LogError(e, "Error trying to save changes.");
+ throw;
+ }
+ }
+
+ private void HandleConcurrencyToken()
+ {
+ foreach (var saveEntity in ChangeTracker.Entries()
+ .Where(e => e.State == EntityState.Modified)
+ .Select(entry => entry.Entity)
+ .OfType<IHasConcurrencyToken>())
+ {
+ saveEntity.OnSavingChanges();
+ }
+ }
+
+ /// <inheritdoc />
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ jellyfinDatabaseProvider.OnModelCreating(modelBuilder);
+ base.OnModelCreating(modelBuilder);
+
+ // Configuration for each entity is in its own class inside 'ModelConfiguration'.
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
+ }
+
+ /// <inheritdoc />
+ protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
+ {
+ jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder);
+ base.ConfigureConventions(configurationBuilder);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs
new file mode 100644
index 000000000..4d5cfb8c9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinQueryHelperExtensions.cs
@@ -0,0 +1,166 @@
+#pragma warning disable RS0030 // Do not use banned APIs
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// Contains a number of query related extensions.
+/// </summary>
+public static class JellyfinQueryHelperExtensions
+{
+ private static readonly MethodInfo _containsMethodGenericCache = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static).First(m => m.Name == nameof(Enumerable.Contains) && m.GetParameters().Length == 2);
+ private static readonly MethodInfo _efParameterInstruction = typeof(EF).GetMethod(nameof(EF.Parameter), BindingFlags.Public | BindingFlags.Static)!;
+ private static readonly ConcurrentDictionary<Type, MethodInfo> _containsQueryCache = new();
+
+ /// <summary>
+ /// Builds an optimised query checking one property against a list of values while maintaining an optimal query.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity.</typeparam>
+ /// <typeparam name="TProperty">The property type to compare.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <param name="oneOf">The list of items to check.</param>
+ /// <param name="property">Property expression.</param>
+ /// <returns>A Query.</returns>
+ public static IQueryable<TEntity> WhereOneOrMany<TEntity, TProperty>(this IQueryable<TEntity> query, IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
+ {
+ return query.Where(OneOrManyExpressionBuilder(oneOf, property));
+ }
+
+ /// <summary>
+ /// Builds a query that checks referenced ItemValues for a cross BaseItem lookup.
+ /// </summary>
+ /// <param name="baseQuery">The source query.</param>
+ /// <param name="context">The database context.</param>
+ /// <param name="itemValueType">The type of item value to reference.</param>
+ /// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
+ /// <param name="invert">If set an exclusion check is performed instead.</param>
+ /// <returns>A Query.</returns>
+ public static IQueryable<BaseItemEntity> WhereReferencedItem(
+ this IQueryable<BaseItemEntity> baseQuery,
+ JellyfinDbContext context,
+ ItemValueType itemValueType,
+ IList<Guid> referenceIds,
+ bool invert = false)
+ {
+ return baseQuery.Where(ReferencedItemFilterExpressionBuilder(context, itemValueType, referenceIds, invert));
+ }
+
+ /// <summary>
+ /// Builds a query expression that checks referenced ItemValues for a cross BaseItem lookup.
+ /// </summary>
+ /// <param name="context">The database context.</param>
+ /// <param name="itemValueType">The type of item value to reference.</param>
+ /// <param name="referenceIds">The list of BaseItem ids to check matches.</param>
+ /// <param name="invert">If set an exclusion check is performed instead.</param>
+ /// <returns>A Query.</returns>
+ public static Expression<Func<BaseItemEntity, bool>> ReferencedItemFilterExpressionBuilder(
+ this JellyfinDbContext context,
+ ItemValueType itemValueType,
+ IList<Guid> referenceIds,
+ bool invert = false)
+ {
+ // Well genre/artist/album etc items do not actually set the ItemValue of thier specitic types so we cannot match it that way.
+ /*
+ "(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreIds and Type=2)))"
+ */
+
+ var itemFilter = OneOrManyExpressionBuilder<BaseItemEntity, Guid>(referenceIds, f => f.Id);
+
+ return item =>
+ context.ItemValues
+ .Join(context.ItemValuesMap, e => e.ItemValueId, e => e.ItemValueId, (item, map) => new { item, map })
+ .Any(val =>
+ val.item.Type == itemValueType
+ && context.BaseItems.Where(itemFilter).Any(e => e.CleanName == val.item.CleanValue)
+ && val.map.ItemId == item.Id) == EF.Constant(!invert);
+ }
+
+ /// <summary>
+ /// Builds an optimised query expression checking one property against a list of values while maintaining an optimal query.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity.</typeparam>
+ /// <typeparam name="TProperty">The property type to compare.</typeparam>
+ /// <param name="oneOf">The list of items to check.</param>
+ /// <param name="property">Property expression.</param>
+ /// <returns>A Query.</returns>
+ public static Expression<Func<TEntity, bool>> OneOrManyExpressionBuilder<TEntity, TProperty>(this IList<TProperty> oneOf, Expression<Func<TEntity, TProperty>> property)
+ {
+ var parameter = Expression.Parameter(typeof(TEntity), "item");
+ property = ParameterReplacer.Replace<Func<TEntity, TProperty>, Func<TEntity, TProperty>>(property, property.Parameters[0], parameter);
+ if (oneOf.Count == 1)
+ {
+ var value = oneOf[0];
+ if (typeof(TProperty).IsValueType)
+ {
+ return Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(property.Body, Expression.Constant(value)), parameter);
+ }
+ else
+ {
+ return Expression.Lambda<Func<TEntity, bool>>(Expression.ReferenceEqual(property.Body, Expression.Constant(value)), parameter);
+ }
+ }
+
+ var containsMethodInfo = _containsQueryCache.GetOrAdd(typeof(TProperty), static (key) => _containsMethodGenericCache.MakeGenericMethod(key));
+
+ if (oneOf.Count < 4) // arbitrary value choosen.
+ {
+ // if we have 3 or fewer values to check against its faster to do a IN(const,const,const) lookup
+ return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Constant(oneOf), property.Body), parameter);
+ }
+
+ return Expression.Lambda<Func<TEntity, bool>>(Expression.Call(null, containsMethodInfo, Expression.Call(null, _efParameterInstruction.MakeGenericMethod(oneOf.GetType()), Expression.Constant(oneOf)), property.Body), parameter);
+ }
+
+ internal static class ParameterReplacer
+ {
+ // Produces an expression identical to 'expression'
+ // except with 'source' parameter replaced with 'target' expression.
+ internal static Expression<TOutput> Replace<TInput, TOutput>(
+ Expression<TInput> expression,
+ ParameterExpression source,
+ ParameterExpression target)
+ {
+ return new ParameterReplacerVisitor<TOutput>(source, target)
+ .VisitAndConvert(expression);
+ }
+
+ private sealed class ParameterReplacerVisitor<TOutput> : ExpressionVisitor
+ {
+ private readonly ParameterExpression _source;
+ private readonly ParameterExpression _target;
+
+ public ParameterReplacerVisitor(ParameterExpression source, ParameterExpression target)
+ {
+ _source = source;
+ _target = target;
+ }
+
+ internal Expression<TOutput> VisitAndConvert<T>(Expression<T> root)
+ {
+ return (Expression<TOutput>)VisitLambda(root);
+ }
+
+ protected override Expression VisitLambda<T>(Expression<T> node)
+ {
+ // Leave all parameters alone except the one we want to replace.
+ var parameters = node.Parameters.Select(p => p == _source ? _target : p);
+
+ return Expression.Lambda<TOutput>(Visit(node.Body), parameters);
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ {
+ // Replace the source with the target, visit other params as usual.
+ return node == _source ? _target : base.VisitParameter(node);
+ }
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/IEntityFrameworkCoreLockingBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/IEntityFrameworkCoreLockingBehavior.cs
new file mode 100644
index 000000000..465c31212
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/IEntityFrameworkCoreLockingBehavior.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Database.Implementations.Locking;
+
+/// <summary>
+/// Defines a jellyfin locking behavior that can be configured.
+/// </summary>
+public interface IEntityFrameworkCoreLockingBehavior
+{
+ /// <summary>
+ /// Provides access to the builder to setup any connection related locking behavior.
+ /// </summary>
+ /// <param name="optionsBuilder">The options builder.</param>
+ void Initialise(DbContextOptionsBuilder optionsBuilder);
+
+ /// <summary>
+ /// Will be invoked when changes should be saved in the current locking behavior.
+ /// </summary>
+ /// <param name="context">The database context invoking the action.</param>
+ /// <param name="saveChanges">Callback for performing the actual save changes.</param>
+ void OnSaveChanges(JellyfinDbContext context, Action saveChanges);
+
+ /// <summary>
+ /// Will be invoked when changes should be saved in the current locking behavior.
+ /// </summary>
+ /// <param name="context">The database context invoking the action.</param>
+ /// <param name="saveChanges">Callback for performing the actual save changes.</param>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges);
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/NoLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/NoLockBehavior.cs
new file mode 100644
index 000000000..3b654f4c4
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/NoLockBehavior.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Database.Implementations.Locking;
+
+/// <summary>
+/// Default lock behavior. Defines no explicit application locking behavior.
+/// </summary>
+public class NoLockBehavior : IEntityFrameworkCoreLockingBehavior
+{
+ private readonly ILogger<NoLockBehavior> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NoLockBehavior"/> class.
+ /// </summary>
+ /// <param name="logger">The Application logger.</param>
+ public NoLockBehavior(ILogger<NoLockBehavior> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
+ {
+ saveChanges();
+ }
+
+ /// <inheritdoc/>
+ public void Initialise(DbContextOptionsBuilder optionsBuilder)
+ {
+ _logger.LogInformation("The database locking mode has been set to: NoLock.");
+ }
+
+ /// <inheritdoc/>
+ public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
+ {
+ await saveChanges().ConfigureAwait(false);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
new file mode 100644
index 000000000..9395b2e2d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/OptimisticLockBehavior.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Data.Common;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Polly;
+
+namespace Jellyfin.Database.Implementations.Locking;
+
+/// <summary>
+/// Defines a locking mechanism that will retry any write operation for a few times.
+/// </summary>
+public class OptimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
+{
+ private readonly Policy _writePolicy;
+ private readonly AsyncPolicy _writeAsyncPolicy;
+ private readonly ILogger<OptimisticLockBehavior> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OptimisticLockBehavior"/> class.
+ /// </summary>
+ /// <param name="logger">The application logger.</param>
+ public OptimisticLockBehavior(ILogger<OptimisticLockBehavior> logger)
+ {
+ TimeSpan[] sleepDurations = [
+ TimeSpan.FromMilliseconds(50),
+ TimeSpan.FromMilliseconds(50),
+ TimeSpan.FromMilliseconds(250),
+ TimeSpan.FromMilliseconds(150),
+ TimeSpan.FromMilliseconds(500),
+ TimeSpan.FromMilliseconds(500),
+ TimeSpan.FromSeconds(3)
+ ];
+ _logger = logger;
+ _writePolicy = Policy.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)).WaitAndRetry(sleepDurations, RetryHandle);
+ _writeAsyncPolicy = Policy.HandleInner<Exception>(e => e.Message.Contains("database is locked", StringComparison.InvariantCultureIgnoreCase)).WaitAndRetryAsync(sleepDurations, RetryHandle);
+
+ void RetryHandle(Exception exception, TimeSpan timespan, int retryNo, Context context)
+ {
+ if (retryNo < sleepDurations.Length)
+ {
+ _logger.LogWarning("Operation failed retry {RetryNo}", retryNo);
+ }
+ else
+ {
+ _logger.LogError(exception, "Operation failed retry {RetryNo}", retryNo);
+ }
+ }
+ }
+
+ /// <inheritdoc/>
+ public void Initialise(DbContextOptionsBuilder optionsBuilder)
+ {
+ _logger.LogInformation("The database locking mode has been set to: Optimistic.");
+ optionsBuilder.AddInterceptors(new RetryInterceptor(_writeAsyncPolicy, _writePolicy));
+ optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_writeAsyncPolicy, _writePolicy));
+ }
+
+ /// <inheritdoc/>
+ public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
+ {
+ _writePolicy.ExecuteAndCapture(saveChanges);
+ }
+
+ /// <inheritdoc/>
+ public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
+ {
+ await _writeAsyncPolicy.ExecuteAndCaptureAsync(saveChanges).ConfigureAwait(false);
+ }
+
+ private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
+ {
+ private readonly AsyncPolicy _asyncRetryPolicy;
+ private readonly Policy _retryPolicy;
+
+ public TransactionLockingInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
+ {
+ _asyncRetryPolicy = asyncRetryPolicy;
+ _retryPolicy = retryPolicy;
+ }
+
+ public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result)
+ {
+ return InterceptionResult<DbTransaction>.SuppressWithResult(_retryPolicy.Execute(() => connection.BeginTransaction(eventData.IsolationLevel)));
+ }
+
+ public override async ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result, CancellationToken cancellationToken = default)
+ {
+ return InterceptionResult<DbTransaction>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await connection.BeginTransactionAsync(eventData.IsolationLevel, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false));
+ }
+ }
+
+ private sealed class RetryInterceptor : DbCommandInterceptor
+ {
+ private readonly AsyncPolicy _asyncRetryPolicy;
+ private readonly Policy _retryPolicy;
+
+ public RetryInterceptor(AsyncPolicy asyncRetryPolicy, Policy retryPolicy)
+ {
+ _asyncRetryPolicy = asyncRetryPolicy;
+ _retryPolicy = retryPolicy;
+ }
+
+ public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
+ {
+ return InterceptionResult<int>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteNonQuery));
+ }
+
+ public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
+ {
+ return InterceptionResult<int>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false));
+ }
+
+ public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
+ {
+ return InterceptionResult<object>.SuppressWithResult(_retryPolicy.Execute(() => command.ExecuteScalar()!));
+ }
+
+ public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
+ {
+ return InterceptionResult<object>.SuppressWithResult((await _asyncRetryPolicy.ExecuteAsync(async () => await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false)!).ConfigureAwait(false))!);
+ }
+
+ public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
+ {
+ return InterceptionResult<DbDataReader>.SuppressWithResult(_retryPolicy.Execute(command.ExecuteReader));
+ }
+
+ public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
+ {
+ return InterceptionResult<DbDataReader>.SuppressWithResult(await _asyncRetryPolicy.ExecuteAsync(async () => await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false));
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
new file mode 100644
index 000000000..2d6bc6902
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Locking/PessimisticLockBehavior.cs
@@ -0,0 +1,296 @@
+#pragma warning disable MT1013 // Releasing lock without guarantee of execution
+#pragma warning disable MT1012 // Acquiring lock without guarantee of releasing
+
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Database.Implementations.Locking;
+
+/// <summary>
+/// A locking behavior that will always block any operation while a write is requested. Mimicks the old SqliteRepository behavior.
+/// </summary>
+public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
+{
+ private readonly ILogger<PessimisticLockBehavior> _logger;
+ private readonly ILoggerFactory _loggerFactory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PessimisticLockBehavior"/> class.
+ /// </summary>
+ /// <param name="logger">The application logger.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
+ public PessimisticLockBehavior(ILogger<PessimisticLockBehavior> logger, ILoggerFactory loggerFactory)
+ {
+ _logger = logger;
+ _loggerFactory = loggerFactory;
+ }
+
+ private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
+
+ /// <inheritdoc/>
+ public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
+ {
+ using (DbLock.EnterWrite(_logger))
+ {
+ saveChanges();
+ }
+ }
+
+ /// <inheritdoc/>
+ public void Initialise(DbContextOptionsBuilder optionsBuilder)
+ {
+ _logger.LogInformation("The database locking mode has been set to: Pessimistic.");
+ optionsBuilder.AddInterceptors(new CommandLockingInterceptor(_loggerFactory.CreateLogger<CommandLockingInterceptor>()));
+ optionsBuilder.AddInterceptors(new TransactionLockingInterceptor(_loggerFactory.CreateLogger<TransactionLockingInterceptor>()));
+ }
+
+ /// <inheritdoc/>
+ public async Task OnSaveChangesAsync(JellyfinDbContext context, Func<Task> saveChanges)
+ {
+ using (DbLock.EnterWrite(_logger))
+ {
+ await saveChanges().ConfigureAwait(false);
+ }
+ }
+
+ private sealed class TransactionLockingInterceptor : DbTransactionInterceptor
+ {
+ private readonly ILogger _logger;
+
+ public TransactionLockingInterceptor(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result)
+ {
+ DbLock.BeginWriteLock(_logger);
+
+ return base.TransactionStarting(connection, eventData, result);
+ }
+
+ public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result, CancellationToken cancellationToken = default)
+ {
+ DbLock.BeginWriteLock(_logger);
+
+ return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
+ }
+
+ public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ base.TransactionCommitted(transaction, eventData);
+ }
+
+ public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
+ }
+
+ public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ base.TransactionFailed(transaction, eventData);
+ }
+
+ public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, CancellationToken cancellationToken = default)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
+ }
+
+ public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ base.TransactionRolledBack(transaction, eventData);
+ }
+
+ public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
+ {
+ DbLock.EndWriteLock(_logger);
+
+ return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Adds strict read/write locking.
+ /// </summary>
+ private sealed class CommandLockingInterceptor : DbCommandInterceptor
+ {
+ private readonly ILogger _logger;
+
+ public CommandLockingInterceptor(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
+ {
+ using (DbLock.EnterWrite(_logger, command))
+ {
+ return InterceptionResult<int>.SuppressWithResult(command.ExecuteNonQuery());
+ }
+ }
+
+ public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
+ {
+ using (DbLock.EnterWrite(_logger, command))
+ {
+ return InterceptionResult<int>.SuppressWithResult(await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false));
+ }
+ }
+
+ public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
+ {
+ using (DbLock.EnterRead(_logger))
+ {
+ return InterceptionResult<object>.SuppressWithResult(command.ExecuteScalar()!);
+ }
+ }
+
+ public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
+ {
+ using (DbLock.EnterRead(_logger))
+ {
+ return InterceptionResult<object>.SuppressWithResult((await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!);
+ }
+ }
+
+ public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
+ {
+ using (DbLock.EnterRead(_logger))
+ {
+ return InterceptionResult<DbDataReader>.SuppressWithResult(command.ExecuteReader()!);
+ }
+ }
+
+ public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
+ {
+ using (DbLock.EnterRead(_logger))
+ {
+ return InterceptionResult<DbDataReader>.SuppressWithResult(await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false));
+ }
+ }
+ }
+
+ private sealed class DbLock : IDisposable
+ {
+ private readonly Action? _action;
+ private bool _disposed;
+
+ private static readonly IDisposable _noLock = new DbLock(null) { _disposed = true };
+ private static (string Command, Guid Id, DateTimeOffset QueryDate, bool Printed) _blockQuery;
+
+ public DbLock(Action? action = null)
+ {
+ _action = action;
+ }
+
+#pragma warning disable IDISP015 // Member should not return created and cached instance
+ public static IDisposable EnterWrite(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+#pragma warning restore IDISP015 // Member should not return created and cached instance
+ {
+ logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
+ if (DatabaseLock.IsWriteLockHeld)
+ {
+ logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
+ return _noLock;
+ }
+
+ BeginWriteLock(logger, command, callerMemberName, callerNo);
+ return new DbLock(() =>
+ {
+ EndWriteLock(logger, callerMemberName, callerNo);
+ });
+ }
+
+#pragma warning disable IDISP015 // Member should not return created and cached instance
+ public static IDisposable EnterRead(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+#pragma warning restore IDISP015 // Member should not return created and cached instance
+ {
+ logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
+ if (DatabaseLock.IsWriteLockHeld)
+ {
+ logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
+ return _noLock;
+ }
+
+ BeginReadLock(logger, callerMemberName, callerNo);
+ return new DbLock(() =>
+ {
+ ExitReadLock(logger, callerMemberName, callerNo);
+ });
+ }
+
+ public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+ {
+ logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
+ if (!DatabaseLock.TryEnterWriteLock(TimeSpan.FromMilliseconds(1000)))
+ {
+ var blockingQuery = _blockQuery;
+ if (!blockingQuery.Printed)
+ {
+ _blockQuery = (blockingQuery.Command, blockingQuery.Id, blockingQuery.QueryDate, true);
+ logger.LogInformation("QueryLock: {Id} --- {Query}", blockingQuery.Id, blockingQuery.Command);
+ }
+
+ logger.LogInformation("Query congestion detected: '{Id}' since '{Date}'", blockingQuery.Id, blockingQuery.QueryDate);
+
+ DatabaseLock.EnterWriteLock();
+
+ logger.LogInformation("Query congestion cleared: '{Id}' for '{Date}'", blockingQuery.Id, DateTimeOffset.Now - blockingQuery.QueryDate);
+ }
+
+ _blockQuery = (command?.CommandText ?? "Transaction", Guid.NewGuid(), DateTimeOffset.Now, false);
+
+ logger.LogTrace("Write Aquired {Caller}:{Line}", callerMemberName, callerNo);
+ }
+
+ public static void BeginReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+ {
+ logger.LogTrace("Aquire Write {Caller}:{Line}", callerMemberName, callerNo);
+ DatabaseLock.EnterReadLock();
+ logger.LogTrace("Read Aquired {Caller}:{Line}", callerMemberName, callerNo);
+ }
+
+ public static void EndWriteLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+ {
+ logger.LogTrace("Release Write {Caller}:{Line}", callerMemberName, callerNo);
+ DatabaseLock.ExitWriteLock();
+ }
+
+ public static void ExitReadLock(ILogger logger, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
+ {
+ logger.LogTrace("Release Read {Caller}:{Line}", callerMemberName, callerNo);
+ DatabaseLock.ExitReadLock();
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ if (_action is not null)
+ {
+ _action();
+ }
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs
new file mode 100644
index 000000000..a209c5b90
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ActivityLogConfiguration.cs
@@ -0,0 +1,17 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the ActivityLog entity.
+/// </summary>
+public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ActivityLog> builder)
+ {
+ builder.HasIndex(entity => entity.DateCreated);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
new file mode 100644
index 000000000..67269153d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AncestorIdConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// AncestorId configuration.
+/// </summary>
+public class AncestorIdConfiguration : IEntityTypeConfiguration<AncestorId>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<AncestorId> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ParentItemId });
+ builder.HasIndex(e => e.ParentItemId);
+ builder.HasOne(e => e.ParentItem).WithMany(e => e.Children).HasForeignKey(f => f.ParentItemId);
+ builder.HasOne(e => e.Item).WithMany(e => e.Parents).HasForeignKey(f => f.ItemId);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
new file mode 100644
index 000000000..ea382c718
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ApiKeyConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Database.Implementations.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the ApiKey entity.
+ /// </summary>
+ public class ApiKeyConfiguration : IEntityTypeConfiguration<ApiKey>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ApiKey> builder)
+ {
+ builder
+ .HasIndex(entity => entity.AccessToken)
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs
new file mode 100644
index 000000000..66cafc83c
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/AttachmentStreamInfoConfiguration.cs
@@ -0,0 +1,17 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the AttachmentStreamInfo entity.
+/// </summary>
+public class AttachmentStreamInfoConfiguration : IEntityTypeConfiguration<AttachmentStreamInfo>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<AttachmentStreamInfo> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.Index });
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
new file mode 100644
index 000000000..bcf458abd
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemConfiguration.cs
@@ -0,0 +1,65 @@
+using System;
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Configuration for BaseItem.
+/// </summary>
+public class BaseItemConfiguration : IEntityTypeConfiguration<BaseItemEntity>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemEntity> builder)
+ {
+ builder.HasKey(e => e.Id);
+ // TODO: See rant in entity file.
+ // builder.HasOne(e => e.Parent).WithMany(e => e.DirectChildren).HasForeignKey(e => e.ParentId);
+ // builder.HasOne(e => e.TopParent).WithMany(e => e.AllChildren).HasForeignKey(e => e.TopParentId);
+ // builder.HasOne(e => e.Season).WithMany(e => e.SeasonEpisodes).HasForeignKey(e => e.SeasonId);
+ // builder.HasOne(e => e.Series).WithMany(e => e.SeriesEpisodes).HasForeignKey(e => e.SeriesId);
+ builder.HasMany(e => e.Peoples);
+ builder.HasMany(e => e.UserData);
+ builder.HasMany(e => e.ItemValues);
+ builder.HasMany(e => e.MediaStreams);
+ builder.HasMany(e => e.Chapters);
+ builder.HasMany(e => e.Provider);
+ builder.HasMany(e => e.Parents);
+ builder.HasMany(e => e.Children);
+ builder.HasMany(e => e.LockedFields);
+ builder.HasMany(e => e.TrailerTypes);
+ builder.HasMany(e => e.Images);
+
+ builder.HasIndex(e => e.Path);
+ builder.HasIndex(e => e.ParentId);
+ builder.HasIndex(e => e.PresentationUniqueKey);
+ builder.HasIndex(e => new { e.Id, e.Type, e.IsFolder, e.IsVirtualItem });
+
+ // covering index
+ builder.HasIndex(e => new { e.TopParentId, e.Id });
+ // series
+ builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.PresentationUniqueKey, e.SortName });
+ // series counts
+ // seriesdateplayed sort order
+ builder.HasIndex(e => new { e.Type, e.SeriesPresentationUniqueKey, e.IsFolder, e.IsVirtualItem });
+ // live tv programs
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.StartDate });
+ // covering index for getitemvalues
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.Id });
+ // used by movie suggestions
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.PresentationUniqueKey });
+ // latest items
+ builder.HasIndex(e => new { e.Type, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
+ builder.HasIndex(e => new { e.IsFolder, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey, e.DateCreated });
+ // resume
+ builder.HasIndex(e => new { e.MediaType, e.TopParentId, e.IsVirtualItem, e.PresentationUniqueKey });
+
+ builder.HasData(new BaseItemEntity()
+ {
+ Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
+ Type = "PLACEHOLDER",
+ Name = "This is a placeholder item for UserData that has been detacted from its original item",
+ });
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
new file mode 100644
index 000000000..a602ea65f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemMetadataFieldConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemMetadataFieldConfiguration : IEntityTypeConfiguration<BaseItemMetadataField>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemMetadataField> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
new file mode 100644
index 000000000..dd28000ba
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemProviderConfiguration.cs
@@ -0,0 +1,19 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// BaseItemProvider configuration.
+/// </summary>
+public class BaseItemProviderConfiguration : IEntityTypeConfiguration<BaseItemProvider>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemProvider> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ProviderId });
+ builder.HasOne(e => e.Item);
+ builder.HasIndex(e => new { e.ProviderId, e.ProviderValue, e.ItemId });
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
new file mode 100644
index 000000000..2a888b7de
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/BaseItemTrailerTypeConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Provides configuration for the BaseItemMetadataField entity.
+/// </summary>
+public class BaseItemTrailerTypeConfiguration : IEntityTypeConfiguration<BaseItemTrailerType>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<BaseItemTrailerType> builder)
+ {
+ builder.HasKey(e => new { e.Id, e.ItemId });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs
new file mode 100644
index 000000000..d97a39f4d
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ChapterConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// Chapter configuration.
+/// </summary>
+public class ChapterConfiguration : IEntityTypeConfiguration<Chapter>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Chapter> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.ChapterIndex });
+ builder.HasOne(e => e.Item);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..e8a510ab9
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/CustomItemDisplayPreferencesConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the CustomItemDisplayPreferences entity.
+ /// </summary>
+ public class CustomItemDisplayPreferencesConfiguration : IEntityTypeConfiguration<CustomItemDisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<CustomItemDisplayPreferences> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs
new file mode 100644
index 000000000..3551f7686
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceConfiguration.cs
@@ -0,0 +1,28 @@
+using Jellyfin.Database.Implementations.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Device entity.
+ /// </summary>
+ public class DeviceConfiguration : IEntityTypeConfiguration<Device>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Device> builder)
+ {
+ builder
+ .HasIndex(entity => new { entity.DeviceId, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.AccessToken, entity.DateLastActivity });
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.DeviceId });
+
+ builder
+ .HasIndex(entity => entity.DeviceId);
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
new file mode 100644
index 000000000..9055e8025
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DeviceOptionsConfiguration.cs
@@ -0,0 +1,20 @@
+using Jellyfin.Database.Implementations.Entities.Security;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DeviceOptions entity.
+ /// </summary>
+ public class DeviceOptionsConfiguration : IEntityTypeConfiguration<DeviceOptions>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DeviceOptions> builder)
+ {
+ builder
+ .HasIndex(entity => entity.DeviceId)
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
new file mode 100644
index 000000000..45e0c6482
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/DisplayPreferencesConfiguration.cs
@@ -0,0 +1,25 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the DisplayPreferencesConfiguration entity.
+ /// </summary>
+ public class DisplayPreferencesConfiguration : IEntityTypeConfiguration<DisplayPreferences>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<DisplayPreferences> builder)
+ {
+ builder
+ .HasMany(d => d.HomeSections)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
new file mode 100644
index 000000000..97ebc2e01
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesConfiguration.cs
@@ -0,0 +1,19 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesConfiguration : IEntityTypeConfiguration<ItemValue>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ItemValue> builder)
+ {
+ builder.HasKey(e => e.ItemValueId);
+ builder.HasIndex(e => new { e.Type, e.CleanValue });
+ builder.HasIndex(e => new { e.Type, e.Value }).IsUnique();
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
new file mode 100644
index 000000000..42ef23532
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/ItemValuesMapConfiguration.cs
@@ -0,0 +1,19 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// itemvalues Configuration.
+/// </summary>
+public class ItemValuesMapConfiguration : IEntityTypeConfiguration<ItemValueMap>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<ItemValueMap> builder)
+ {
+ builder.HasKey(e => new { e.ItemValueId, e.ItemId });
+ builder.HasOne(e => e.Item);
+ builder.HasOne(e => e.ItemValue);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs
new file mode 100644
index 000000000..3f5d458ca
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/KeyframeDataConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// KeyframeData Configuration.
+/// </summary>
+public class KeyframeDataConfiguration : IEntityTypeConfiguration<KeyframeData>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<KeyframeData> builder)
+ {
+ builder.HasKey(e => e.ItemId);
+ builder.HasOne(e => e.Item).WithMany().HasForeignKey(e => e.ItemId);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
new file mode 100644
index 000000000..075af2c05
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/MediaStreamInfoConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class MediaStreamInfoConfiguration : IEntityTypeConfiguration<MediaStreamInfo>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<MediaStreamInfo> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.StreamIndex });
+ builder.HasIndex(e => e.StreamIndex);
+ builder.HasIndex(e => e.StreamType);
+ builder.HasIndex(e => new { e.StreamIndex, e.StreamType });
+ builder.HasIndex(e => new { e.StreamIndex, e.StreamType, e.Language });
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
new file mode 100644
index 000000000..5e3ab4443
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleBaseItemMapConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleBaseItemMapConfiguration : IEntityTypeConfiguration<PeopleBaseItemMap>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<PeopleBaseItemMap> builder)
+ {
+ builder.HasKey(e => new { e.ItemId, e.PeopleId });
+ builder.HasIndex(e => new { e.ItemId, e.SortOrder });
+ builder.HasIndex(e => new { e.ItemId, e.ListOrder });
+ builder.HasOne(e => e.Item);
+ builder.HasOne(e => e.People);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs
new file mode 100644
index 000000000..e8f77a806
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PeopleConfiguration.cs
@@ -0,0 +1,19 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// People configuration.
+/// </summary>
+public class PeopleConfiguration : IEntityTypeConfiguration<People>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<People> builder)
+ {
+ builder.HasKey(e => e.Id);
+ builder.HasIndex(e => e.Name);
+ builder.HasMany(e => e.BaseItems);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs
new file mode 100644
index 000000000..d2aed54eb
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PermissionConfiguration.cs
@@ -0,0 +1,24 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PermissionConfiguration : IEntityTypeConfiguration<Permission>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Permission> builder)
+ {
+ // Used to get a user's permissions or a specific permission for a user.
+ // Also prevents multiple values being created for a user.
+ // Filtered over non-null user ids for when other entities (groups, API keys) get permissions
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs
new file mode 100644
index 000000000..207051bcd
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/PreferenceConfiguration.cs
@@ -0,0 +1,21 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the Permission entity.
+ /// </summary>
+ public class PreferenceConfiguration : IEntityTypeConfiguration<Preference>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<Preference> builder)
+ {
+ builder
+ .HasIndex(p => new { p.UserId, p.Kind })
+ .HasFilter("[UserId] IS NOT NULL")
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
new file mode 100644
index 000000000..1b364a05e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
@@ -0,0 +1,18 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the TrickplayInfo entity.
+ /// </summary>
+ public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<TrickplayInfo> builder)
+ {
+ builder.HasKey(info => new { info.ItemId, info.Width });
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs
new file mode 100644
index 000000000..61b5e06e8
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserConfiguration.cs
@@ -0,0 +1,55 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration
+{
+ /// <summary>
+ /// FluentAPI configuration for the User entity.
+ /// </summary>
+ public class UserConfiguration : IEntityTypeConfiguration<User>
+ {
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<User> builder)
+ {
+ builder
+ .Property(user => user.Username);
+
+ builder
+ .HasOne(u => u.ProfileImage)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Permissions)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.Preferences)
+ .WithOne()
+ .HasForeignKey(p => p.UserId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.AccessSchedules)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.DisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasMany(u => u.ItemDisplayPreferences)
+ .WithOne()
+ .OnDelete(DeleteBehavior.Cascade);
+
+ builder
+ .HasIndex(entity => entity.Username)
+ .IsUnique();
+ }
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs
new file mode 100644
index 000000000..e7b436293
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ModelConfiguration/UserDataConfiguration.cs
@@ -0,0 +1,22 @@
+using Jellyfin.Database.Implementations.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Database.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the UserData entity.
+/// </summary>
+public class UserDataConfiguration : IEntityTypeConfiguration<UserData>
+{
+ /// <inheritdoc/>
+ public void Configure(EntityTypeBuilder<UserData> builder)
+ {
+ builder.HasKey(d => new { d.ItemId, d.UserId, d.CustomDataKey });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.Played });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.PlaybackPositionTicks });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.IsFavorite });
+ builder.HasIndex(d => new { d.ItemId, d.UserId, d.LastPlayedDate });
+ builder.HasOne(e => e.Item).WithMany(e => e.UserData);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/ProgressablePartitionReporting.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ProgressablePartitionReporting.cs
new file mode 100644
index 000000000..7654dd3c5
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/ProgressablePartitionReporting.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// Wrapper for progress reporting on Partition helpers.
+/// </summary>
+/// <typeparam name="TEntity">The entity to load.</typeparam>
+public class ProgressablePartitionReporting<TEntity>
+{
+ private readonly IOrderedQueryable<TEntity> _source;
+
+ private readonly Stopwatch _partitionTime = new();
+
+ private readonly Stopwatch _itemTime = new();
+
+ internal ProgressablePartitionReporting(IOrderedQueryable<TEntity> source)
+ {
+ _source = source;
+ }
+
+ internal Action<TEntity, int, int>? OnBeginItem { get; set; }
+
+ internal Action<int>? OnBeginPartition { get; set; }
+
+ internal Action<TEntity, int, int, TimeSpan>? OnEndItem { get; set; }
+
+ internal Action<int, TimeSpan>? OnEndPartition { get; set; }
+
+ internal IOrderedQueryable<TEntity> Source => _source;
+
+ internal void BeginItem(TEntity entity, int iteration, int itemIndex)
+ {
+ _itemTime.Restart();
+ OnBeginItem?.Invoke(entity, iteration, itemIndex);
+ }
+
+ internal void BeginPartition(int iteration)
+ {
+ _partitionTime.Restart();
+ OnBeginPartition?.Invoke(iteration);
+ }
+
+ internal void EndItem(TEntity entity, int iteration, int itemIndex)
+ {
+ OnEndItem?.Invoke(entity, iteration, itemIndex, _itemTime.Elapsed);
+ }
+
+ internal void EndPartition(int iteration)
+ {
+ OnEndPartition?.Invoke(iteration, _partitionTime.Elapsed);
+ }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs
new file mode 100644
index 000000000..c20dfeeb5
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/QueryPartitionHelpers.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.EntityFrameworkCore;
+
+namespace Jellyfin.Database.Implementations;
+
+/// <summary>
+/// Contains helpers to partition EFCore queries.
+/// </summary>
+public static class QueryPartitionHelpers
+{
+ /// <summary>
+ /// Adds a callback to any directly following calls of Partition for every partition thats been invoked.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <param name="beginPartition">The callback invoked for partition before enumerating items.</param>
+ /// <param name="endPartition">The callback invoked for partition after enumerating items.</param>
+ /// <returns>A queryable that can be used to partition.</returns>
+ public static ProgressablePartitionReporting<TEntity> WithPartitionProgress<TEntity>(this IOrderedQueryable<TEntity> query, Action<int>? beginPartition = null, Action<int, TimeSpan>? endPartition = null)
+ {
+ var progressable = new ProgressablePartitionReporting<TEntity>(query);
+ progressable.OnBeginPartition = beginPartition;
+ progressable.OnEndPartition = endPartition;
+ return progressable;
+ }
+
+ /// <summary>
+ /// Adds a callback to any directly following calls of Partition for every item thats been invoked.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <param name="beginItem">The callback invoked for each item before processing.</param>
+ /// <param name="endItem">The callback invoked for each item after processing.</param>
+ /// <returns>A queryable that can be used to partition.</returns>
+ public static ProgressablePartitionReporting<TEntity> WithItemProgress<TEntity>(this IOrderedQueryable<TEntity> query, Action<TEntity, int, int>? beginItem = null, Action<TEntity, int, int, TimeSpan>? endItem = null)
+ {
+ var progressable = new ProgressablePartitionReporting<TEntity>(query);
+ progressable.OnBeginItem = beginItem;
+ progressable.OnEndItem = endItem;
+ return progressable;
+ }
+
+ /// <summary>
+ /// Adds a callback to any directly following calls of Partition for every partition thats been invoked.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="progressable">The source query.</param>
+ /// <param name="beginPartition">The callback invoked for partition before enumerating items.</param>
+ /// <param name="endPartition">The callback invoked for partition after enumerating items.</param>
+ /// <returns>A queryable that can be used to partition.</returns>
+ public static ProgressablePartitionReporting<TEntity> WithPartitionProgress<TEntity>(this ProgressablePartitionReporting<TEntity> progressable, Action<int>? beginPartition = null, Action<int, TimeSpan>? endPartition = null)
+ {
+ progressable.OnBeginPartition = beginPartition;
+ progressable.OnEndPartition = endPartition;
+ return progressable;
+ }
+
+ /// <summary>
+ /// Adds a callback to any directly following calls of Partition for every item thats been invoked.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="progressable">The source query.</param>
+ /// <param name="beginItem">The callback invoked for each item before processing.</param>
+ /// <param name="endItem">The callback invoked for each item after processing.</param>
+ /// <returns>A queryable that can be used to partition.</returns>
+ public static ProgressablePartitionReporting<TEntity> WithItemProgress<TEntity>(this ProgressablePartitionReporting<TEntity> progressable, Action<TEntity, int, int>? beginItem = null, Action<TEntity, int, int, TimeSpan>? endItem = null)
+ {
+ progressable.OnBeginItem = beginItem;
+ progressable.OnEndItem = endItem;
+ return progressable;
+ }
+
+ /// <summary>
+ /// Enumerates the source query by loading the entities in partitions in a lazy manner reading each item from the database as its requested.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="partitionInfo">The source query.</param>
+ /// <param name="partitionSize">The number of elements to load per partition.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A enumerable representing the whole of the query.</returns>
+ public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await foreach (var item in partitionInfo.Source.PartitionAsync(partitionSize, partitionInfo, cancellationToken).ConfigureAwait(false))
+ {
+ yield return item;
+ }
+ }
+
+ /// <summary>
+ /// Enumerates the source query by loading the entities in partitions directly into memory.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="partitionInfo">The source query.</param>
+ /// <param name="partitionSize">The number of elements to load per partition.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A enumerable representing the whole of the query.</returns>
+ public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(this ProgressablePartitionReporting<TEntity> partitionInfo, int partitionSize, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await foreach (var item in partitionInfo.Source.PartitionEagerAsync(partitionSize, partitionInfo, cancellationToken).ConfigureAwait(false))
+ {
+ yield return item;
+ }
+ }
+
+ /// <summary>
+ /// Enumerates the source query by loading the entities in partitions in a lazy manner reading each item from the database as its requested.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <param name="partitionSize">The number of elements to load per partition.</param>
+ /// <param name="progressablePartition">Reporting helper.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A enumerable representing the whole of the query.</returns>
+ public static async IAsyncEnumerable<TEntity> PartitionAsync<TEntity>(
+ this IOrderedQueryable<TEntity> query,
+ int partitionSize,
+ ProgressablePartitionReporting<TEntity>? progressablePartition = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var iterator = 0;
+ int itemCounter;
+ do
+ {
+ progressablePartition?.BeginPartition(iterator);
+ itemCounter = 0;
+ await foreach (var item in query
+ .Skip(partitionSize * iterator)
+ .Take(partitionSize)
+ .AsAsyncEnumerable()
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ progressablePartition?.BeginItem(item, iterator, itemCounter);
+ yield return item;
+ progressablePartition?.EndItem(item, iterator, itemCounter);
+ itemCounter++;
+ }
+
+ progressablePartition?.EndPartition(iterator);
+ iterator++;
+ } while (itemCounter == partitionSize && !cancellationToken.IsCancellationRequested);
+ }
+
+ /// <summary>
+ /// Enumerates the source query by loading the entities in partitions directly into memory.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <param name="partitionSize">The number of elements to load per partition.</param>
+ /// <param name="progressablePartition">Reporting helper.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>A enumerable representing the whole of the query.</returns>
+ public static async IAsyncEnumerable<TEntity> PartitionEagerAsync<TEntity>(
+ this IOrderedQueryable<TEntity> query,
+ int partitionSize,
+ ProgressablePartitionReporting<TEntity>? progressablePartition = null,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ var iterator = 0;
+ int itemCounter;
+ var items = ArrayPool<TEntity>.Shared.Rent(partitionSize);
+ try
+ {
+ do
+ {
+ progressablePartition?.BeginPartition(iterator);
+ itemCounter = 0;
+ await foreach (var item in query
+ .Skip(partitionSize * iterator)
+ .Take(partitionSize)
+ .AsAsyncEnumerable()
+ .WithCancellation(cancellationToken)
+ .ConfigureAwait(false))
+ {
+ items[itemCounter++] = item;
+ }
+
+ for (int i = 0; i < itemCounter; i++)
+ {
+ progressablePartition?.BeginItem(items[i], iterator, itemCounter);
+ yield return items[i];
+ progressablePartition?.EndItem(items[i], iterator, itemCounter);
+ }
+
+ progressablePartition?.EndPartition(iterator);
+ iterator++;
+ } while (itemCounter == partitionSize && !cancellationToken.IsCancellationRequested);
+ }
+ finally
+ {
+ ArrayPool<TEntity>.Shared.Return(items);
+ }
+ }
+
+ /// <summary>
+ /// Adds an Index to the enumeration of the async enumerable.
+ /// </summary>
+ /// <typeparam name="TEntity">The entity to load.</typeparam>
+ /// <param name="query">The source query.</param>
+ /// <returns>The source list with an index added.</returns>
+ public static async IAsyncEnumerable<(TEntity Item, int Index)> WithIndex<TEntity>(this IAsyncEnumerable<TEntity> query)
+ {
+ var index = 0;
+ await foreach (var item in query.ConfigureAwait(false))
+ {
+ yield return (item, index++);
+ }
+ }
+}