diff --git a/CHANGELOG.md b/CHANGELOG.md
index 843082fdc..327a2d9a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Added
+- Add custom resolution management to `Settings` page
+
### Fixed
- Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable
@@ -13,7 +16,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- A minimal UI will indicate when the database and search index are initializing
- The UI will automatically refresh when the initialization processes have completed
-
## [0.8.0-beta] - 2023-06-23
### Added
- Disable playout buttons and show spinning indicator when a playout is being modified (built/extended, or subtitles are being extracted)
diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
index fc08ec0b1..0dc000387 100644
--- a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
+++ b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
@@ -33,6 +33,7 @@
True
True
True
+ True
True
True
True
diff --git a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
index 2f8c821e5..0fa1250a0 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
@@ -1,5 +1,4 @@
-using ErsatzTV.Application.Resolutions;
-using ErsatzTV.Core.Api.FFmpegProfiles;
+using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
@@ -15,7 +14,7 @@ internal static class Mapper
profile.VaapiDriver,
profile.VaapiDevice,
profile.QsvExtraHardwareFrames,
- Project(profile.Resolution),
+ Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
profile.VideoFormat,
profile.BitDepth,
profile.VideoBitrate,
@@ -57,7 +56,4 @@ internal static class Mapper
ffmpegProfile.AudioSampleRate,
ffmpegProfile.NormalizeFramerate,
ffmpegProfile.DeinterlaceVideo);
-
- private static ResolutionViewModel Project(Resolution resolution) =>
- new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}
diff --git a/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolution.cs b/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolution.cs
new file mode 100644
index 000000000..ee89ba417
--- /dev/null
+++ b/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolution.cs
@@ -0,0 +1,5 @@
+using ErsatzTV.Core;
+
+namespace ErsatzTV.Application.Resolutions;
+
+public record CreateCustomResolution(int Width, int Height) : IRequest>;
diff --git a/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolutionHandler.cs b/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolutionHandler.cs
new file mode 100644
index 000000000..c32cc4e8e
--- /dev/null
+++ b/ErsatzTV.Application/Resolutions/Commands/CreateCustomResolutionHandler.cs
@@ -0,0 +1,76 @@
+using ErsatzTV.Core;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Application.Resolutions;
+
+public class CreateCustomResolutionHandler : IRequestHandler>
+{
+ private readonly IDbContextFactory _dbContextFactory;
+
+ public CreateCustomResolutionHandler(IDbContextFactory dbContextFactory)
+ {
+ _dbContextFactory = dbContextFactory;
+ }
+
+ public async Task> Handle(CreateCustomResolution request, CancellationToken cancellationToken)
+ {
+ await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
+ Validation validation = await Validate(dbContext, request);
+ return await validation.Match(
+ r => PersistResolution(dbContext, r, cancellationToken),
+ error => Task.FromResult>(error.Join()));
+ }
+
+ private static async Task > PersistResolution(
+ TvContext dbContext,
+ Resolution resolution,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ await dbContext.Resolutions.AddAsync(resolution, cancellationToken);
+ await dbContext.SaveChangesAsync(cancellationToken);
+ return Option.None;
+ }
+ catch (Exception ex)
+ {
+ return BaseError.New(ex.Message);
+ }
+ }
+
+ private static Task> Validate(
+ TvContext dbContext,
+ CreateCustomResolution request) =>
+ ResolutionMustBeUnique(dbContext, request)
+ .MapT(
+ _ => new Resolution
+ {
+ Name = $"{request.Width}x{request.Height}",
+ Width = request.Width,
+ Height = request.Height,
+ IsCustom = true
+ });
+
+ private static async Task> ResolutionMustBeUnique(
+ TvContext dbContext,
+ CreateCustomResolution request)
+ {
+ Option maybeExisting = await dbContext.Resolutions
+ .FirstOrDefaultAsync(r => r.Height == request.Height && r.Width == request.Width)
+ .Map(Optional);
+
+ if (maybeExisting.IsSome)
+ {
+ return BaseError.New("Resolution width and height must be unique");
+ }
+
+ if (request.Height <= 0 || request.Width <= 0)
+ {
+ return BaseError.New("Resolution width or height is invalid");
+ }
+
+ return Unit.Default;
+ }
+}
diff --git a/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolution.cs b/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolution.cs
new file mode 100644
index 000000000..2eb0c33e3
--- /dev/null
+++ b/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolution.cs
@@ -0,0 +1,5 @@
+using ErsatzTV.Core;
+
+namespace ErsatzTV.Application.Resolutions;
+
+public record DeleteCustomResolution(int ResolutionId) : IRequest>;
diff --git a/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolutionHandler.cs b/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolutionHandler.cs
new file mode 100644
index 000000000..6853e68af
--- /dev/null
+++ b/ErsatzTV.Application/Resolutions/Commands/DeleteCustomResolutionHandler.cs
@@ -0,0 +1,40 @@
+using Dapper;
+using ErsatzTV.Core;
+using ErsatzTV.Core.Domain;
+using ErsatzTV.Infrastructure.Data;
+using ErsatzTV.Infrastructure.Extensions;
+using Microsoft.EntityFrameworkCore;
+
+namespace ErsatzTV.Application.Resolutions;
+
+public class DeleteCustomResolutionHandler : IRequestHandler>
+{
+ private readonly IDbContextFactory _dbContextFactory;
+
+ public DeleteCustomResolutionHandler(IDbContextFactory dbContextFactory) =>
+ _dbContextFactory = dbContextFactory;
+
+ public async Task> Handle(DeleteCustomResolution request, CancellationToken cancellationToken)
+ {
+ await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
+
+ Option maybeResolution = await dbContext.Resolutions
+ .AsNoTracking()
+ .SelectOneAsync(p => p.Id, p => p.Id == request.ResolutionId && p.IsCustom == true);
+
+ foreach (Resolution resolution in maybeResolution)
+ {
+ // reset any ffmpeg profiles using this resolution to 1920x1080
+ await dbContext.Connection.ExecuteAsync(
+ @"UPDATE FFmpegProfile SET ResolutionId = 3 WHERE ResolutionId = @ResolutionId",
+ new { request.ResolutionId });
+
+ dbContext.Resolutions.Remove(resolution);
+ await dbContext.SaveChangesAsync(cancellationToken);
+ }
+
+ return maybeResolution.IsNone
+ ? BaseError.New($"Resolution {request.ResolutionId} does not exist.")
+ : Option.None;
+ }
+}
diff --git a/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs b/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs
index 3cfabf46e..de1514f80 100644
--- a/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs
+++ b/ErsatzTV.Application/Resolutions/FFmpegProfileResolutionViewModel.cs
@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Resolutions;
-public record ResolutionViewModel(int Id, string Name, int Width, int Height);
+public record ResolutionViewModel(int Id, string Name, int Width, int Height, bool IsCustom);
diff --git a/ErsatzTV.Application/Resolutions/Mapper.cs b/ErsatzTV.Application/Resolutions/Mapper.cs
index 7bba76128..1a8504ac3 100644
--- a/ErsatzTV.Application/Resolutions/Mapper.cs
+++ b/ErsatzTV.Application/Resolutions/Mapper.cs
@@ -5,5 +5,5 @@ namespace ErsatzTV.Application.Resolutions;
internal static class Mapper
{
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
- new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
+ new(resolution.Id, resolution.Name, resolution.Width, resolution.Height, resolution.IsCustom);
}
diff --git a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs
index 7eabf4fc6..7bc34b411 100644
--- a/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs
+++ b/ErsatzTV.Application/Resolutions/Queries/GetAllResolutionsHandler.cs
@@ -15,9 +15,9 @@ public class GetAllResolutionsHandler : IRequestHandler list.Map(ProjectToViewModel).ToList());
+ .Map(list => list.OrderBy(r => r.Width).ThenBy(r => r.Height).Map(ProjectToViewModel).ToList());
}
}
diff --git a/ErsatzTV.Core/Domain/Resolution.cs b/ErsatzTV.Core/Domain/Resolution.cs
index 7921b16a8..131b882b7 100644
--- a/ErsatzTV.Core/Domain/Resolution.cs
+++ b/ErsatzTV.Core/Domain/Resolution.cs
@@ -8,6 +8,7 @@ public class Resolution : IDisplaySize
public string Name { get; set; }
public int Height { get; set; }
public int Width { get; set; }
+ public bool IsCustom { get; set; }
public override string ToString() => $"{Width}x{Height}";
}
diff --git a/ErsatzTV.Infrastructure/Data/Configurations/ResolutionConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/ResolutionConfiguration.cs
index c65fa54e0..af128b249 100644
--- a/ErsatzTV.Infrastructure/Data/Configurations/ResolutionConfiguration.cs
+++ b/ErsatzTV.Infrastructure/Data/Configurations/ResolutionConfiguration.cs
@@ -6,5 +6,10 @@ namespace ErsatzTV.Infrastructure.Data.Configurations;
public class ResolutionConfiguration : IEntityTypeConfiguration
{
- public void Configure(EntityTypeBuilder builder) => builder.ToTable("Resolution");
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("Resolution");
+
+ builder.Property(r => r.IsCustom).HasDefaultValue(false);
+ }
}
diff --git a/ErsatzTV.Infrastructure/Migrations/20230625130236_Add_Resolution_IsCustom.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20230625130236_Add_Resolution_IsCustom.Designer.cs
new file mode 100644
index 000000000..e5b811511
--- /dev/null
+++ b/ErsatzTV.Infrastructure/Migrations/20230625130236_Add_Resolution_IsCustom.Designer.cs
@@ -0,0 +1,4424 @@
+//
+using System;
+using ErsatzTV.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ErsatzTV.Infrastructure.Migrations
+{
+ [DbContext(typeof(TvContext))]
+ [Migration("20230625130236_Add_Resolution_IsCustom")]
+ partial class Add_Resolution_IsCustom
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtworkId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Role")
+ .HasColumnType("TEXT");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("ArtworkId")
+ .IsUnique();
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("MovieMetadataId");
+
+ b.HasIndex("MusicVideoMetadataId");
+
+ b.HasIndex("OtherVideoMetadataId");
+
+ b.HasIndex("SeasonMetadataId");
+
+ b.HasIndex("ShowMetadataId");
+
+ b.HasIndex("SongMetadataId");
+
+ b.ToTable("Actor", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.ArtistMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtistId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Biography")
+ .HasColumnType("TEXT");
+
+ b.Property("DateAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("Disambiguation")
+ .HasColumnType("TEXT");
+
+ b.Property("Formed")
+ .HasColumnType("TEXT");
+
+ b.Property("MetadataKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("SortTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Year")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistId");
+
+ b.ToTable("ArtistMetadata", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtworkKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("BlurHash43")
+ .HasColumnType("TEXT");
+
+ b.Property("BlurHash54")
+ .HasColumnType("TEXT");
+
+ b.Property("BlurHash64")
+ .HasColumnType("TEXT");
+
+ b.Property("ChannelId")
+ .HasColumnType("INTEGER");
+
+ b.Property("DateAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SourcePath")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("ChannelId");
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("MovieMetadataId");
+
+ b.HasIndex("MusicVideoMetadataId");
+
+ b.HasIndex("OtherVideoMetadataId");
+
+ b.HasIndex("SeasonMetadataId");
+
+ b.HasIndex("ShowMetadataId");
+
+ b.HasIndex("SongMetadataId");
+
+ b.ToTable("Artwork", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Categories")
+ .HasColumnType("TEXT");
+
+ b.Property("FFmpegProfileId")
+ .HasColumnType("INTEGER");
+
+ b.Property("FallbackFillerId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Group")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasDefaultValue("ErsatzTV");
+
+ b.Property("MusicVideoCreditsMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoCreditsTemplate")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Number")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredAudioLanguageCode")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredAudioTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("PreferredSubtitleLanguageCode")
+ .HasColumnType("TEXT");
+
+ b.Property("StreamingMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("UniqueId")
+ .HasColumnType("TEXT");
+
+ b.Property("WatermarkId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FFmpegProfileId");
+
+ b.HasIndex("FallbackFillerId");
+
+ b.HasIndex("Number")
+ .IsUnique();
+
+ b.HasIndex("WatermarkId");
+
+ b.ToTable("Channel", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.ChannelWatermark", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DurationSeconds")
+ .HasColumnType("INTEGER");
+
+ b.Property("FrequencyMinutes")
+ .HasColumnType("INTEGER");
+
+ b.Property("HorizontalMarginPercent")
+ .HasColumnType("INTEGER");
+
+ b.Property("Image")
+ .HasColumnType("TEXT");
+
+ b.Property("ImageSource")
+ .HasColumnType("INTEGER");
+
+ b.Property("Location")
+ .HasColumnType("INTEGER");
+
+ b.Property("Mode")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("Opacity")
+ .HasColumnType("INTEGER");
+
+ b.Property("PlaceWithinSourceContent")
+ .HasColumnType("INTEGER");
+
+ b.Property("Size")
+ .HasColumnType("INTEGER");
+
+ b.Property("VerticalMarginPercent")
+ .HasColumnType("INTEGER");
+
+ b.Property("WidthPercent")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("ChannelWatermark", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("UseCustomPlaybackOrder")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("Collection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MediaItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomIndex")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("CollectionId", "MediaItemId");
+
+ b.HasIndex("MediaItemId");
+
+ b.ToTable("CollectionItem", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Key")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Key")
+ .IsUnique();
+
+ b.ToTable("ConfigElement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("MovieMetadataId");
+
+ b.HasIndex("MusicVideoMetadataId");
+
+ b.HasIndex("OtherVideoMetadataId");
+
+ b.ToTable("Director", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Etag")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("EmbyCollection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Address")
+ .HasColumnType("TEXT");
+
+ b.Property("EmbyMediaSourceId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmbyMediaSourceId");
+
+ b.ToTable("EmbyConnection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EmbyMediaSourceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EmbyPath")
+ .HasColumnType("TEXT");
+
+ b.Property("LocalPath")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmbyMediaSourceId");
+
+ b.ToTable("EmbyPathReplacement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("EpisodeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EpisodeNumber")
+ .HasColumnType("INTEGER");
+
+ b.Property("MetadataKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Outline")
+ .HasColumnType("TEXT");
+
+ b.Property("Plot")
+ .HasColumnType("TEXT");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("TEXT");
+
+ b.Property("SortTitle")
+ .HasColumnType("TEXT");
+
+ b.Property("Tagline")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.Property("Year")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EpisodeId");
+
+ b.ToTable("EpisodeMetadata", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AudioBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property("AudioBufferSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("AudioChannels")
+ .HasColumnType("INTEGER");
+
+ b.Property("AudioFormat")
+ .HasColumnType("INTEGER");
+
+ b.Property("AudioSampleRate")
+ .HasColumnType("INTEGER");
+
+ b.Property("BitDepth")
+ .HasColumnType("INTEGER");
+
+ b.Property("DeinterlaceVideo")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true);
+
+ b.Property("HardwareAcceleration")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("NormalizeFramerate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(false);
+
+ b.Property("NormalizeLoudness")
+ .HasColumnType("INTEGER");
+
+ b.Property("QsvExtraHardwareFrames")
+ .HasColumnType("INTEGER");
+
+ b.Property("ResolutionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThreadCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("VaapiDevice")
+ .HasColumnType("TEXT");
+
+ b.Property("VaapiDriver")
+ .HasColumnType("INTEGER");
+
+ b.Property("VideoBitrate")
+ .HasColumnType("INTEGER");
+
+ b.Property("VideoBufferSize")
+ .HasColumnType("INTEGER");
+
+ b.Property("VideoFormat")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ResolutionId");
+
+ b.ToTable("FFmpegProfile", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AllowWatermarks")
+ .HasColumnType("INTEGER");
+
+ b.Property("CollectionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("CollectionType")
+ .HasColumnType("INTEGER");
+
+ b.Property("Count")
+ .HasColumnType("INTEGER");
+
+ b.Property("Duration")
+ .HasColumnType("TEXT");
+
+ b.Property("FillerKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("FillerMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("MediaItemId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MultiCollectionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("PadToNearestMinute")
+ .HasColumnType("INTEGER");
+
+ b.Property("SmartCollectionId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CollectionId");
+
+ b.HasIndex("MediaItemId");
+
+ b.HasIndex("MultiCollectionId");
+
+ b.HasIndex("SmartCollectionId");
+
+ b.ToTable("FillerPreset", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("MovieMetadataId");
+
+ b.HasIndex("MusicVideoMetadataId");
+
+ b.HasIndex("OtherVideoMetadataId");
+
+ b.HasIndex("SeasonMetadataId");
+
+ b.HasIndex("ShowMetadataId");
+
+ b.HasIndex("SongMetadataId");
+
+ b.ToTable("Genre");
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Etag")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("JellyfinCollection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Address")
+ .HasColumnType("TEXT");
+
+ b.Property("JellyfinMediaSourceId")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JellyfinMediaSourceId");
+
+ b.ToTable("JellyfinConnection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("JellyfinMediaSourceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("JellyfinPath")
+ .HasColumnType("TEXT");
+
+ b.Property("LocalPath")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JellyfinMediaSourceId");
+
+ b.ToTable("JellyfinPathReplacement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("EnglishName")
+ .HasColumnType("TEXT");
+
+ b.Property("FrenchName")
+ .HasColumnType("TEXT");
+
+ b.Property("ThreeCode1")
+ .HasColumnType("TEXT");
+
+ b.Property("ThreeCode2")
+ .HasColumnType("TEXT");
+
+ b.Property("TwoCode")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("LanguageCode", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScan")
+ .HasColumnType("TEXT");
+
+ b.Property("MediaKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("MediaSourceId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaSourceId");
+
+ b.ToTable("Library", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryFolder", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Etag")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryPathId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryPathId");
+
+ b.ToTable("LibraryFolder", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastScan")
+ .HasColumnType("TEXT");
+
+ b.Property("LibraryId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("LibraryPath", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChapterId")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndTime")
+ .HasColumnType("TEXT");
+
+ b.Property("MediaVersionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("StartTime")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaVersionId");
+
+ b.ToTable("MediaChapter", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("MediaVersionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Path")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaVersionId");
+
+ b.HasIndex("Path")
+ .IsUnique();
+
+ b.ToTable("MediaFile", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LibraryPathId")
+ .HasColumnType("INTEGER");
+
+ b.Property("State")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryPathId");
+
+ b.ToTable("MediaItem", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSource", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AttachedPic")
+ .HasColumnType("INTEGER");
+
+ b.Property("BitsPerRawSample")
+ .HasColumnType("INTEGER");
+
+ b.Property("Channels")
+ .HasColumnType("INTEGER");
+
+ b.Property("Codec")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorPrimaries")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorRange")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorSpace")
+ .HasColumnType("TEXT");
+
+ b.Property("ColorTransfer")
+ .HasColumnType("TEXT");
+
+ b.Property("Default")
+ .HasColumnType("INTEGER");
+
+ b.Property("FileName")
+ .HasColumnType("TEXT");
+
+ b.Property("Forced")
+ .HasColumnType("INTEGER");
+
+ b.Property("Index")
+ .HasColumnType("INTEGER");
+
+ b.Property("Language")
+ .HasColumnType("TEXT");
+
+ b.Property("MediaStreamKind")
+ .HasColumnType("INTEGER");
+
+ b.Property("MediaVersionId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MimeType")
+ .HasColumnType("TEXT");
+
+ b.Property("PixelFormat")
+ .HasColumnType("TEXT");
+
+ b.Property("Profile")
+ .HasColumnType("TEXT");
+
+ b.Property("Title")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaVersionId");
+
+ b.ToTable("MediaStream", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateAdded")
+ .HasColumnType("TEXT");
+
+ b.Property("DateUpdated")
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayAspectRatio")
+ .HasColumnType("TEXT");
+
+ b.Property("Duration")
+ .HasColumnType("TEXT");
+
+ b.Property("EpisodeId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("MovieId")
+ .HasColumnType("INTEGER");
+
+ b.Property("MusicVideoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .HasColumnType("TEXT");
+
+ b.Property("OtherVideoId")
+ .HasColumnType("INTEGER");
+
+ b.Property("RFrameRate")
+ .HasColumnType("TEXT");
+
+ b.Property