From 6b44873474f59a2c6ce95bcc13d5a1fbce0d18e4 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 4 Apr 2021 10:44:10 -0500 Subject: [PATCH] add library scan progress detail (#133) * add library scan progress detail * scan plex libraries on plex thread --- .../Commands/CreateFFmpegProfileHandler.cs | 2 +- .../MediaSources/Commands/ScanLocalLibrary.cs | 9 - .../Commands/ScanLocalLibraryHandler.cs | 80 +- .../Commands/SynchronizePlexLibraryById.cs | 2 +- .../SynchronizePlexLibraryByIdHandler.cs | 5 +- .../Metadata/MovieFolderScannerTests.cs | 51 +- ErsatzTV.Core/Domain/Library/LibraryPath.cs | 4 +- ErsatzTV.Core/ErsatzTV.Core.csproj | 1 + ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs | 1 + .../Metadata/IMovieFolderScanner.cs | 7 +- .../Metadata/IMusicVideoFolderScanner.cs | 7 +- .../Metadata/ITelevisionFolderScanner.cs | 7 +- .../Repositories/ILibraryRepository.cs | 1 + ErsatzTV.Core/Metadata/LibraryScanProgress.cs | 6 + ErsatzTV.Core/Metadata/MovieFolderScanner.cs | 18 +- .../Metadata/MusicVideoFolderScanner.cs | 27 +- .../Metadata/TelevisionFolderScanner.cs | 15 +- ErsatzTV.Core/Plex/PlexLibraryScanner.cs | 9 +- ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs | 22 +- .../Plex/PlexTelevisionLibraryScanner.cs | 12 +- .../Data/Repositories/LibraryRepository.cs | 4 + .../Data/Repositories/MusicVideoRepository.cs | 2 + ...21830_Add_LibraryPath_LastScan.Designer.cs | 1964 +++++++++++++++++ ...20210404121830_Add_LibraryPath_LastScan.cs | 20 + ...ryPathLastScan_LibraryLastScan.Designer.cs | 1964 +++++++++++++++++ ...ate_LibraryPathLastScan_LibraryLastScan.cs | 18 + .../Migrations/TvContextModelSnapshot.cs | 458 ++-- ErsatzTV/ErsatzTV.csproj | 1 + ErsatzTV/Pages/CollectionItems.razor | 15 +- ErsatzTV/Pages/FFmpegEditor.razor | 4 +- ErsatzTV/Pages/Libraries.razor | 55 +- ErsatzTV/Pages/LocalLibraryPathEditor.razor | 2 +- ErsatzTV/Pages/MusicVideoList.razor | 5 +- ErsatzTV/Pages/PlexLibrariesEditor.razor | 2 +- ErsatzTV/Pages/Search.razor | 11 +- ErsatzTV/Services/PlexService.cs | 56 +- ErsatzTV/Services/SchedulerService.cs | 17 +- ErsatzTV/Services/WorkerService.cs | 14 - ErsatzTV/Startup.cs | 2 + 39 files changed, 4637 insertions(+), 263 deletions(-) create mode 100644 ErsatzTV.Core/Metadata/LibraryScanProgress.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs create mode 100644 ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs index 5e5042c56..57ab9cbc2 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs @@ -50,7 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands AudioCodec = request.AudioCodec, AudioBitrate = request.AudioBitrate, AudioBufferSize = request.AudioBufferSize, - NormalizeLoudness= request.NormalizeLoudness, + NormalizeLoudness = request.NormalizeLoudness, AudioChannels = request.AudioChannels, AudioSampleRate = request.AudioSampleRate, NormalizeAudio = request.NormalizeAudio, diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs index b4bd2e74f..da4999e12 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs @@ -8,24 +8,15 @@ namespace ErsatzTV.Application.MediaSources.Commands { int LibraryId { get; } bool ForceScan { get; } - bool Rescan { get; } } public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary { public bool ForceScan => false; - public bool Rescan => false; } public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary { public bool ForceScan => true; - public bool Rescan => false; - } - - public record ForceRescanLocalLibrary(int LibraryId) : IScanLocalLibrary - { - public bool ForceScan => true; - public bool Rescan => true; } } diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs index 38b016433..f58a10790 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Metadata; using LanguageExt; using MediatR; using Microsoft.Extensions.Logging; @@ -16,13 +17,13 @@ using Unit = LanguageExt.Unit; namespace ErsatzTV.Application.MediaSources.Commands { public class ScanLocalLibraryHandler : IRequestHandler>, - IRequestHandler>, - IRequestHandler> + IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly IEntityLocker _entityLocker; private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly IMovieFolderScanner _movieFolderScanner; private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly ITelevisionFolderScanner _televisionFolderScanner; @@ -34,6 +35,7 @@ namespace ErsatzTV.Application.MediaSources.Commands ITelevisionFolderScanner televisionFolderScanner, IMusicVideoFolderScanner musicVideoFolderScanner, IEntityLocker entityLocker, + IMediator mediator, ILogger logger) { _libraryRepository = libraryRepository; @@ -42,13 +44,10 @@ namespace ErsatzTV.Application.MediaSources.Commands _televisionFolderScanner = televisionFolderScanner; _musicVideoFolderScanner = musicVideoFolderScanner; _entityLocker = entityLocker; + _mediator = mediator; _logger = logger; } - public Task> Handle( - ForceRescanLocalLibrary request, - CancellationToken cancellationToken) => Handle(request); - public Task> Handle( ForceScanLocalLibrary request, CancellationToken cancellationToken) => Handle(request); @@ -65,48 +64,64 @@ namespace ErsatzTV.Application.MediaSources.Commands private async Task PerformScan(RequestParameters parameters) { - (LocalLibrary localLibrary, string ffprobePath, bool forceScan, bool rescan) = parameters; + (LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters; - var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero); - if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) + var sw = new Stopwatch(); + sw.Start(); + + for (var i = 0; i < localLibrary.Paths.Count; i++) { - var sw = new Stopwatch(); - sw.Start(); + LibraryPath libraryPath = localLibrary.Paths[i]; - DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan; + decimal progressMin = (decimal) i / localLibrary.Paths.Count; + decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count; - foreach (LibraryPath libraryPath in localLibrary.Paths) + var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero); + if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) { switch (localLibrary.MediaKind) { case LibraryMediaKind.Movies: - await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); + await _movieFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + lastScan, + progressMin, + progressMax); break; case LibraryMediaKind.Shows: - await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); + await _televisionFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + lastScan, + progressMin, + progressMax); break; case LibraryMediaKind.MusicVideos: - await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); + await _musicVideoFolderScanner.ScanFolder( + libraryPath, + ffprobePath, + lastScan, + progressMin, + progressMax); break; } - } - localLibrary.LastScan = DateTime.UtcNow; - await _libraryRepository.UpdateLastScan(localLibrary); + libraryPath.LastScan = DateTime.UtcNow; + await _libraryRepository.UpdateLastScan(libraryPath); + } - sw.Stop(); - _logger.LogDebug( - "Scan of library {Name} completed in {Duration}", - localLibrary.Name, - TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); - } - else - { - _logger.LogDebug( - "Skipping unforced scan of library {Name}", - localLibrary.Name); + await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax)); } + sw.Stop(); + _logger.LogDebug( + "Scan of library {Name} completed in {Duration}", + localLibrary.Name, + TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds)); + + await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0)); + _entityLocker.UnlockLibrary(localLibrary.Id); return Unit.Default; } @@ -117,8 +132,7 @@ namespace ErsatzTV.Application.MediaSources.Commands (library, ffprobePath) => new RequestParameters( library, ffprobePath, - request.ForceScan, - request.Rescan)); + request.ForceScan)); private Task> LocalLibraryMustExist( IScanLocalLibrary request) => @@ -133,6 +147,6 @@ namespace ErsatzTV.Application.MediaSources.Commands ffprobePath => ffprobePath.ToValidation("FFprobe path does not exist on the file system")); - private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan); + private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan); } } diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs index 80c612a4c..4a979d047 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs @@ -4,7 +4,7 @@ using MediatR; namespace ErsatzTV.Application.Plex.Commands { - public interface ISynchronizePlexLibraryById : IRequest>, IBackgroundServiceRequest + public interface ISynchronizePlexLibraryById : IRequest>, IPlexBackgroundServiceRequest { int PlexLibraryId { get; } bool ForceScan { get; } diff --git a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs index b78518025..ca98b9090 100644 --- a/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs +++ b/ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs @@ -20,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands IRequestHandler> { private readonly IEntityLocker _entityLocker; + private readonly ILibraryRepository _libraryRepository; private readonly ILogger _logger; private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner; @@ -31,6 +32,7 @@ namespace ErsatzTV.Application.Plex.Commands IPlexSecretStore plexSecretStore, IPlexMovieLibraryScanner plexMovieLibraryScanner, IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner, + ILibraryRepository libraryRepository, IEntityLocker entityLocker, ILogger logger) { @@ -38,6 +40,7 @@ namespace ErsatzTV.Application.Plex.Commands _plexSecretStore = plexSecretStore; _plexMovieLibraryScanner = plexMovieLibraryScanner; _plexTelevisionLibraryScanner = plexTelevisionLibraryScanner; + _libraryRepository = libraryRepository; _entityLocker = entityLocker; _logger = logger; } @@ -78,7 +81,7 @@ namespace ErsatzTV.Application.Plex.Commands } parameters.Library.LastScan = DateTime.UtcNow; - await _mediaSourceRepository.Update(parameters.Library); + await _libraryRepository.UpdateLastScan(parameters.Library); } else { diff --git a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs index 85f727d0f..60856949d 100644 --- a/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs +++ b/ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs @@ -14,10 +14,12 @@ using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Tests.Metadata { @@ -84,7 +86,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsLeft.Should().BeTrue(); result.IfLeft(error => error.Should().BeOfType()); @@ -107,7 +111,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -146,7 +152,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -186,7 +194,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -230,7 +240,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -277,7 +289,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -324,7 +338,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -370,7 +386,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -412,7 +430,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -448,7 +468,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -486,7 +508,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -513,7 +537,9 @@ namespace ErsatzTV.Core.Tests.Metadata Either result = await service.ScanFolder( libraryPath, FFprobePath, - DateTimeOffset.MinValue); + DateTimeOffset.MinValue, + 0, + 1); result.IsRight.Should().BeTrue(); @@ -531,6 +557,7 @@ namespace ErsatzTV.Core.Tests.Metadata new Mock().Object, _imageCache.Object, new Mock().Object, + new Mock().Object, new Mock>().Object ); } diff --git a/ErsatzTV.Core/Domain/Library/LibraryPath.cs b/ErsatzTV.Core/Domain/Library/LibraryPath.cs index 074d7fa76..8c2684a82 100644 --- a/ErsatzTV.Core/Domain/Library/LibraryPath.cs +++ b/ErsatzTV.Core/Domain/Library/LibraryPath.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace ErsatzTV.Core.Domain { @@ -6,6 +7,7 @@ namespace ErsatzTV.Core.Domain { public int Id { get; set; } public string Path { get; set; } + public DateTime? LastScan { get; set; } public int LibraryId { get; set; } public Library Library { get; set; } diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index cf455e810..426c97a08 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -11,6 +11,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 984c137b4..f6daf5ec8 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -199,6 +199,7 @@ namespace ErsatzTV.Core.FFmpeg "-c", "copy", "-muxdelay", "0", "-muxpreload", "0" + // "-avoid_negative_ts", "make_zero" }; _arguments.AddRange(arguments); return this; diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs index 73fed41d6..366993725 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMovieFolderScanner.cs @@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata { public interface IMovieFolderScanner { - Task> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); + Task> ScanFolder( + LibraryPath libraryPath, + string ffprobePath, + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs index bde026dab..1947c3f7f 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs @@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata { public interface IMusicVideoFolderScanner { - Task> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); + Task> ScanFolder( + LibraryPath libraryPath, + string ffprobePath, + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax); } } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs index f2b95303a..f2d3d2b65 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ITelevisionFolderScanner.cs @@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata { public interface ITelevisionFolderScanner { - Task> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); + Task> ScanFolder( + LibraryPath libraryPath, + string ffprobePath, + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs index ce0f89a23..7a8ba387c 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ILibraryRepository.cs @@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task> GetLocal(int libraryId); Task> GetAll(); Task UpdateLastScan(Library library); + Task UpdateLastScan(LibraryPath libraryPath); Task> GetLocalPaths(int libraryId); Task> GetPath(int libraryPathId); Task CountMediaItemsByPath(int libraryPathId); diff --git a/ErsatzTV.Core/Metadata/LibraryScanProgress.cs b/ErsatzTV.Core/Metadata/LibraryScanProgress.cs new file mode 100644 index 000000000..49ae721f2 --- /dev/null +++ b/ErsatzTV.Core/Metadata/LibraryScanProgress.cs @@ -0,0 +1,6 @@ +using MediatR; + +namespace ErsatzTV.Core.Metadata +{ + public record LibraryScanProgress(int LibraryId, decimal Progress) : INotification; +} diff --git a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs index ef2d5e8d6..508485923 100644 --- a/ErsatzTV.Core/Metadata/MovieFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MovieFolderScanner.cs @@ -10,9 +10,11 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; using Seq = LanguageExt.Seq; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Metadata { @@ -21,6 +23,7 @@ namespace ErsatzTV.Core.Metadata private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly IMovieRepository _movieRepository; private readonly ISearchIndex _searchIndex; @@ -32,6 +35,7 @@ namespace ErsatzTV.Core.Metadata IMetadataRepository metadataRepository, IImageCache imageCache, ISearchIndex searchIndex, + IMediator mediator, ILogger logger) : base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, logger) { @@ -39,19 +43,26 @@ namespace ErsatzTV.Core.Metadata _movieRepository = movieRepository; _localMetadataProvider = localMetadataProvider; _searchIndex = searchIndex; + _mediator = mediator; _logger = logger; } public async Task> ScanFolder( LibraryPath libraryPath, string ffprobePath, - DateTimeOffset lastScan) + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax) { + decimal progressSpread = progressMax - progressMin; + if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return new MediaSourceInaccessible(); } + var foldersCompleted = 0; + var folderQueue = new Queue(); foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) { @@ -60,7 +71,12 @@ namespace ErsatzTV.Core.Metadata while (folderQueue.Count > 0) { + decimal percentCompletion = (decimal) foldersCompleted / (foldersCompleted + folderQueue.Count); + await _mediator.Publish( + new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); + string movieFolder = folderQueue.Dequeue(); + foldersCompleted++; var allFiles = _localFileSystem.ListFiles(movieFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs index 9208fb175..d264f8ec0 100644 --- a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -10,8 +10,10 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Metadata { @@ -20,6 +22,7 @@ namespace ErsatzTV.Core.Metadata private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly IMusicVideoRepository _musicVideoRepository; private readonly ISearchIndex _searchIndex; @@ -31,6 +34,7 @@ namespace ErsatzTV.Core.Metadata IImageCache imageCache, ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository, + IMediator mediator, ILogger logger) : base( localFileSystem, localStatisticsProvider, @@ -42,19 +46,26 @@ namespace ErsatzTV.Core.Metadata _localMetadataProvider = localMetadataProvider; _searchIndex = searchIndex; _musicVideoRepository = musicVideoRepository; + _mediator = mediator; _logger = logger; } public async Task> ScanFolder( LibraryPath libraryPath, string ffprobePath, - DateTimeOffset lastScan) + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax) { + decimal progressSpread = progressMax - progressMin; + if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return new MediaSourceInaccessible(); } + var foldersCompleted = 0; + var folderQueue = new Queue(); foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) { @@ -63,9 +74,14 @@ namespace ErsatzTV.Core.Metadata while (folderQueue.Count > 0) { - string movieFolder = folderQueue.Dequeue(); + decimal percentCompletion = (decimal) foldersCompleted / (foldersCompleted + folderQueue.Count); + await _mediator.Publish( + new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); + + string musicVideoFolder = folderQueue.Dequeue(); + foldersCompleted++; - var allFiles = _localFileSystem.ListFiles(movieFolder) + var allFiles = _localFileSystem.ListFiles(musicVideoFolder) .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) .Filter( f => !ExtraFiles.Any( @@ -74,7 +90,8 @@ namespace ErsatzTV.Core.Metadata if (allFiles.Count == 0) { - foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) + foreach (string subdirectory in _localFileSystem.ListSubdirectories(musicVideoFolder) + .OrderBy(identity)) { folderQueue.Enqueue(subdirectory); } @@ -82,7 +99,7 @@ namespace ErsatzTV.Core.Metadata continue; } - if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan) + if (_localFileSystem.GetLastWriteTime(musicVideoFolder) < lastScan) { continue; } diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index fa2b5363b..7c88c4047 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -10,8 +10,10 @@ using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Metadata { @@ -20,6 +22,7 @@ namespace ErsatzTV.Core.Metadata private readonly ILocalFileSystem _localFileSystem; private readonly ILocalMetadataProvider _localMetadataProvider; private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly ISearchIndex _searchIndex; private readonly ITelevisionRepository _televisionRepository; @@ -31,6 +34,7 @@ namespace ErsatzTV.Core.Metadata IMetadataRepository metadataRepository, IImageCache imageCache, ISearchIndex searchIndex, + IMediator mediator, ILogger logger) : base( localFileSystem, localStatisticsProvider, @@ -42,14 +46,19 @@ namespace ErsatzTV.Core.Metadata _televisionRepository = televisionRepository; _localMetadataProvider = localMetadataProvider; _searchIndex = searchIndex; + _mediator = mediator; _logger = logger; } public async Task> ScanFolder( LibraryPath libraryPath, string ffprobePath, - DateTimeOffset lastScan) + DateTimeOffset lastScan, + decimal progressMin, + decimal progressMax) { + decimal progressSpread = progressMax - progressMin; + if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) { return new MediaSourceInaccessible(); @@ -62,6 +71,10 @@ namespace ErsatzTV.Core.Metadata foreach (string showFolder in allShowFolders) { + decimal percentCompletion = (decimal) allShowFolders.IndexOf(showFolder) / allShowFolders.Count; + await _mediator.Publish( + new LibraryScanProgress(libraryPath.LibraryId, progressMin + percentCompletion * progressSpread)); + Either> maybeShow = await FindOrCreateShow(libraryPath.Id, showFolder) .BindT(show => UpdateMetadataForShow(show, showFolder)) diff --git a/ErsatzTV.Core/Plex/PlexLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexLibraryScanner.cs index f20c7fb9f..4f23c0172 100644 --- a/ErsatzTV.Core/Plex/PlexLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexLibraryScanner.cs @@ -3,16 +3,21 @@ using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; +using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Plex { public abstract class PlexLibraryScanner { + private readonly ILogger _logger; private readonly IMetadataRepository _metadataRepository; - protected PlexLibraryScanner(IMetadataRepository metadataRepository) => + protected PlexLibraryScanner(IMetadataRepository metadataRepository, ILogger logger) + { _metadataRepository = metadataRepository; + _logger = logger; + } protected async Task UpdateArtworkIfNeeded( Domain.Metadata existingMetadata, @@ -27,6 +32,8 @@ namespace ErsatzTV.Core.Plex await maybeIncomingArtwork.Match( async incomingArtwork => { + _logger.LogDebug("Refreshing Plex {Attribute} from {Path}", artworkKind, incomingArtwork.Path); + Option maybeExistingArtwork = Optional(existingMetadata.Artwork).Flatten() .Find(a => a.ArtworkKind == artworkKind); diff --git a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs index 00c5fbe40..fffa6f694 100644 --- a/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs @@ -7,13 +7,16 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Plex { public class PlexMovieLibraryScanner : PlexLibraryScanner, IPlexMovieLibraryScanner { private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly IMetadataRepository _metadataRepository; private readonly IMovieRepository _movieRepository; private readonly IPlexServerApiClient _plexServerApiClient; @@ -24,13 +27,15 @@ namespace ErsatzTV.Core.Plex IMovieRepository movieRepository, IMetadataRepository metadataRepository, ISearchIndex searchIndex, + IMediator mediator, ILogger logger) - : base(metadataRepository) + : base(metadataRepository, logger) { _plexServerApiClient = plexServerApiClient; _movieRepository = movieRepository; _metadataRepository = metadataRepository; _searchIndex = searchIndex; + _mediator = mediator; _logger = logger; } @@ -49,6 +54,9 @@ namespace ErsatzTV.Core.Plex { foreach (PlexMovie incoming in movieEntries) { + decimal percentCompletion = (decimal) movieEntries.IndexOf(incoming) / movieEntries.Count; + await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, percentCompletion)); + // TODO: figure out how to rebuild playlists Either> maybeMovie = await _movieRepository .GetOrAdd(plexMediaSourceLibrary, incoming) @@ -81,6 +89,8 @@ namespace ErsatzTV.Core.Plex var movieKeys = movieEntries.Map(s => s.Key).ToList(); List ids = await _movieRepository.RemoveMissingPlexMovies(plexMediaSourceLibrary, movieKeys); await _searchIndex.RemoveItems(ids); + + await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0)); }, error => { @@ -113,6 +123,11 @@ namespace ErsatzTV.Core.Plex await maybeStatistics.Match( async mediaVersion => { + _logger.LogDebug( + "Refreshing {Attribute} from {Path}", + "Plex Statistics", + existingVersion.MediaFiles.Head().Path); + existingVersion.SampleAspectRatio = mediaVersion.SampleAspectRatio; existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.DateUpdated = mediaVersion.DateUpdated; @@ -135,6 +150,11 @@ namespace ErsatzTV.Core.Plex if (incomingMetadata.DateUpdated > existingMetadata.DateUpdated) { + _logger.LogDebug( + "Refreshing {Attribute} from {Path}", + "Plex Metadata", + existing.MediaVersions.Head().MediaFiles.Head().Path); + foreach (Genre genre in existingMetadata.Genres .Filter(g => incomingMetadata.Genres.All(g2 => g2.Name != g.Name)) .ToList()) diff --git a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs index 70d14d291..2179a5fc2 100644 --- a/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs +++ b/ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs @@ -7,14 +7,17 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Metadata; using LanguageExt; +using MediatR; using Microsoft.Extensions.Logging; using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; namespace ErsatzTV.Core.Plex { public class PlexTelevisionLibraryScanner : PlexLibraryScanner, IPlexTelevisionLibraryScanner { private readonly ILogger _logger; + private readonly IMediator _mediator; private readonly IMetadataRepository _metadataRepository; private readonly IPlexServerApiClient _plexServerApiClient; private readonly ISearchIndex _searchIndex; @@ -25,13 +28,15 @@ namespace ErsatzTV.Core.Plex ITelevisionRepository televisionRepository, IMetadataRepository metadataRepository, ISearchIndex searchIndex, + IMediator mediator, ILogger logger) - : base(metadataRepository) + : base(metadataRepository, logger) { _plexServerApiClient = plexServerApiClient; _televisionRepository = televisionRepository; _metadataRepository = metadataRepository; _searchIndex = searchIndex; + _mediator = mediator; _logger = logger; } @@ -50,6 +55,9 @@ namespace ErsatzTV.Core.Plex { foreach (PlexShow incoming in showEntries) { + decimal percentCompletion = (decimal) showEntries.IndexOf(incoming) / showEntries.Count; + await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, percentCompletion)); + // TODO: figure out how to rebuild playlists Either> maybeShow = await _televisionRepository .GetOrAddPlexShow(plexMediaSourceLibrary, incoming) @@ -85,6 +93,8 @@ namespace ErsatzTV.Core.Plex await _televisionRepository.RemoveMissingPlexShows(plexMediaSourceLibrary, showKeys); await _searchIndex.RemoveItems(ids); + await _mediator.Publish(new LibraryScanProgress(plexMediaSourceLibrary.Id, 0)); + return Unit.Default; }, error => diff --git a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs index a9a4dd6b0..0e0716b8d 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs @@ -62,6 +62,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories "UPDATE Library SET LastScan = @LastScan WHERE Id = @Id", new { library.LastScan, library.Id }).ToUnit(); + public Task UpdateLastScan(LibraryPath libraryPath) => _dbConnection.ExecuteAsync( + "UPDATE LibraryPath SET LastScan = @LastScan WHERE Id = @Id", + new { libraryPath.LastScan, libraryPath.Id }).ToUnit(); + public Task> GetLocalPaths(int libraryId) { using TvContext context = _dbContextFactory.CreateDbContext(); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs index 2bd6d9aed..e8b60695c 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs @@ -52,6 +52,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(lp => lp.Library) .Include(mv => mv.MediaVersions) .ThenInclude(mv => mv.MediaFiles) + .Include(mv => mv.MediaVersions) + .ThenInclude(mv => mv.Streams) .OrderBy(mv => mv.Id) .SingleOrDefaultAsync(mv => mv.Id == id) .Map(Optional); diff --git a/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs new file mode 100644 index 000000000..6f13852d5 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.Designer.cs @@ -0,0 +1,1964 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210404121830_Add_LibraryPath_LastScan")] + partial class Add_LibraryPath_LastScan + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.4"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + 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("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channel"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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("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"); + }); + + 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("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Genre"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + 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("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream"); + }); + + 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("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .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("MovieId"); + + b.ToTable("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("Artist") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .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("MusicVideoId"); + + b.ToTable("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomGroup") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("PlexPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + 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("ShowId") + .HasColumnType("INTEGER"); + + 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("ShowId"); + + b.ToTable("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Studio"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("PlatformVersion") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Genres") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Genres") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Streams") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") + .WithMany("MusicVideoMetadata") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("DurationFinish") + .HasColumnType("TEXT"); + + b1.Property("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playout"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs b/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs new file mode 100644 index 000000000..fb3b263c0 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210404121830_Add_LibraryPath_LastScan.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_LibraryPath_LastScan : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) => + migrationBuilder.AddColumn( + "LastScan", + "LibraryPath", + "TEXT", + nullable: true); + + protected override void Down(MigrationBuilder migrationBuilder) => + migrationBuilder.DropColumn( + "LastScan", + "LibraryPath"); + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs new file mode 100644 index 000000000..6a149fad9 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.Designer.cs @@ -0,0 +1,1964 @@ +// +using System; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace ErsatzTV.Infrastructure.Migrations +{ + [DbContext(typeof(TvContext))] + [Migration("20210404122137_Update_LibraryPathLastScan_LibraryLastScan")] + partial class Update_LibraryPathLastScan_LibraryLastScan + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.4"); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ArtworkKind") + .HasColumnType("INTEGER"); + + 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("Path") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Artwork"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FFmpegProfileId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PreferredLanguageCode") + .HasColumnType("TEXT"); + + b.Property("StreamingMode") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("FFmpegProfileId"); + + b.HasIndex("Number") + .IsUnique(); + + b.ToTable("Channel"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + 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("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"); + }); + + 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("AudioCodec") + .HasColumnType("TEXT"); + + b.Property("AudioSampleRate") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .HasColumnType("INTEGER"); + + b.Property("NormalizeLoudness") + .HasColumnType("INTEGER"); + + b.Property("NormalizeVideo") + .HasColumnType("INTEGER"); + + b.Property("ResolutionId") + .HasColumnType("INTEGER"); + + b.Property("ThreadCount") + .HasColumnType("INTEGER"); + + b.Property("Transcode") + .HasColumnType("INTEGER"); + + b.Property("VideoBitrate") + .HasColumnType("INTEGER"); + + b.Property("VideoBufferSize") + .HasColumnType("INTEGER"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ResolutionId"); + + b.ToTable("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Genre"); + }); + + 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"); + }); + + 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"); + }); + + 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"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LibraryPathId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LibraryPathId"); + + b.ToTable("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Channels") + .HasColumnType("INTEGER"); + + b.Property("Codec") + .HasColumnType("TEXT"); + + b.Property("Default") + .HasColumnType("INTEGER"); + + 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("Profile") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaVersionId"); + + b.ToTable("MediaStream"); + }); + + 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("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + b.HasIndex("MusicVideoId"); + + b.ToTable("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MovieId") + .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("MovieId"); + + b.ToTable("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Album") + .HasColumnType("TEXT"); + + b.Property("Artist") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Plot") + .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("MusicVideoId"); + + b.ToTable("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ProgramSchedulePlayoutType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomGroup") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Finish") + .HasColumnType("TEXT"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("Start") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.ToTable("PlayoutItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("PlayoutId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("PlayoutProgramScheduleAnchor"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexConnection"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LocalPath") + .HasColumnType("TEXT"); + + b.Property("PlexMediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("PlexPath") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlexMediaSourceId"); + + b.ToTable("PlexPathReplacement"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MediaCollectionPlaybackOrder") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CollectionId") + .HasColumnType("INTEGER"); + + b.Property("CollectionType") + .HasColumnType("INTEGER"); + + b.Property("CustomTitle") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("MediaItemId") + .HasColumnType("INTEGER"); + + b.Property("ProgramScheduleId") + .HasColumnType("INTEGER"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CollectionId"); + + b.HasIndex("MediaItemId"); + + b.HasIndex("ProgramScheduleId"); + + b.ToTable("ProgramScheduleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + b.Property("MetadataKind") + .HasColumnType("INTEGER"); + + b.Property("OriginalTitle") + .HasColumnType("TEXT"); + + b.Property("Outline") + .HasColumnType("TEXT"); + + b.Property("ReleaseDate") + .HasColumnType("TEXT"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.Property("SortTitle") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Year") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("SeasonId"); + + b.ToTable("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DateUpdated") + .HasColumnType("TEXT"); + + 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("ShowId") + .HasColumnType("INTEGER"); + + 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("ShowId"); + + b.ToTable("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Studio"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", 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("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + b.HasIndex("MusicVideoMetadataId"); + + b.HasIndex("SeasonMetadataId"); + + b.HasIndex("ShowMetadataId"); + + b.ToTable("Tag"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.ToTable("LocalLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Library"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("ShouldSyncItems") + .HasColumnType("INTEGER"); + + b.ToTable("PlexLibrary"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("PlexId") + .HasColumnType("INTEGER"); + + b.ToTable("PlexMediaFile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("EpisodeNumber") + .HasColumnType("INTEGER"); + + b.Property("SeasonId") + .HasColumnType("INTEGER"); + + b.HasIndex("SeasonId"); + + b.ToTable("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.Property("SeasonNumber") + .HasColumnType("INTEGER"); + + b.Property("ShowId") + .HasColumnType("INTEGER"); + + b.HasIndex("ShowId"); + + b.ToTable("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); + + b.ToTable("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.ToTable("LocalMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); + + b.Property("ClientIdentifier") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("PlatformVersion") + .HasColumnType("TEXT"); + + b.Property("ProductVersion") + .HasColumnType("TEXT"); + + b.Property("ServerName") + .HasColumnType("TEXT"); + + b.ToTable("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("OfflineTail") + .HasColumnType("INTEGER"); + + b.Property("PlayoutDuration") + .HasColumnType("TEXT"); + + b.ToTable("ProgramScheduleDurationItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleFloodItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.ToTable("ProgramScheduleMultipleItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); + + b.ToTable("ProgramScheduleOneItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Episode"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexEpisode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Movie"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexMovie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Season"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexSeason"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.Show"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.ToTable("PlexShow"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", null) + .WithMany("Artwork") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Artwork") + .HasForeignKey("EpisodeMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Artwork") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Artwork") + .HasForeignKey("SeasonMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Artwork") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") + .WithMany() + .HasForeignKey("FFmpegProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FFmpegProfile"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany("CollectionItems") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany("CollectionItems") + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") + .WithMany("EpisodeMetadata") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Episode"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + { + b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") + .WithMany() + .HasForeignKey("ResolutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Resolution"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Genres") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Genres") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Genres") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Genres") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Genres") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") + .WithMany("Libraries") + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", "Library") + .WithMany("Paths") + .HasForeignKey("LibraryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Library"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("MediaFiles") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") + .WithMany("MediaItems") + .HasForeignKey("LibraryPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LibraryPath"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") + .WithMany("Streams") + .HasForeignKey("MediaVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaVersion"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithMany("MediaVersions") + .HasForeignKey("EpisodeId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithMany("MediaVersions") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") + .WithMany("MovieMetadata") + .HasForeignKey("MovieId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Movie"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") + .WithMany("MusicVideoMetadata") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MusicVideo"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") + .WithMany("Playouts") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Playouts") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + { + b1.Property("PlayoutId") + .HasColumnType("INTEGER"); + + b1.Property("DurationFinish") + .HasColumnType("TEXT"); + + b1.Property("MultipleRemaining") + .HasColumnType("INTEGER"); + + b1.Property("NextScheduleItemId") + .HasColumnType("INTEGER"); + + b1.Property("NextStart") + .HasColumnType("TEXT"); + + b1.HasKey("PlayoutId"); + + b1.HasIndex("NextScheduleItemId"); + + b1.ToTable("Playout"); + + b1.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", "NextScheduleItem") + .WithMany() + .HasForeignKey("NextScheduleItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b1.WithOwner() + .HasForeignKey("PlayoutId"); + + b1.Navigation("NextScheduleItem"); + }); + + b.Navigation("Anchor"); + + b.Navigation("Channel"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("Items") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout") + .WithMany("ProgramScheduleAnchors") + .HasForeignKey("PlayoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany() + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + { + b1.Property("PlayoutProgramScheduleAnchorId") + .HasColumnType("INTEGER"); + + b1.Property("Index") + .HasColumnType("INTEGER"); + + b1.Property("Seed") + .HasColumnType("INTEGER"); + + b1.HasKey("PlayoutProgramScheduleAnchorId"); + + b1.ToTable("PlayoutProgramScheduleAnchor"); + + b1.WithOwner() + .HasForeignKey("PlayoutProgramScheduleAnchorId"); + }); + + b.Navigation("Collection"); + + b.Navigation("EnumeratorState"); + + b.Navigation("MediaItem"); + + b.Navigation("Playout"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("Connections") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + { + b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") + .WithMany("PathReplacements") + .HasForeignKey("PlexMediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlexMediaSource"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + { + b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") + .WithMany() + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") + .WithMany() + .HasForeignKey("MediaItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule") + .WithMany("Items") + .HasForeignKey("ProgramScheduleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("MediaItem"); + + b.Navigation("ProgramSchedule"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("SeasonMetadata") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("ShowMetadata") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Studios") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Studios") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Studios") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Studios") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Studios") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + { + b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) + .WithMany("Tags") + .HasForeignKey("EpisodeMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.MovieMetadata", null) + .WithMany("Tags") + .HasForeignKey("MovieMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideoMetadata", null) + .WithMany("Tags") + .HasForeignKey("MusicVideoMetadataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.SeasonMetadata", null) + .WithMany("Tags") + .HasForeignKey("SeasonMetadataId"); + + b.HasOne("ErsatzTV.Core.Domain.ShowMetadata", null) + .WithMany("Tags") + .HasForeignKey("ShowMetadataId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + { + b.HasOne("ErsatzTV.Core.Domain.Library", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexLibrary", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaFile", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Episode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Season", "Season") + .WithMany("Episodes") + .HasForeignKey("SeasonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Season"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Movie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.MusicVideo", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Season", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ErsatzTV.Core.Domain.Show", "Show") + .WithMany("Seasons") + .HasForeignKey("ShowId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Show"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.Show", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.LocalMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMediaSource", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + { + b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.ProgramScheduleItemOne", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + { + b.HasOne("ErsatzTV.Core.Domain.Episode", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexEpisode", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + { + b.HasOne("ErsatzTV.Core.Domain.Movie", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexMovie", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + { + b.HasOne("ErsatzTV.Core.Domain.Season", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexSeason", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + { + b.HasOne("ErsatzTV.Core.Domain.Show", null) + .WithOne() + .HasForeignKey("ErsatzTV.Core.Domain.PlexShow", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + { + b.Navigation("Artwork"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + { + b.Navigation("Paths"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + { + b.Navigation("MediaItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + { + b.Navigation("CollectionItems"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + { + b.Navigation("Libraries"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + { + b.Navigation("MediaFiles"); + + b.Navigation("Streams"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + { + b.Navigation("Items"); + + b.Navigation("ProgramScheduleAnchors"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + { + b.Navigation("Items"); + + b.Navigation("Playouts"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + { + b.Navigation("Artwork"); + + b.Navigation("Genres"); + + b.Navigation("Studios"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + { + b.Navigation("EpisodeMetadata"); + + b.Navigation("MediaVersions"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MovieMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + { + b.Navigation("Episodes"); + + b.Navigation("SeasonMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + { + b.Navigation("Seasons"); + + b.Navigation("ShowMetadata"); + }); + + modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + { + b.Navigation("Connections"); + + b.Navigation("PathReplacements"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs b/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs new file mode 100644 index 000000000..98f9e9fe8 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210404122137_Update_LibraryPathLastScan_LibraryLastScan.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Update_LibraryPathLastScan_LibraryLastScan : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) => + migrationBuilder.Sql( + @"UPDATE LibraryPath SET LastScan = + (SELECT LastScan FROM Library L + INNER JOIN LocalLibrary LL on L.Id = LL.Id + WHERE LibraryPath.LibraryId = L.Id)"); + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 98f4b2bd3..e42b7b23c 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -1,14 +1,14 @@ // + using System; using ErsatzTV.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace ErsatzTV.Infrastructure.Migrations { [DbContext(typeof(TvContext))] - partial class TvContextModelSnapshot : ModelSnapshot + internal class TvContextModelSnapshot : ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { @@ -16,7 +16,9 @@ namespace ErsatzTV.Infrastructure.Migrations modelBuilder .HasAnnotation("ProductVersion", "5.0.4"); - modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Artwork", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -69,7 +71,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Artwork"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Channel", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -103,7 +107,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Channel"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Collection", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -120,7 +126,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Collection"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.CollectionItem", + b => { b.Property("CollectionId") .HasColumnType("INTEGER"); @@ -138,7 +146,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("CollectionItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ConfigElement", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ConfigElement", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -158,7 +168,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ConfigElement"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.EpisodeMetadata", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -207,7 +219,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("EpisodeMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.FFmpegProfile", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -271,7 +285,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("FFmpegProfile"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Genre", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -310,7 +326,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Genre"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Library", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -335,12 +353,17 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Library"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LibraryPath", + b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("LastScan") + .HasColumnType("TEXT"); + b.Property("LibraryId") .HasColumnType("INTEGER"); @@ -354,7 +377,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("LibraryPath"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaFile", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -376,7 +401,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaFile"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaItem", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -392,7 +419,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaSource", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -403,7 +432,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaStream", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -446,7 +477,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaStream"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaVersion", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -499,7 +532,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MediaVersion"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MovieMetadata", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -548,7 +583,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MovieMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideoMetadata", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -597,7 +634,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("MusicVideoMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Playout", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -621,7 +660,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Playout"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlayoutItem", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -654,7 +695,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlayoutItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -688,7 +731,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlayoutProgramScheduleAnchor"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexConnection", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -710,7 +755,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexConnection"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexPathReplacement", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -732,7 +779,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexPathReplacement"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramSchedule", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -752,7 +801,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramSchedule"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItem", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -790,7 +841,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Resolution", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Resolution", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -810,7 +863,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Resolution"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.SeasonMetadata", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -853,7 +908,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("SeasonMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ShowMetadata", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -902,7 +959,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ShowMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Studio", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -941,7 +1000,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Studio"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Tag", + b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -980,14 +1041,18 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Tag"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalLibrary", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Library"); b.ToTable("LocalLibrary"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexLibrary", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Library"); @@ -1000,7 +1065,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexLibrary"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMediaFile", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaFile"); @@ -1013,7 +1080,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexMediaFile"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Episode", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); @@ -1028,21 +1097,27 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Episode"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Movie", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); b.ToTable("Movie"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideo", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); b.ToTable("MusicVideo"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Season", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); @@ -1057,21 +1132,27 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("Season"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Show", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaItem"); b.ToTable("Show"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalMediaSource", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); b.ToTable("LocalMediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMediaSource", + b => { b.HasBaseType("ErsatzTV.Core.Domain.MediaSource"); @@ -1093,7 +1174,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexMediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemDuration", + b => { b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); @@ -1106,14 +1189,18 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleDurationItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemFlood", + b => { b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); b.ToTable("ProgramScheduleFloodItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", + b => { b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); @@ -1123,14 +1210,18 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleMultipleItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemOne", + b => { b.HasBaseType("ErsatzTV.Core.Domain.ProgramScheduleItem"); b.ToTable("ProgramScheduleOneItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexEpisode", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Episode"); @@ -1140,7 +1231,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexEpisode"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMovie", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Movie"); @@ -1150,7 +1243,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexMovie"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexSeason", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Season"); @@ -1160,7 +1255,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexSeason"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexShow", + b => { b.HasBaseType("ErsatzTV.Core.Domain.Show"); @@ -1170,7 +1267,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("PlexShow"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Artwork", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Artwork", + b => { b.HasOne("ErsatzTV.Core.Domain.Channel", null) .WithMany("Artwork") @@ -1203,7 +1302,9 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Channel", + b => { b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") .WithMany() @@ -1214,7 +1315,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("FFmpegProfile"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.CollectionItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.CollectionItem", + b => { b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") .WithMany("CollectionItems") @@ -1233,7 +1336,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MediaItem"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.EpisodeMetadata", + b => { b.HasOne("ErsatzTV.Core.Domain.Episode", "Episode") .WithMany("EpisodeMetadata") @@ -1244,7 +1349,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Episode"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.FFmpegProfile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.FFmpegProfile", + b => { b.HasOne("ErsatzTV.Core.Domain.Resolution", "Resolution") .WithMany() @@ -1255,7 +1362,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Resolution"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Genre", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Genre", + b => { b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) .WithMany("Genres") @@ -1281,7 +1390,9 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Library", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaSource", "MediaSource") .WithMany("Libraries") @@ -1292,7 +1403,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LibraryPath", + b => { b.HasOne("ErsatzTV.Core.Domain.Library", "Library") .WithMany("Paths") @@ -1303,7 +1416,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Library"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaFile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaFile", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") .WithMany("MediaFiles") @@ -1314,7 +1429,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MediaVersion"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaItem", + b => { b.HasOne("ErsatzTV.Core.Domain.LibraryPath", "LibraryPath") .WithMany("MediaItems") @@ -1325,7 +1442,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("LibraryPath"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaStream", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaStream", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaVersion", "MediaVersion") .WithMany("Streams") @@ -1336,7 +1455,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MediaVersion"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaVersion", + b => { b.HasOne("ErsatzTV.Core.Domain.Episode", null) .WithMany("MediaVersions") @@ -1354,7 +1475,9 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MovieMetadata", + b => { b.HasOne("ErsatzTV.Core.Domain.Movie", "Movie") .WithMany("MovieMetadata") @@ -1365,7 +1488,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Movie"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideoMetadata", + b => { b.HasOne("ErsatzTV.Core.Domain.MusicVideo", "MusicVideo") .WithMany("MusicVideoMetadata") @@ -1376,7 +1501,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MusicVideo"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Playout", + b => { b.HasOne("ErsatzTV.Core.Domain.Channel", "Channel") .WithMany("Playouts") @@ -1390,7 +1517,10 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 => + b.OwnsOne( + "ErsatzTV.Core.Domain.PlayoutAnchor", + "Anchor", + b1 => { b1.Property("PlayoutId") .HasColumnType("INTEGER"); @@ -1432,7 +1562,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("ProgramSchedule"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlayoutItem", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem") .WithMany() @@ -1451,7 +1583,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Playout"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor", + b => { b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") .WithMany() @@ -1475,7 +1609,10 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 => + b.OwnsOne( + "ErsatzTV.Core.Domain.CollectionEnumeratorState", + "EnumeratorState", + b1 => { b1.Property("PlayoutProgramScheduleAnchorId") .HasColumnType("INTEGER"); @@ -1505,7 +1642,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("ProgramSchedule"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexConnection", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexConnection", + b => { b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") .WithMany("Connections") @@ -1516,7 +1655,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("PlexMediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexPathReplacement", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexPathReplacement", + b => { b.HasOne("ErsatzTV.Core.Domain.PlexMediaSource", "PlexMediaSource") .WithMany("PathReplacements") @@ -1527,7 +1668,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("PlexMediaSource"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItem", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItem", + b => { b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection") .WithMany() @@ -1552,7 +1695,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("ProgramSchedule"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.SeasonMetadata", + b => { b.HasOne("ErsatzTV.Core.Domain.Season", "Season") .WithMany("SeasonMetadata") @@ -1563,7 +1708,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Season"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ShowMetadata", + b => { b.HasOne("ErsatzTV.Core.Domain.Show", "Show") .WithMany("ShowMetadata") @@ -1574,7 +1721,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Show"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Studio", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Studio", + b => { b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) .WithMany("Studios") @@ -1600,7 +1749,9 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Tag", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Tag", + b => { b.HasOne("ErsatzTV.Core.Domain.EpisodeMetadata", null) .WithMany("Tags") @@ -1626,7 +1777,9 @@ namespace ErsatzTV.Infrastructure.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalLibrary", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalLibrary", + b => { b.HasOne("ErsatzTV.Core.Domain.Library", null) .WithOne() @@ -1635,7 +1788,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexLibrary", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexLibrary", + b => { b.HasOne("ErsatzTV.Core.Domain.Library", null) .WithOne() @@ -1644,7 +1799,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaFile", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMediaFile", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaFile", null) .WithOne() @@ -1653,7 +1810,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Episode", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() @@ -1670,7 +1829,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Season"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Movie", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() @@ -1679,7 +1840,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideo", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() @@ -1688,7 +1851,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Season", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() @@ -1705,7 +1870,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Show"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Show", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaItem", null) .WithOne() @@ -1714,7 +1881,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LocalMediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalMediaSource", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) .WithOne() @@ -1723,7 +1892,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMediaSource", + b => { b.HasOne("ErsatzTV.Core.Domain.MediaSource", null) .WithOne() @@ -1732,7 +1903,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemDuration", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemDuration", + b => { b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) .WithOne() @@ -1741,7 +1914,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemFlood", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemFlood", + b => { b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) .WithOne() @@ -1750,7 +1925,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemMultiple", + b => { b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) .WithOne() @@ -1759,7 +1936,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramScheduleItemOne", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramScheduleItemOne", + b => { b.HasOne("ErsatzTV.Core.Domain.ProgramScheduleItem", null) .WithOne() @@ -1768,7 +1947,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexEpisode", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexEpisode", + b => { b.HasOne("ErsatzTV.Core.Domain.Episode", null) .WithOne() @@ -1777,7 +1958,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMovie", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMovie", + b => { b.HasOne("ErsatzTV.Core.Domain.Movie", null) .WithOne() @@ -1786,7 +1969,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexSeason", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexSeason", + b => { b.HasOne("ErsatzTV.Core.Domain.Season", null) .WithOne() @@ -1795,7 +1980,9 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexShow", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexShow", + b => { b.HasOne("ErsatzTV.Core.Domain.Show", null) .WithOne() @@ -1804,19 +1991,20 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Channel", + b => { b.Navigation("Artwork"); b.Navigation("Playouts"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => - { - b.Navigation("CollectionItems"); - }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Collection", b => { b.Navigation("CollectionItems"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.EpisodeMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.EpisodeMetadata", + b => { b.Navigation("Artwork"); @@ -1827,34 +2015,26 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Tags"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => - { - b.Navigation("Paths"); - }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Library", b => { b.Navigation("Paths"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => - { - b.Navigation("MediaItems"); - }); + modelBuilder.Entity("ErsatzTV.Core.Domain.LibraryPath", b => { b.Navigation("MediaItems"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => - { - b.Navigation("CollectionItems"); - }); + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaItem", b => { b.Navigation("CollectionItems"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => - { - b.Navigation("Libraries"); - }); + modelBuilder.Entity("ErsatzTV.Core.Domain.MediaSource", b => { b.Navigation("Libraries"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MediaVersion", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MediaVersion", + b => { b.Navigation("MediaFiles"); b.Navigation("Streams"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MovieMetadata", + b => { b.Navigation("Artwork"); @@ -1865,7 +2045,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Tags"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideoMetadata", + b => { b.Navigation("Artwork"); @@ -1876,21 +2058,27 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Tags"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Playout", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Playout", + b => { b.Navigation("Items"); b.Navigation("ProgramScheduleAnchors"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ProgramSchedule", + b => { b.Navigation("Items"); b.Navigation("Playouts"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.SeasonMetadata", + b => { b.Navigation("Artwork"); @@ -1901,7 +2089,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Tags"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.ShowMetadata", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.ShowMetadata", + b => { b.Navigation("Artwork"); @@ -1912,42 +2102,54 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Tags"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Episode", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Episode", + b => { b.Navigation("EpisodeMetadata"); b.Navigation("MediaVersions"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Movie", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Movie", + b => { b.Navigation("MediaVersions"); b.Navigation("MovieMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideo", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideo", + b => { b.Navigation("MediaVersions"); b.Navigation("MusicVideoMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Season", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Season", + b => { b.Navigation("Episodes"); b.Navigation("SeasonMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.Show", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.Show", + b => { b.Navigation("Seasons"); b.Navigation("ShowMetadata"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.PlexMediaSource", b => + modelBuilder.Entity( + "ErsatzTV.Core.Domain.PlexMediaSource", + b => { b.Navigation("Connections"); diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index a75f60a2d..b0cf0b182 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -19,6 +19,7 @@ + diff --git a/ErsatzTV/Pages/CollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor index a95d2cc0f..e0f6031c2 100644 --- a/ErsatzTV/Pages/CollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -243,12 +243,15 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _data.MovieCards.OrderBy(m => m.SortTitle) - .Append(_data.ShowCards.OrderBy(s => s.SortTitle)) - .Append(_data.SeasonCards.OrderBy(s => s.SortTitle)) - .Append(_data.EpisodeCards.OrderBy(ep => ep.Aired)) - .Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle)) - .ToList(); + List GetSortedItems() + { + return _data.MovieCards.OrderBy(m => m.SortTitle) + .Append(_data.ShowCards.OrderBy(s => s.SortTitle)) + .Append(_data.SeasonCards.OrderBy(s => s.SortTitle)) + .Append(_data.EpisodeCards.OrderBy(ep => ep.Aired)) + .Append(_data.MusicVideoCards.OrderBy(mv => mv.SortTitle)) + .ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Pages/FFmpegEditor.razor b/ErsatzTV/Pages/FFmpegEditor.razor index 4b2719b73..c762e4632 100644 --- a/ErsatzTV/Pages/FFmpegEditor.razor +++ b/ErsatzTV/Pages/FFmpegEditor.razor @@ -62,12 +62,12 @@ - + Audio - + diff --git a/ErsatzTV/Pages/Libraries.razor b/ErsatzTV/Pages/Libraries.razor index 9c7f16d18..1a23985a6 100644 --- a/ErsatzTV/Pages/Libraries.razor +++ b/ErsatzTV/Pages/Libraries.razor @@ -1,12 +1,17 @@ @page "/media/libraries" +@using MediatR.Courier @using ErsatzTV.Application.Libraries @using ErsatzTV.Application.Libraries.Queries @using ErsatzTV.Application.MediaSources.Commands @using ErsatzTV.Application.Plex.Commands +@using ErsatzTV.Core.Metadata +@using System.Threading @implements IDisposable @inject IMediator Mediator @inject IEntityLocker Locker -@inject ChannelWriter Channel +@inject ChannelWriter WorkerChannel +@inject ChannelWriter PlexWorkerChannel +@inject ICourier Courier @@ -17,7 +22,7 @@ - + Library Kind @@ -33,12 +38,21 @@
@if (Locker.IsLibraryLocked(context.Id)) { +
+ @if (_progressByLibrary[context.Id] > 0) + { + + @($"{_progressByLibrary[context.Id]} %") + + } +
} else { +
_libraries; + private Dictionary _progressByLibrary; - protected override void OnInitialized() => + protected override void OnInitialized() + { Locker.OnLibraryChanged += LockChanged; + Courier.Subscribe(HandleScanProgress); + } protected override async Task OnParametersSetAsync() => await LoadLibraries(); - private async Task LoadLibraries() => + private async Task LoadLibraries() + { _libraries = await Mediator.Send(new GetAllLibraries()); + _progressByLibrary = _libraries.ToDictionary(vm => vm.Id, _ => 0); + } private async Task ScanLibrary(LibraryViewModel library) { @@ -79,10 +100,10 @@ switch (library) { case LocalLibraryViewModel: - await Channel.WriteAsync(new ForceScanLocalLibrary(library.Id)); + await WorkerChannel.WriteAsync(new ForceScanLocalLibrary(library.Id)); break; case PlexLibraryViewModel: - await Channel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id)); + await PlexWorkerChannel.WriteAsync(new ForceSynchronizePlexLibraryById(library.Id)); break; } @@ -93,6 +114,26 @@ private void LockChanged(object sender, EventArgs e) => InvokeAsync(StateHasChanged); - void IDisposable.Dispose() => Locker.OnLibraryChanged -= LockChanged; + private async Task HandleScanProgress(LibraryScanProgress libraryScanProgress, CancellationToken cancellationToken) + { + try + { + if (_progressByLibrary != null && _progressByLibrary.ContainsKey(libraryScanProgress.LibraryId)) + { + _progressByLibrary[libraryScanProgress.LibraryId] = (int) (libraryScanProgress.Progress * 100); + await InvokeAsync(StateHasChanged); + } + } + catch (Exception) + { + // ignore + } + } + + void IDisposable.Dispose() + { + Locker.OnLibraryChanged -= LockChanged; + Courier.UnSubscribe(HandleScanProgress); + } } \ No newline at end of file diff --git a/ErsatzTV/Pages/LocalLibraryPathEditor.razor b/ErsatzTV/Pages/LocalLibraryPathEditor.razor index 198c77d71..f47abef22 100644 --- a/ErsatzTV/Pages/LocalLibraryPathEditor.razor +++ b/ErsatzTV/Pages/LocalLibraryPathEditor.razor @@ -74,7 +74,7 @@ { if (Locker.LockLibrary(_library.Id)) { - await Channel.WriteAsync(new ForceRescanLocalLibrary(_library.Id)); + await Channel.WriteAsync(new ScanLocalLibraryIfNeeded(_library.Id)); NavigationManager.NavigateTo("/media/libraries"); } }); diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor index af9d72114..35cc4322b 100644 --- a/ErsatzTV/Pages/MusicVideoList.razor +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -132,7 +132,10 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList(); + List GetSortedItems() + { + return _data.Cards.OrderBy(m => m.SortTitle).ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Pages/PlexLibrariesEditor.razor b/ErsatzTV/Pages/PlexLibrariesEditor.razor index 0e8dc0356..2f358a9d7 100644 --- a/ErsatzTV/Pages/PlexLibrariesEditor.razor +++ b/ErsatzTV/Pages/PlexLibrariesEditor.razor @@ -6,7 +6,7 @@ @inject NavigationManager NavigationManager @inject ILogger Logger @inject ISnackbar Snackbar -@inject ChannelWriter Channel +@inject ChannelWriter Channel @inject IEntityLocker Locker diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index 455d4a115..9c5ec91eb 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -169,10 +169,13 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() => _movies.Cards.OrderBy(m => m.SortTitle) - .Append(_shows.Cards.OrderBy(s => s.SortTitle)) - .Append(_musicVideos.Cards.OrderBy(s => s.SortTitle)) - .ToList(); + List GetSortedItems() + { + return _movies.Cards.OrderBy(m => m.SortTitle) + .Append(_shows.Cards.OrderBy(s => s.SortTitle)) + .Append(_musicVideos.Cards.OrderBy(s => s.SortTitle)) + .ToList(); + } SelectClicked(GetSortedItems, card, e); } diff --git a/ErsatzTV/Services/PlexService.cs b/ErsatzTV/Services/PlexService.cs index c137ac359..ceefcbab4 100644 --- a/ErsatzTV/Services/PlexService.cs +++ b/ErsatzTV/Services/PlexService.cs @@ -52,17 +52,24 @@ namespace ErsatzTV.Services { try { - Task requestTask = request switch + Task requestTask; + switch (request) { - TryCompletePlexPinFlow pinRequest => CompletePinFlow(pinRequest, cancellationToken), - SynchronizePlexMediaSources sourcesRequest => SynchronizeSources( - sourcesRequest, - cancellationToken), - SynchronizePlexLibraries synchronizePlexLibrariesRequest => SynchronizeLibraries( - synchronizePlexLibrariesRequest, - cancellationToken), - _ => throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}") - }; + case TryCompletePlexPinFlow pinRequest: + requestTask = CompletePinFlow(pinRequest, cancellationToken); + break; + case SynchronizePlexMediaSources sourcesRequest: + requestTask = SynchronizeSources(sourcesRequest, cancellationToken); + break; + case SynchronizePlexLibraries synchronizePlexLibrariesRequest: + requestTask = SynchronizeLibraries(synchronizePlexLibrariesRequest, cancellationToken); + break; + case ISynchronizePlexLibraryById synchronizePlexLibraryById: + requestTask = SynchronizePlexLibrary(synchronizePlexLibraryById, cancellationToken); + break; + default: + throw new NotSupportedException($"Unsupported request type: {request.GetType().Name}"); + } await requestTask; } @@ -109,17 +116,8 @@ namespace ErsatzTV.Services Either result = await mediator.Send(request, cancellationToken); result.BiIter( - success => - { - if (success) - { - _logger.LogInformation("Successfully authenticated with plex"); - } - else - { - _logger.LogInformation("Plex authentication timeout"); - } - }, + success => _logger.LogInformation( + success ? "Successfully authenticated with plex" : "Plex authentication timeout"), error => _logger.LogWarning("Unable to poll plex token: {Error}", error.Value)); } @@ -138,5 +136,21 @@ namespace ErsatzTV.Services request.PlexMediaSourceId, error.Value)); } + + private async Task SynchronizePlexLibrary( + ISynchronizePlexLibraryById request, + CancellationToken cancellationToken) + { + using IServiceScope scope = _serviceScopeFactory.CreateScope(); + IMediator mediator = scope.ServiceProvider.GetRequiredService(); + + Either result = await mediator.Send(request, cancellationToken); + result.BiIter( + name => _logger.LogDebug("Done synchronizing plex library {Name}", name), + error => _logger.LogWarning( + "Unable to synchronize plex library {LibraryId}: {Error}", + request.PlexLibraryId, + error.Value)); + } } } diff --git a/ErsatzTV/Services/SchedulerService.cs b/ErsatzTV/Services/SchedulerService.cs index 0ffa90f97..3ad2f1240 100644 --- a/ErsatzTV/Services/SchedulerService.cs +++ b/ErsatzTV/Services/SchedulerService.cs @@ -21,19 +21,22 @@ namespace ErsatzTV.Services { public class SchedulerService : BackgroundService { - private readonly ChannelWriter _channel; private readonly IEntityLocker _entityLocker; private readonly ILogger _logger; + private readonly ChannelWriter _plexWorkerChannel; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ChannelWriter _workerChannel; public SchedulerService( IServiceScopeFactory serviceScopeFactory, - ChannelWriter channel, + ChannelWriter workerChannel, + ChannelWriter plexWorkerChannel, IEntityLocker entityLocker, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; - _channel = channel; + _workerChannel = workerChannel; + _plexWorkerChannel = plexWorkerChannel; _entityLocker = entityLocker; _logger = logger; } @@ -74,7 +77,7 @@ namespace ErsatzTV.Services List playoutIds = await dbContext.Playouts.Map(p => p.Id).ToListAsync(cancellationToken); foreach (int playoutId in playoutIds) { - await _channel.WriteAsync(new BuildPlayout(playoutId), cancellationToken); + await _workerChannel.WriteAsync(new BuildPlayout(playoutId), cancellationToken); } } @@ -92,7 +95,7 @@ namespace ErsatzTV.Services { if (_entityLocker.LockLibrary(libraryId)) { - await _channel.WriteAsync( + await _workerChannel.WriteAsync( new ScanLocalLibraryIfNeeded(libraryId), cancellationToken); } @@ -112,7 +115,7 @@ namespace ErsatzTV.Services { if (_entityLocker.LockLibrary(library.Id)) { - await _channel.WriteAsync( + await _plexWorkerChannel.WriteAsync( new SynchronizePlexLibraryByIdIfNeeded(library.Id), cancellationToken); } @@ -120,6 +123,6 @@ namespace ErsatzTV.Services } private ValueTask RebuildSearchIndex(CancellationToken cancellationToken) => - _channel.WriteAsync(new RebuildSearchIndex(), cancellationToken); + _workerChannel.WriteAsync(new RebuildSearchIndex(), cancellationToken); } } diff --git a/ErsatzTV/Services/WorkerService.cs b/ErsatzTV/Services/WorkerService.cs index 0a2e7b5d2..cdde0a643 100644 --- a/ErsatzTV/Services/WorkerService.cs +++ b/ErsatzTV/Services/WorkerService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using ErsatzTV.Application; using ErsatzTV.Application.MediaSources.Commands; using ErsatzTV.Application.Playouts.Commands; -using ErsatzTV.Application.Plex.Commands; using ErsatzTV.Application.Search.Commands; using ErsatzTV.Core; using LanguageExt; @@ -70,19 +69,6 @@ namespace ErsatzTV.Services scanLocalLibrary.LibraryId, error.Value)); break; - case ISynchronizePlexLibraryById synchronizePlexLibraryById: - Either result = await mediator.Send( - synchronizePlexLibraryById, - cancellationToken); - result.BiIter( - name => _logger.LogDebug( - "Done synchronizing plex library {Name}", - name), - error => _logger.LogWarning( - "Unable to synchronize plex library {LibraryId}: {Error}", - synchronizePlexLibraryById.PlexLibraryId, - error.Value)); - break; case RebuildSearchIndex rebuildSearchIndex: await mediator.Send(rebuildSearchIndex, cancellationToken); break; diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 1f95d3633..b25b73be7 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -33,6 +33,7 @@ using ErsatzTV.Services; using ErsatzTV.Services.RunOnce; using FluentValidation.AspNetCore; using MediatR; +using MediatR.Courier.DependencyInjection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Data.Sqlite; @@ -88,6 +89,7 @@ namespace ErsatzTV services.AddServerSideBlazor(); services.AddMudServices(); + services.AddCourier(Assembly.GetAssembly(typeof(LibraryScanProgress))); Log.Logger.Information( "ErsatzTV version {Version}",