diff --git a/CHANGELOG.md b/CHANGELOG.md
index 065e7bef..d244233d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -61,6 +61,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add two new environment variables to customize config and transcode folder locations
- `ETV_CONFIG_FOLDER`
- `ETV_TRANSCODE_FOLDER`
+- Add checkbox to allow use of B-frames in FFmpeg Profile (disabled by default)
### Fixed
- Fix some cases of 404s from Plex when files were replaced and scanning the library from ETV didn't help
diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
index d42e7e6d..62a3caa4 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
@@ -16,6 +16,7 @@ public record CreateFFmpegProfile(
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
+ bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
index 69bbe585..d59670b7 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
@@ -56,6 +56,7 @@ public class CreateFFmpegProfileHandler :
VideoFormat = request.VideoFormat,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,
+ AllowBFrames = request.AllowBFrames,
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
index 29d737ea..11f9f4b9 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
@@ -17,6 +17,7 @@ public record UpdateFFmpegProfile(
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
+ bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
index ed52669d..04041ae3 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
@@ -44,6 +44,7 @@ public class
p.VideoFormat = update.VideoFormat;
p.VideoProfile = update.VideoProfile;
p.VideoPreset = update.VideoPreset;
+ p.AllowBFrames = update.AllowBFrames;
// mpeg2video only supports 8-bit content
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
diff --git a/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs b/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
index b7764f2e..fee9855b 100644
--- a/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
@@ -17,6 +17,7 @@ public record FFmpegProfileViewModel(
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
+ bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
diff --git a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
index b0bb2d96..6a9ca88b 100644
--- a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
+++ b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs
@@ -19,6 +19,7 @@ internal static class Mapper
profile.VideoFormat,
profile.VideoProfile,
profile.VideoPreset ?? string.Empty,
+ profile.AllowBFrames,
profile.BitDepth,
profile.VideoBitrate,
profile.VideoBufferSize,
diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
index e6729d82..cf1d90ff 100644
--- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
+++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
@@ -16,7 +16,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/ErsatzTV.Core/Domain/FFmpegProfile.cs b/ErsatzTV.Core/Domain/FFmpegProfile.cs
index 3e0f4966..105f07fa 100644
--- a/ErsatzTV.Core/Domain/FFmpegProfile.cs
+++ b/ErsatzTV.Core/Domain/FFmpegProfile.cs
@@ -17,6 +17,7 @@ public record FFmpegProfile
public FFmpegProfileVideoFormat VideoFormat { get; set; }
public string VideoProfile { get; set; }
public string VideoPreset { get; set; }
+ public bool AllowBFrames { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; }
@@ -39,6 +40,7 @@ public record FFmpegProfile
VideoFormat = FFmpegProfileVideoFormat.H264,
VideoProfile = "high",
VideoPreset = ErsatzTV.FFmpeg.Preset.VideoPreset.Unset,
+ AllowBFrames = false,
AudioFormat = FFmpegProfileAudioFormat.Aac,
VideoBitrate = 2000,
VideoBufferSize = 4000,
diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
index 8e2815df..b200829c 100644
--- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
+++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
@@ -348,6 +348,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoFormat,
maybeVideoProfile,
maybeVideoPreset,
+ channel.FFmpegProfile.AllowBFrames,
Optional(playbackSettings.PixelFormat),
scaledSize,
paddedSize,
@@ -456,6 +457,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoFormat,
GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile),
VideoPreset.Unset,
+ channel.FFmpegProfile.AllowBFrames,
new PixelFormatYuv420P(),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
@@ -682,6 +684,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoFormat,
maybeVideoProfile,
maybeVideoPreset,
+ channel.FFmpegProfile.AllowBFrames,
Optional(playbackSettings.PixelFormat),
resolution,
resolution,
diff --git a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
index 5aafceb8..90e282ca 100644
--- a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
+++ b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
@@ -10,7 +10,7 @@
-
+
diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
index bcfef367..3f4679f1 100644
--- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
+++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
@@ -60,6 +60,7 @@ public class PipelineBuilderBaseTests
VideoFormat.Hevc,
VideoProfile.Main,
VideoPreset.Unset,
+ false,
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
@@ -151,6 +152,7 @@ public class PipelineBuilderBaseTests
VideoFormat.Hevc,
VideoProfile.Main,
VideoPreset.Unset,
+ false,
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
@@ -298,6 +300,7 @@ public class PipelineBuilderBaseTests
VideoFormat.Copy,
VideoProfile.Main,
VideoPreset.Unset,
+ false,
Option.None,
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
@@ -383,6 +386,7 @@ public class PipelineBuilderBaseTests
VideoFormat.Copy,
VideoProfile.Main,
VideoPreset.Unset,
+ false,
new PixelFormatYuv420P(),
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
diff --git a/ErsatzTV.FFmpeg/FrameState.cs b/ErsatzTV.FFmpeg/FrameState.cs
index f1236d41..50238c43 100644
--- a/ErsatzTV.FFmpeg/FrameState.cs
+++ b/ErsatzTV.FFmpeg/FrameState.cs
@@ -8,6 +8,7 @@ public record FrameState(
string VideoFormat,
Option VideoProfile,
Option VideoPreset,
+ bool AllowBFrames,
Option PixelFormat,
FrameSize ScaledSize,
FrameSize PaddedSize,
diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
index 6b51c7f5..2d580da0 100644
--- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
+++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
@@ -174,7 +174,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
new ClosedGopOutputOption()
};
- if (desiredState.VideoFormat != VideoFormat.Copy)
+ if (desiredState.VideoFormat != VideoFormat.Copy && !desiredState.AllowBFrames)
{
pipelineSteps.Add(new NoBFramesOutputOption());
}
diff --git a/ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj b/ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
index e9ac7358..14ac77c6 100644
--- a/ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
+++ b/ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/ErsatzTV.Infrastructure.MySql/Migrations/20240524134543_Add_FFmpegProfile_AllowBFrames.Designer.cs b/ErsatzTV.Infrastructure.MySql/Migrations/20240524134543_Add_FFmpegProfile_AllowBFrames.Designer.cs
new file mode 100644
index 00000000..8508d149
--- /dev/null
+++ b/ErsatzTV.Infrastructure.MySql/Migrations/20240524134543_Add_FFmpegProfile_AllowBFrames.Designer.cs
@@ -0,0 +1,5777 @@
+//
+using System;
+using ErsatzTV.Infrastructure.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ErsatzTV.Infrastructure.MySql.Migrations
+{
+ [DbContext(typeof(TvContext))]
+ [Migration("20240524134543_Add_FFmpegProfile_AllowBFrames")]
+ partial class Add_FFmpegProfile_AllowBFrames
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.4")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ArtworkId")
+ .HasColumnType("int");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ImageMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("Order")
+ .HasColumnType("int");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("Role")
+ .HasColumnType("longtext");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("int");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("ArtworkId")
+ .IsUnique();
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("ImageMetadataId");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ArtistId")
+ .HasColumnType("int");
+
+ b.Property("Biography")
+ .HasColumnType("longtext");
+
+ b.Property("DateAdded")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DateUpdated")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Disambiguation")
+ .HasColumnType("longtext");
+
+ b.Property("Formed")
+ .HasColumnType("longtext");
+
+ b.Property("MetadataKind")
+ .HasColumnType("int");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("longtext");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SortTitle")
+ .HasColumnType("longtext");
+
+ b.Property("Title")
+ .HasColumnType("longtext");
+
+ b.Property("Year")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistId");
+
+ b.ToTable("ArtistMetadata", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ArtworkKind")
+ .HasColumnType("int");
+
+ b.Property("BlurHash43")
+ .HasColumnType("longtext");
+
+ b.Property("BlurHash54")
+ .HasColumnType("longtext");
+
+ b.Property("BlurHash64")
+ .HasColumnType("longtext");
+
+ b.Property("ChannelId")
+ .HasColumnType("int");
+
+ b.Property("DateAdded")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DateUpdated")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ImageMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("Path")
+ .HasColumnType("longtext");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("int");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("int");
+
+ b.Property("SourcePath")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("ChannelId");
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("ImageMetadataId");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Categories")
+ .HasColumnType("longtext");
+
+ b.Property("FFmpegProfileId")
+ .HasColumnType("int");
+
+ b.Property("FallbackFillerId")
+ .HasColumnType("int");
+
+ b.Property("Group")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasColumnType("longtext")
+ .HasDefaultValue("ErsatzTV");
+
+ b.Property("MusicVideoCreditsMode")
+ .HasColumnType("int");
+
+ b.Property("MusicVideoCreditsTemplate")
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("Number")
+ .HasColumnType("varchar(255)");
+
+ b.Property("PreferredAudioLanguageCode")
+ .HasColumnType("longtext");
+
+ b.Property("PreferredAudioTitle")
+ .HasColumnType("longtext");
+
+ b.Property("PreferredSubtitleLanguageCode")
+ .HasColumnType("longtext");
+
+ b.Property("StreamingMode")
+ .HasColumnType("int");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("int");
+
+ b.Property("UniqueId")
+ .HasColumnType("char(36)");
+
+ b.Property("WatermarkId")
+ .HasColumnType("int");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DurationSeconds")
+ .HasColumnType("int");
+
+ b.Property("FrequencyMinutes")
+ .HasColumnType("int");
+
+ b.Property("HorizontalMarginPercent")
+ .HasColumnType("int");
+
+ b.Property("Image")
+ .HasColumnType("longtext");
+
+ b.Property("ImageSource")
+ .HasColumnType("int");
+
+ b.Property("Location")
+ .HasColumnType("int");
+
+ b.Property("Mode")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("Opacity")
+ .HasColumnType("int");
+
+ b.Property("PlaceWithinSourceContent")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Size")
+ .HasColumnType("int");
+
+ b.Property("VerticalMarginPercent")
+ .HasColumnType("int");
+
+ b.Property("WidthPercent")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("ChannelWatermark", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("UseCustomPlaybackOrder")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Collection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b =>
+ {
+ b.Property("CollectionId")
+ .HasColumnType("int");
+
+ b.Property("MediaItemId")
+ .HasColumnType("int");
+
+ b.Property("CustomIndex")
+ .HasColumnType("int");
+
+ b.HasKey("CollectionId", "MediaItemId");
+
+ b.HasIndex("MediaItemId");
+
+ b.ToTable("CollectionItem", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Key")
+ .HasColumnType("varchar(255)");
+
+ b.Property("Value")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Key")
+ .IsUnique();
+
+ b.ToTable("ConfigElement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Director", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("int");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Etag")
+ .HasColumnType("longtext");
+
+ b.Property("ItemId")
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("EmbyCollection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("longtext");
+
+ b.Property("EmbyMediaSourceId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmbyMediaSourceId");
+
+ b.ToTable("EmbyConnection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyPathReplacement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("EmbyMediaSourceId")
+ .HasColumnType("int");
+
+ b.Property("EmbyPath")
+ .HasColumnType("longtext");
+
+ b.Property("LocalPath")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EmbyMediaSourceId");
+
+ b.ToTable("EmbyPathReplacement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DateAdded")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DateUpdated")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EpisodeId")
+ .HasColumnType("int");
+
+ b.Property("EpisodeNumber")
+ .HasColumnType("int");
+
+ b.Property("MetadataKind")
+ .HasColumnType("int");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("longtext");
+
+ b.Property("Outline")
+ .HasColumnType("longtext");
+
+ b.Property("Plot")
+ .HasColumnType("longtext");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SortTitle")
+ .HasColumnType("longtext");
+
+ b.Property("Tagline")
+ .HasColumnType("longtext");
+
+ b.Property("Title")
+ .HasColumnType("longtext");
+
+ b.Property("Year")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EpisodeId");
+
+ b.ToTable("EpisodeMetadata", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AllowBFrames")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("AudioBitrate")
+ .HasColumnType("int");
+
+ b.Property("AudioBufferSize")
+ .HasColumnType("int");
+
+ b.Property("AudioChannels")
+ .HasColumnType("int");
+
+ b.Property("AudioFormat")
+ .HasColumnType("int");
+
+ b.Property("AudioSampleRate")
+ .HasColumnType("int");
+
+ b.Property("BitDepth")
+ .HasColumnType("int");
+
+ b.Property("DeinterlaceVideo")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("HardwareAcceleration")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("NormalizeFramerate")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(false);
+
+ b.Property("NormalizeLoudnessMode")
+ .HasColumnType("int");
+
+ b.Property("QsvExtraHardwareFrames")
+ .HasColumnType("int");
+
+ b.Property("ResolutionId")
+ .HasColumnType("int");
+
+ b.Property("ScalingBehavior")
+ .HasColumnType("int");
+
+ b.Property("ThreadCount")
+ .HasColumnType("int");
+
+ b.Property("VaapiDevice")
+ .HasColumnType("longtext");
+
+ b.Property("VaapiDriver")
+ .HasColumnType("int");
+
+ b.Property("VideoBitrate")
+ .HasColumnType("int");
+
+ b.Property("VideoBufferSize")
+ .HasColumnType("int");
+
+ b.Property("VideoFormat")
+ .HasColumnType("int");
+
+ b.Property("VideoPreset")
+ .HasColumnType("longtext");
+
+ b.Property("VideoProfile")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ResolutionId");
+
+ b.ToTable("FFmpegProfile", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Filler.FillerPreset", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("AllowWatermarks")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("CollectionId")
+ .HasColumnType("int");
+
+ b.Property("CollectionType")
+ .HasColumnType("int");
+
+ b.Property("Count")
+ .HasColumnType("int");
+
+ b.Property("Duration")
+ .HasColumnType("time(6)");
+
+ b.Property("FillerKind")
+ .HasColumnType("int");
+
+ b.Property("FillerMode")
+ .HasColumnType("int");
+
+ b.Property("MediaItemId")
+ .HasColumnType("int");
+
+ b.Property("MultiCollectionId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("PadToNearestMinute")
+ .HasColumnType("int");
+
+ b.Property("SmartCollectionId")
+ .HasColumnType("int");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ArtistMetadataId")
+ .HasColumnType("int");
+
+ b.Property("EpisodeMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ImageMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MovieMetadataId")
+ .HasColumnType("int");
+
+ b.Property("MusicVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("OtherVideoMetadataId")
+ .HasColumnType("int");
+
+ b.Property("SeasonMetadataId")
+ .HasColumnType("int");
+
+ b.Property("ShowMetadataId")
+ .HasColumnType("int");
+
+ b.Property("SongMetadataId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ArtistMetadataId");
+
+ b.HasIndex("EpisodeMetadataId");
+
+ b.HasIndex("ImageMetadataId");
+
+ 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.ImageFolderDuration", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DurationSeconds")
+ .HasColumnType("double");
+
+ b.Property("LibraryFolderId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryFolderId")
+ .IsUnique();
+
+ b.ToTable("ImageFolderDuration", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.ImageMetadata", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("DateAdded")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DateUpdated")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DurationSeconds")
+ .HasColumnType("double");
+
+ b.Property("ImageId")
+ .HasColumnType("int");
+
+ b.Property("MetadataKind")
+ .HasColumnType("int");
+
+ b.Property("OriginalTitle")
+ .HasColumnType("longtext");
+
+ b.Property("ReleaseDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("SortTitle")
+ .HasColumnType("longtext");
+
+ b.Property("Title")
+ .HasColumnType("longtext");
+
+ b.Property("Year")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ImageId");
+
+ b.ToTable("ImageMetadata", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Etag")
+ .HasColumnType("longtext");
+
+ b.Property("ItemId")
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("JellyfinCollection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("longtext");
+
+ b.Property("JellyfinMediaSourceId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JellyfinMediaSourceId");
+
+ b.ToTable("JellyfinConnection", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinPathReplacement", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("JellyfinMediaSourceId")
+ .HasColumnType("int");
+
+ b.Property("JellyfinPath")
+ .HasColumnType("longtext");
+
+ b.Property("LocalPath")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("JellyfinMediaSourceId");
+
+ b.ToTable("JellyfinPathReplacement", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.LanguageCode", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("EnglishName")
+ .HasColumnType("longtext");
+
+ b.Property("FrenchName")
+ .HasColumnType("longtext");
+
+ b.Property("ThreeCode1")
+ .HasColumnType("longtext");
+
+ b.Property("ThreeCode2")
+ .HasColumnType("longtext");
+
+ b.Property("TwoCode")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.ToTable("LanguageCode", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LastScan")
+ .HasColumnType("datetime(6)");
+
+ b.Property("MediaKind")
+ .HasColumnType("int");
+
+ b.Property("MediaSourceId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("Etag")
+ .HasColumnType("longtext");
+
+ b.Property("LibraryPathId")
+ .HasColumnType("int");
+
+ b.Property("ParentId")
+ .HasColumnType("int");
+
+ b.Property("Path")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryPathId");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("LibraryFolder", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LastScan")
+ .HasColumnType("datetime(6)");
+
+ b.Property("LibraryId")
+ .HasColumnType("int");
+
+ b.Property("Path")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryId");
+
+ b.ToTable("LibraryPath", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaChapter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("ChapterId")
+ .HasColumnType("bigint");
+
+ b.Property("EndTime")
+ .HasColumnType("time(6)");
+
+ b.Property("MediaVersionId")
+ .HasColumnType("int");
+
+ b.Property("StartTime")
+ .HasColumnType("time(6)");
+
+ b.Property("Title")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaVersionId");
+
+ b.ToTable("MediaChapter", (string)null);
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LibraryFolderId")
+ .HasColumnType("int");
+
+ b.Property("MediaVersionId")
+ .HasColumnType("int");
+
+ b.Property("Path")
+ .HasColumnType("varchar(255)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("LibraryFolderId");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property("LibraryPathId")
+ .HasColumnType("int");
+
+ b.Property("State")
+ .HasColumnType("int");
+
+ 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("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.HasKey("Id");
+
+ b.ToTable("MediaSource", (string)null);
+
+ b.UseTptMappingStrategy();
+ });
+
+ modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id"));
+
+ b.Property