diff --git a/ErsatzTV.Application/Libraries/Queries/GetAllLibrariesHandler.cs b/ErsatzTV.Application/Libraries/Queries/GetAllLibrariesHandler.cs index 7672ae20..64047f08 100644 --- a/ErsatzTV.Application/Libraries/Queries/GetAllLibrariesHandler.cs +++ b/ErsatzTV.Application/Libraries/Queries/GetAllLibrariesHandler.cs @@ -18,7 +18,11 @@ namespace ErsatzTV.Application.Libraries.Queries public Task> Handle(GetAllLibraries request, CancellationToken cancellationToken) => _libraryRepository.GetAll() - .Map(list => list.Filter(ShouldIncludeLibrary).Map(ProjectToViewModel).ToList()); + .Map( + list => list.Filter(ShouldIncludeLibrary) + .OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1) + .ThenBy(l => l.MediaKind) + .Map(ProjectToViewModel).ToList()); private static bool ShouldIncludeLibrary(Library library) => library switch diff --git a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs index 140844bd..91deba3a 100644 --- a/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs +++ b/ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs @@ -7,7 +7,8 @@ namespace ErsatzTV.Application.MediaCards List MovieCards, List ShowCards, List SeasonCards, - List EpisodeCards) + List EpisodeCards, + List MusicVideoCards) { public bool UseCustomPlaybackOrder { get; set; } } diff --git a/ErsatzTV.Application/MediaCards/Mapper.cs b/ErsatzTV.Application/MediaCards/Mapper.cs index 2d80ca63..adcb5df6 100644 --- a/ErsatzTV.Application/MediaCards/Mapper.cs +++ b/ErsatzTV.Application/MediaCards/Mapper.cs @@ -52,6 +52,14 @@ namespace ErsatzTV.Application.MediaCards movieMetadata.SortTitle, GetPoster(movieMetadata)); + internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) => + new( + musicVideoMetadata.MusicVideoId, + $"{musicVideoMetadata.Title} ({musicVideoMetadata.Artist})", + musicVideoMetadata.Year?.ToString(), + musicVideoMetadata.SortTitle, + GetThumbnail(musicVideoMetadata)); + internal static CollectionCardResultsViewModel ProjectToViewModel(Collection collection) => new( @@ -64,6 +72,8 @@ namespace ErsatzTV.Application.MediaCards collection.MediaItems.OfType().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(), collection.MediaItems.OfType().Map(ProjectToViewModel).ToList(), collection.MediaItems.OfType().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head())) + .ToList(), + collection.MediaItems.OfType().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head())) .ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder }; private static int GetCustomIndex(Collection collection, int mediaItemId) => diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs new file mode 100644 index 00000000..ff0816da --- /dev/null +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardResultsViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Search; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCards +{ + public record MusicVideoCardResultsViewModel( + int Count, + List Cards, + Option PageMap); +} diff --git a/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs new file mode 100644 index 00000000..5c06ee4d --- /dev/null +++ b/ErsatzTV.Application/MediaCards/MusicVideoCardViewModel.cs @@ -0,0 +1,13 @@ +namespace ErsatzTV.Application.MediaCards +{ + public record MusicVideoCardViewModel + (int MusicVideoId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel( + MusicVideoId, + Title, + Subtitle, + SortTitle, + Poster) + { + public int CustomIndex { get; set; } + } +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs index c0df0030..4da9a73d 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollection.cs @@ -5,5 +5,9 @@ using LanguageExt; namespace ErsatzTV.Application.MediaCollections.Commands { public record AddItemsToCollection - (int CollectionId, List MovieIds, List ShowIds) : MediatR.IRequest>; + ( + int CollectionId, + List MovieIds, + List ShowIds, + List MusicVideoIds) : MediatR.IRequest>; } diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs index 835a8001..692ecaeb 100644 --- a/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs +++ b/ErsatzTV.Application/MediaCollections/Commands/AddItemsToCollectionHandler.cs @@ -41,7 +41,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands { if (await _mediaCollectionRepository.AddMediaItems( request.CollectionId, - request.MovieIds.Append(request.ShowIds).ToList())) + request.MovieIds.Append(request.ShowIds).Append(request.MusicVideoIds).ToList())) { // rebuild all playouts that use this collection foreach (int playoutId in await _mediaCollectionRepository diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs new file mode 100644 index 00000000..b89e902b --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollection.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Core; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record AddMusicVideoToCollection + (int CollectionId, int MusicVideoId) : MediatR.IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs new file mode 100644 index 00000000..4dfea21e --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/AddMusicVideoToCollectionHandler.cs @@ -0,0 +1,68 @@ +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using ErsatzTV.Application.Playouts.Commands; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using LanguageExt; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + AddMusicVideoToCollectionHandler : MediatR.IRequestHandler> + { + private readonly ChannelWriter _channel; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly IMusicVideoRepository _musicVideoRepository; + + public AddMusicVideoToCollectionHandler( + IMediaCollectionRepository mediaCollectionRepository, + IMusicVideoRepository musicVideoRepository, + ChannelWriter channel) + { + _mediaCollectionRepository = mediaCollectionRepository; + _musicVideoRepository = musicVideoRepository; + _channel = channel; + } + + public Task> Handle( + AddMusicVideoToCollection request, + CancellationToken cancellationToken) => + Validate(request) + .MapT(_ => ApplyAddMusicVideoRequest(request)) + .Bind(v => v.ToEitherAsync()); + + private async Task ApplyAddMusicVideoRequest(AddMusicVideoToCollection request) + { + if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId)) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository + .PlayoutIdsUsingCollection(request.CollectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + + return Unit.Default; + } + + private async Task> Validate(AddMusicVideoToCollection request) => + (await CollectionMustExist(request), await ValidateMusicVideo(request)) + .Apply((_, _) => Unit.Default); + + private Task> CollectionMustExist(AddMusicVideoToCollection request) => + _mediaCollectionRepository.GetCollectionWithItems(request.CollectionId) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Collection does not exist.")); + + private Task> ValidateMusicVideo(AddMusicVideoToCollection request) => + LoadMusicVideo(request) + .MapT(_ => Unit.Default) + .Map(v => v.ToValidation("Music video does not exist")); + + private Task> LoadMusicVideo(AddMusicVideoToCollection request) => + _musicVideoRepository.GetMusicVideo(request.MusicVideoId); + } +} diff --git a/ErsatzTV.Application/MediaItems/Mapper.cs b/ErsatzTV.Application/MediaItems/Mapper.cs index ef60cf27..175f69ac 100644 --- a/ErsatzTV.Application/MediaItems/Mapper.cs +++ b/ErsatzTV.Application/MediaItems/Mapper.cs @@ -1,5 +1,4 @@ -using System; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.MediaItems { @@ -8,59 +7,6 @@ namespace ErsatzTV.Application.MediaItems internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) => new(mediaItem.Id, mediaItem.LibraryPathId); - internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) => - mediaItem switch - { - Episode e => ProjectToSearchViewModel(e), - Movie m => ProjectToSearchViewModel(m), - _ => throw new ArgumentOutOfRangeException() - }; - - private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Episode mediaItem) => - new( - mediaItem.Id, - GetLibraryName(mediaItem), - "TV Show", - GetDisplayTitle(mediaItem), - GetDisplayDuration(mediaItem)); - - private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Movie mediaItem) => - new( - mediaItem.Id, - GetLibraryName(mediaItem), - "Movie", - GetDisplayTitle(mediaItem), - GetDisplayDuration(mediaItem)); - - - private static string GetDisplayTitle(MediaItem mediaItem) => - mediaItem switch - { - Episode e => e.EpisodeMetadata.HeadOrNone() - .Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}") - .IfNone("[unknown episode]"), - Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"), - _ => string.Empty - }; - - private static string GetDisplayDuration(MediaItem mediaItem) - { - MediaVersion version = mediaItem switch - { - Movie m => m.MediaVersions.Head(), - Episode e => e.MediaVersions.Head(), - _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) - }; - - return string.Format( - version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", - version.Duration); - } - - // TODO: fix this when search is reimplemented - private static string GetLibraryName(MediaItem item) => - "Library Name"; - public static NamedMediaItemViewModel ProjectToViewModel(Show show) => new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???")); diff --git a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs index da4999e1..b4bd2e74 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibrary.cs @@ -8,15 +8,24 @@ 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 676e58cc..38b01643 100644 --- a/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs +++ b/ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs @@ -16,13 +16,15 @@ 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 IMovieFolderScanner _movieFolderScanner; + private readonly IMusicVideoFolderScanner _musicVideoFolderScanner; private readonly ITelevisionFolderScanner _televisionFolderScanner; public ScanLocalLibraryHandler( @@ -30,6 +32,7 @@ namespace ErsatzTV.Application.MediaSources.Commands IConfigElementRepository configElementRepository, IMovieFolderScanner movieFolderScanner, ITelevisionFolderScanner televisionFolderScanner, + IMusicVideoFolderScanner musicVideoFolderScanner, IEntityLocker entityLocker, ILogger logger) { @@ -37,10 +40,15 @@ namespace ErsatzTV.Application.MediaSources.Commands _configElementRepository = configElementRepository; _movieFolderScanner = movieFolderScanner; _televisionFolderScanner = televisionFolderScanner; + _musicVideoFolderScanner = musicVideoFolderScanner; _entityLocker = entityLocker; _logger = logger; } + public Task> Handle( + ForceRescanLocalLibrary request, + CancellationToken cancellationToken) => Handle(request); + public Task> Handle( ForceScanLocalLibrary request, CancellationToken cancellationToken) => Handle(request); @@ -57,7 +65,7 @@ namespace ErsatzTV.Application.MediaSources.Commands private async Task PerformScan(RequestParameters parameters) { - (LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters; + (LocalLibrary localLibrary, string ffprobePath, bool forceScan, bool rescan) = parameters; var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero); if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6)) @@ -65,15 +73,20 @@ namespace ErsatzTV.Application.MediaSources.Commands var sw = new Stopwatch(); sw.Start(); + DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan; + foreach (LibraryPath libraryPath in localLibrary.Paths) { switch (localLibrary.MediaKind) { case LibraryMediaKind.Movies: - await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan); + await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); break; case LibraryMediaKind.Shows: - await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan); + await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); + break; + case LibraryMediaKind.MusicVideos: + await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan); break; } } @@ -104,7 +117,8 @@ namespace ErsatzTV.Application.MediaSources.Commands (library, ffprobePath) => new RequestParameters( library, ffprobePath, - request.ForceScan)); + request.ForceScan, + request.Rescan)); private Task> LocalLibraryMustExist( IScanLocalLibrary request) => @@ -119,6 +133,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); + private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan); } } diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 6a7eea1d..5bbe18d9 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -36,6 +36,9 @@ namespace ErsatzTV.Application.Playouts .IfNone("[unknown episode]"); case Movie m: return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"); + case MusicVideo mv: + return mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}") + .IfNone("[unknown music video]"); default: return string.Empty; } @@ -47,6 +50,7 @@ namespace ErsatzTV.Application.Playouts { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) }; diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs new file mode 100644 index 00000000..8d7fd6df --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideos.cs @@ -0,0 +1,8 @@ +using ErsatzTV.Application.MediaCards; +using MediatR; + +namespace ErsatzTV.Application.Search.Queries +{ + public record QuerySearchIndexMusicVideos + (string Query, int PageNumber, int PageSize) : IRequest; +} diff --git a/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs new file mode 100644 index 00000000..59739565 --- /dev/null +++ b/ErsatzTV.Application/Search/Queries/QuerySearchIndexMusicVideosHandler.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ErsatzTV.Application.MediaCards; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Search; +using LanguageExt; +using MediatR; +using static ErsatzTV.Application.MediaCards.Mapper; + +namespace ErsatzTV.Application.Search.Queries +{ + public class + QuerySearchIndexMusicVideosHandler : IRequestHandler + { + private readonly IMusicVideoRepository _musicVideoRepository; + private readonly ISearchIndex _searchIndex; + + public QuerySearchIndexMusicVideosHandler(ISearchIndex searchIndex, IMusicVideoRepository musicVideoRepository) + { + _searchIndex = searchIndex; + _musicVideoRepository = musicVideoRepository; + } + + public async Task Handle( + QuerySearchIndexMusicVideos request, + CancellationToken cancellationToken) + { + SearchResult searchResult = await _searchIndex.Search( + request.Query, + (request.PageNumber - 1) * request.PageSize, + request.PageSize); + + List items = await _musicVideoRepository + .GetMusicVideosForCards(searchResult.Items.Map(i => i.Id).ToList()) + .Map(list => list.Map(ProjectToViewModel).ToList()); + + return new MusicVideoCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap); + } + } +} diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 9b80474e..384ae980 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -56,6 +56,7 @@ namespace ErsatzTV.Application.Streaming.Queries { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath)) }; @@ -153,6 +154,7 @@ namespace ErsatzTV.Application.Streaming.Queries { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(playoutItem)) }; diff --git a/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs b/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs index 4ac8aab6..a5571f42 100644 --- a/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs +++ b/ErsatzTV.Core/Domain/Library/LibraryMediaKind.cs @@ -3,6 +3,7 @@ public enum LibraryMediaKind { Movies = 1, - Shows = 2 + Shows = 2, + MusicVideos = 3 } } diff --git a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs index 9b0b145b..40c0685f 100644 --- a/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs +++ b/ErsatzTV.Core/Domain/MediaItem/MediaVersion.cs @@ -13,16 +13,6 @@ namespace ErsatzTV.Core.Domain public TimeSpan Duration { get; set; } public string SampleAspectRatio { get; set; } public string DisplayAspectRatio { get; set; } - - [Obsolete("Use MediaSource instead")] - public string VideoCodec { get; set; } - - [Obsolete("Use MediaSource instead")] - public string VideoProfile { get; set; } - - [Obsolete("Use MediaSource instead")] - public string AudioCodec { get; set; } - public VideoScanKind VideoScanKind { get; set; } public DateTime DateAdded { get; set; } public DateTime DateUpdated { get; set; } diff --git a/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs new file mode 100644 index 00000000..396ffc91 --- /dev/null +++ b/ErsatzTV.Core/Domain/MediaItem/MusicVideo.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Domain +{ + public class MusicVideo : MediaItem + { + public List MusicVideoMetadata { get; set; } + public List MediaVersions { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/Metadata/MusicVideoMetadata.cs b/ErsatzTV.Core/Domain/Metadata/MusicVideoMetadata.cs new file mode 100644 index 00000000..cdd22e34 --- /dev/null +++ b/ErsatzTV.Core/Domain/Metadata/MusicVideoMetadata.cs @@ -0,0 +1,11 @@ +namespace ErsatzTV.Core.Domain +{ + public class MusicVideoMetadata : Metadata + { + public string Album { get; set; } + public string Plot { get; set; } + public string Artist { get; set; } + public int MusicVideoId { get; set; } + public MusicVideo MusicVideo { get; set; } + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs index 402852eb..b95e7e4c 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs @@ -332,11 +332,12 @@ namespace ErsatzTV.Core.FFmpeg public FFmpegProcessBuilder WithVideoTrackTimeScale(Option videoTrackTimeScale) { - videoTrackTimeScale.IfSome(timeScale => - { - _arguments.Add("-video_track_timescale"); - _arguments.Add($"{timeScale}"); - }); + videoTrackTimeScale.IfSome( + timeScale => + { + _arguments.Add("-video_track_timescale"); + _arguments.Add($"{timeScale}"); + }); return this; } diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 156cbe72..6299ac90 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -1,11 +1,13 @@ using System.Threading.Tasks; using ErsatzTV.Core.Domain; +using LanguageExt; namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalMetadataProvider { Task GetMetadataForShow(string showFolder); + Task> GetMetadataForMusicVideo(string filePath); Task RefreshSidecarMetadata(MediaItem mediaItem, string path); Task RefreshSidecarMetadata(Show televisionShow, string showFolder); Task RefreshFallbackMetadata(MediaItem mediaItem); diff --git a/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs new file mode 100644 index 00000000..bde026da --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/IMusicVideoFolderScanner.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface IMusicVideoFolderScanner + { + Task> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan); + } +} diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs new file mode 100644 index 00000000..2450ee75 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Repositories/IMusicVideoRepository.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Metadata; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Repositories +{ + public interface IMusicVideoRepository + { + Task> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata); + + Task>> Add( + LibraryPath libraryPath, + string filePath, + MusicVideoMetadata metadata); + + Task> FindMusicVideoPaths(LibraryPath libraryPath); + Task> DeleteByPath(LibraryPath libraryPath, string path); + Task AddGenre(MusicVideoMetadata metadata, Genre genre); + Task AddTag(MusicVideoMetadata metadata, Tag tag); + Task AddStudio(MusicVideoMetadata metadata, Studio studio); + Task> GetMusicVideosForCards(List ids); + Task> GetMusicVideo(int musicVideoId); + } +} diff --git a/ErsatzTV.Core/Iptv/ChannelGuide.cs b/ErsatzTV.Core/Iptv/ChannelGuide.cs index ab340cfb..f599a49a 100644 --- a/ErsatzTV.Core/Iptv/ChannelGuide.cs +++ b/ErsatzTV.Core/Iptv/ChannelGuide.cs @@ -221,6 +221,8 @@ namespace ErsatzTV.Core.Iptv .IfNone("[unknown movie]"), Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty) .IfNone("[unknown show]"), + MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}") + .IfNone("[unknown music video]"), _ => "[unknown]" }; } @@ -253,6 +255,8 @@ namespace ErsatzTV.Core.Iptv Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty), Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty) .IfNone(string.Empty), + MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Map(mvm => mvm.Plot ?? string.Empty) + .IfNone(string.Empty), _ => string.Empty }; } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index 21f71e58..daa5e547 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -72,6 +72,7 @@ namespace ErsatzTV.Core.Metadata { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) }; diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index e44e6dff..39a2065f 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -18,18 +18,21 @@ namespace ErsatzTV.Core.Metadata private static readonly XmlSerializer MovieSerializer = new(typeof(MovieNfo)); private static readonly XmlSerializer EpisodeSerializer = new(typeof(TvShowEpisodeNfo)); private static readonly XmlSerializer TvShowSerializer = new(typeof(TvShowNfo)); + private static readonly XmlSerializer MusicVideoSerializer = new(typeof(MusicVideoNfo)); private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMetadataRepository _metadataRepository; private readonly IMovieRepository _movieRepository; + private readonly IMusicVideoRepository _musicVideoRepository; private readonly ITelevisionRepository _televisionRepository; public LocalMetadataProvider( IMetadataRepository metadataRepository, IMovieRepository movieRepository, ITelevisionRepository televisionRepository, + IMusicVideoRepository musicVideoRepository, IFallbackMetadataProvider fallbackMetadataProvider, ILocalFileSystem localFileSystem, ILogger logger) @@ -37,6 +40,7 @@ namespace ErsatzTV.Core.Metadata _metadataRepository = metadataRepository; _movieRepository = movieRepository; _televisionRepository = televisionRepository; + _musicVideoRepository = musicVideoRepository; _fallbackMetadataProvider = fallbackMetadataProvider; _localFileSystem = localFileSystem; _logger = logger; @@ -65,6 +69,23 @@ namespace ErsatzTV.Core.Metadata }); } + public async Task> GetMetadataForMusicVideo(string filePath) + { + string nfoFileName = Path.ChangeExtension(filePath, "nfo"); + Option maybeMetadata = None; + if (_localFileSystem.FileExists(nfoFileName)) + { + maybeMetadata = await LoadMusicVideoMetadata(nfoFileName); + } + + return maybeMetadata.Map( + metadata => + { + metadata.SortTitle = _fallbackMetadataProvider.GetSortTitle(metadata.Title); + return metadata; + }); + } + public Task RefreshSidecarMetadata(MediaItem mediaItem, string path) => mediaItem switch { @@ -78,6 +99,11 @@ namespace ErsatzTV.Core.Metadata maybeMetadata => maybeMetadata.Match( metadata => ApplyMetadataUpdate(m, metadata), () => Task.FromResult(false))), + MusicVideo mv => LoadMetadata(mv, path) + .Bind( + maybeMetadata => maybeMetadata.Match( + metadata => ApplyMetadataUpdate(mv, metadata), + () => Task.FromResult(false))), _ => Task.FromResult(false) }; @@ -98,6 +124,37 @@ namespace ErsatzTV.Core.Metadata public Task RefreshFallbackMetadata(Show televisionShow, string showFolder) => ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)); + private async Task> LoadMusicVideoMetadata(string nfoFileName) + { + try + { + await using FileStream fileStream = File.Open(nfoFileName, FileMode.Open, FileAccess.Read); + Option maybeNfo = MusicVideoSerializer.Deserialize(fileStream) as MusicVideoNfo; + return maybeNfo.Match>( + nfo => new MusicVideoMetadata + { + MetadataKind = MetadataKind.Sidecar, + DateAdded = DateTime.UtcNow, + DateUpdated = File.GetLastWriteTimeUtc(nfoFileName), + Artist = nfo.Artist, + Album = nfo.Album, + Title = nfo.Title, + Plot = nfo.Plot, + Year = GetYear(nfo.Year, nfo.Premiered), + ReleaseDate = GetAired(nfo.Year, nfo.Premiered), + Genres = nfo.Genres.Map(g => new Genre { Name = g }).ToList(), + Tags = nfo.Tags.Map(t => new Tag { Name = t }).ToList(), + Studios = nfo.Studios.Map(s => new Studio { Name = s }).ToList() + }, + None); + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Failed to read music video nfo metadata from {Path}", nfoFileName); + return None; + } + } + private async Task ApplyMetadataUpdate(Episode episode, Tuple metadataEpisodeNumber) { (EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber; @@ -344,6 +401,106 @@ namespace ErsatzTV.Core.Metadata return await _metadataRepository.Add(metadata); }); + private Task ApplyMetadataUpdate(MusicVideo musicVideo, MusicVideoMetadata metadata) => + Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match( + async existing => + { + var updated = false; + + existing.Artist = metadata.Artist; + existing.Title = metadata.Title; + existing.Year = metadata.Year; + existing.Plot = metadata.Plot; + existing.Album = metadata.Album; + + if (existing.DateAdded == DateTime.MinValue) + { + existing.DateAdded = metadata.DateAdded; + } + + existing.DateUpdated = metadata.DateUpdated; + existing.MetadataKind = metadata.MetadataKind; + existing.OriginalTitle = metadata.OriginalTitle; + existing.ReleaseDate = metadata.ReleaseDate; + existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) + ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + : metadata.SortTitle; + + foreach (Genre genre in existing.Genres.Filter(g => metadata.Genres.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existing.Genres.Remove(genre); + if (await _metadataRepository.RemoveGenre(genre)) + { + updated = true; + } + } + + foreach (Genre genre in metadata.Genres.Filter(g => existing.Genres.All(g2 => g2.Name != g.Name)) + .ToList()) + { + existing.Genres.Add(genre); + if (await _musicVideoRepository.AddGenre(existing, genre)) + { + updated = true; + } + } + + foreach (Tag tag in existing.Tags.Filter(t => metadata.Tags.All(t2 => t2.Name != t.Name)) + .ToList()) + { + existing.Tags.Remove(tag); + if (await _metadataRepository.RemoveTag(tag)) + { + updated = true; + } + } + + foreach (Tag tag in metadata.Tags.Filter(t => existing.Tags.All(t2 => t2.Name != t.Name)) + .ToList()) + { + existing.Tags.Add(tag); + if (await _musicVideoRepository.AddTag(existing, tag)) + { + updated = true; + } + } + + foreach (Studio studio in existing.Studios + .Filter(s => metadata.Studios.All(s2 => s2.Name != s.Name)) + .ToList()) + { + existing.Studios.Remove(studio); + if (await _metadataRepository.RemoveStudio(studio)) + { + updated = true; + } + } + + foreach (Studio studio in metadata.Studios + .Filter(s => existing.Studios.All(s2 => s2.Name != s.Name)) + .ToList()) + { + existing.Studios.Add(studio); + if (await _musicVideoRepository.AddStudio(existing, studio)) + { + updated = true; + } + } + + return await _metadataRepository.Update(existing) || updated; + }, + async () => + { + metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) + ? _fallbackMetadataProvider.GetSortTitle(metadata.Title) + : metadata.SortTitle; + metadata.MusicVideoId = musicVideo.Id; + musicVideo.MusicVideoMetadata = new List { metadata }; + + return await _metadataRepository.Add(metadata); + }); + private async Task> LoadMetadata(Movie mediaItem, string nfoFileName) { if (nfoFileName == null || !File.Exists(nfoFileName)) @@ -377,6 +534,17 @@ namespace ErsatzTV.Core.Metadata return await LoadTelevisionShowMetadata(nfoFileName); } + private async Task> LoadMetadata(MusicVideo musicVideo, string nfoFileName) + { + if (nfoFileName == null || !File.Exists(nfoFileName)) + { + _logger.LogDebug("NFO file does not exist at {Path}", nfoFileName); + return None; + } + + return await LoadMusicVideoMetadata(nfoFileName); + } + private async Task> LoadTelevisionShowMetadata(string nfoFileName) { try @@ -589,5 +757,36 @@ namespace ErsatzTV.Core.Metadata [XmlElement("plot")] public string Plot { get; set; } } + + [XmlRoot("musicvideo")] + public class MusicVideoNfo + { + [XmlElement("artist")] + public string Artist { get; set; } + + [XmlElement("title")] + public string Title { get; set; } + + [XmlElement("album")] + public string Album { get; set; } + + [XmlElement("plot")] + public string Plot { get; set; } + + [XmlElement("premiered")] + public string Premiered { get; set; } + + [XmlElement("year")] + public int Year { get; set; } + + [XmlElement("genre")] + public List Genres { get; set; } + + [XmlElement("tag")] + public List Tags { get; set; } + + [XmlElement("studio")] + public List Studios { get; set; } + } } } diff --git a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs index 19d5d087..75c3a574 100644 --- a/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs @@ -37,6 +37,7 @@ namespace ErsatzTV.Core.Metadata { Movie m => m.MediaVersions.Head().MediaFiles.Head().Path, Episode e => e.MediaVersions.Head().MediaFiles.Head().Path, + MusicVideo mv => mv.MediaVersions.Head().MediaFiles.Head().Path, _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) }; @@ -63,6 +64,7 @@ namespace ErsatzTV.Core.Metadata { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) }; diff --git a/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs new file mode 100644 index 00000000..9208fb17 --- /dev/null +++ b/ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Search; +using LanguageExt; +using Microsoft.Extensions.Logging; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Core.Metadata +{ + public class MusicVideoFolderScanner : LocalFolderScanner, IMusicVideoFolderScanner + { + private readonly ILocalFileSystem _localFileSystem; + private readonly ILocalMetadataProvider _localMetadataProvider; + private readonly ILogger _logger; + private readonly IMusicVideoRepository _musicVideoRepository; + private readonly ISearchIndex _searchIndex; + + public MusicVideoFolderScanner( + ILocalFileSystem localFileSystem, + ILocalStatisticsProvider localStatisticsProvider, + ILocalMetadataProvider localMetadataProvider, + IMetadataRepository metadataRepository, + IImageCache imageCache, + ISearchIndex searchIndex, + IMusicVideoRepository musicVideoRepository, + ILogger logger) : base( + localFileSystem, + localStatisticsProvider, + metadataRepository, + imageCache, + logger) + { + _localFileSystem = localFileSystem; + _localMetadataProvider = localMetadataProvider; + _searchIndex = searchIndex; + _musicVideoRepository = musicVideoRepository; + _logger = logger; + } + + public async Task> ScanFolder( + LibraryPath libraryPath, + string ffprobePath, + DateTimeOffset lastScan) + { + if (!_localFileSystem.IsLibraryPathAccessible(libraryPath)) + { + return new MediaSourceInaccessible(); + } + + var folderQueue = new Queue(); + foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path).OrderBy(identity)) + { + folderQueue.Enqueue(folder); + } + + while (folderQueue.Count > 0) + { + string movieFolder = folderQueue.Dequeue(); + + var allFiles = _localFileSystem.ListFiles(movieFolder) + .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))) + .Filter( + f => !ExtraFiles.Any( + e => Path.GetFileNameWithoutExtension(f).EndsWith(e, StringComparison.OrdinalIgnoreCase))) + .ToList(); + + if (allFiles.Count == 0) + { + foreach (string subdirectory in _localFileSystem.ListSubdirectories(movieFolder).OrderBy(identity)) + { + folderQueue.Enqueue(subdirectory); + } + + continue; + } + + if (_localFileSystem.GetLastWriteTime(movieFolder) < lastScan) + { + continue; + } + + foreach (string file in allFiles.OrderBy(identity)) + { + // TODO: figure out how to rebuild playouts + Either> maybeMusicVideo = + await FindOrCreateMusicVideo(libraryPath, file) + .BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath)) + .BindT(UpdateMetadata) + .BindT(UpdateThumbnail); + + await maybeMusicVideo.Match( + async result => + { + if (result.IsAdded) + { + await _searchIndex.AddItems(new List { result.Item }); + } + else if (result.IsUpdated) + { + await _searchIndex.UpdateItems(new List { result.Item }); + } + }, + error => + { + _logger.LogWarning("Error processing music video at {Path}: {Error}", file, error.Value); + return Task.CompletedTask; + }); + } + } + + foreach (string path in await _musicVideoRepository.FindMusicVideoPaths(libraryPath)) + { + if (!_localFileSystem.FileExists(path)) + { + _logger.LogInformation("Removing missing music video at {Path}", path); + List ids = await _musicVideoRepository.DeleteByPath(libraryPath, path); + await _searchIndex.RemoveItems(ids); + } + } + + return Unit.Default; + } + + private async Task>> FindOrCreateMusicVideo( + LibraryPath libraryPath, + string filePath) + { + Option maybeMetadata = await _localMetadataProvider.GetMetadataForMusicVideo(filePath); + return await maybeMetadata.Match( + async metadata => + { + Option maybeMusicVideo = + await _musicVideoRepository.GetByMetadata(libraryPath, metadata); + return await maybeMusicVideo.Match( + musicVideo => + Right>( + new MediaItemScanResult(musicVideo)) + .AsTask(), + async () => await _musicVideoRepository.Add(libraryPath, filePath, metadata)); + }, + () => Left>( + BaseError.New("Unable to locate metadata for music video")).AsTask()); + } + + private async Task>> UpdateMetadata( + MediaItemScanResult result) + { + try + { + MusicVideo musicVideo = result.Item; + return await LocateNfoFile(musicVideo).Match>>>( + async nfoFile => + { + bool shouldUpdate = Optional(musicVideo.MusicVideoMetadata).Flatten().HeadOrNone().Match( + m => m.MetadataKind == MetadataKind.Fallback || + m.DateUpdated < _localFileSystem.GetLastWriteTime(nfoFile), + true); + + if (shouldUpdate) + { + _logger.LogDebug("Refreshing {Attribute} from {Path}", "Sidecar Metadata", nfoFile); + if (await _localMetadataProvider.RefreshSidecarMetadata(musicVideo, nfoFile)) + { + result.IsUpdated = true; + } + } + + return result; + }, + () => Left>( + BaseError.New("Unable to locate metadata for music video")).AsTask()); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + private Option LocateNfoFile(MusicVideo musicVideo) + { + string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; + return Optional(Path.ChangeExtension(path, "nfo")) + .Filter(s => _localFileSystem.FileExists(s)) + .HeadOrNone(); + } + + private async Task>> UpdateThumbnail( + MediaItemScanResult result) + { + try + { + MusicVideo musicVideo = result.Item; + await LocateThumbnail(musicVideo).IfSomeAsync( + async thumbnailFile => + { + MusicVideoMetadata metadata = musicVideo.MusicVideoMetadata.Head(); + await RefreshArtwork(thumbnailFile, metadata, ArtworkKind.Thumbnail); + }); + + return result; + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + private Option LocateThumbnail(MusicVideo musicVideo) + { + string path = musicVideo.MediaVersions.Head().MediaFiles.Head().Path; + return ImageFileExtensions + .Map(ext => Path.ChangeExtension(path, ext)) + .Filter(f => _localFileSystem.FileExists(f)) + .HeadOrNone(); + } + } +} diff --git a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs index adc3b028..73ca4612 100644 --- a/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ChronologicalMediaCollectionEnumerator.cs @@ -48,6 +48,9 @@ namespace ErsatzTV.Core.Scheduling Movie m => m.MovieMetadata.HeadOrNone().Match( mm => mm.ReleaseDate ?? DateTime.MaxValue, () => DateTime.MaxValue), + MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match( + mvm => mvm.ReleaseDate ?? DateTime.MaxValue, + () => DateTime.MaxValue), _ => DateTime.MaxValue }; @@ -59,6 +62,9 @@ namespace ErsatzTV.Core.Scheduling Movie m => m.MovieMetadata.HeadOrNone().Match( mm => mm.ReleaseDate ?? DateTime.MaxValue, () => DateTime.MaxValue), + MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match( + mvm => mvm.ReleaseDate ?? DateTime.MaxValue, + () => DateTime.MaxValue), _ => DateTime.MaxValue }; diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index a56bd343..3066e4b8 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -99,6 +99,8 @@ namespace ErsatzTV.Core.Scheduling TimeSpan.Zero, Episode e => e.MediaVersions.HeadOrNone().Map(mv => mv.Duration).IfNone(TimeSpan.Zero) == TimeSpan.Zero, + MusicVideo mv => mv.MediaVersions.HeadOrNone().Map(v => v.Duration).IfNone(TimeSpan.Zero) == + TimeSpan.Zero, _ => true })).Map(c => c.Key); if (zeroDurationCollection.IsSome) @@ -180,6 +182,7 @@ namespace ErsatzTV.Core.Scheduling { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(mediaItem)) }; @@ -240,6 +243,7 @@ namespace ErsatzTV.Core.Scheduling { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem)) }; @@ -274,6 +278,7 @@ namespace ErsatzTV.Core.Scheduling { Movie m => m.MediaVersions.Head(), Episode e => e.MediaVersions.Head(), + MusicVideo mv => mv.MediaVersions.Head(), _ => throw new ArgumentOutOfRangeException(nameof(peekMediaItem)) }; @@ -479,6 +484,9 @@ namespace ErsatzTV.Core.Scheduling Movie m => m.MovieMetadata.HeadOrNone().Match( mm => mm.Title ?? string.Empty, () => "[unknown movie]"), + MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match( + mvm => $"{mvm.Artist} - {mvm.Title}", + () => "[unknown music video]"), _ => string.Empty }; diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MusicVideoConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MusicVideoConfiguration.cs new file mode 100644 index 00000000..aec65afb --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/MediaItem/MusicVideoConfiguration.cs @@ -0,0 +1,23 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class MusicVideoConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MusicVideo"); + + builder.HasMany(m => m.MusicVideoMetadata) + .WithOne(m => m.MusicVideo) + .HasForeignKey(m => m.MusicVideoId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(m => m.MediaVersions) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MusicVideoMetadataConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MusicVideoMetadataConfiguration.cs new file mode 100644 index 00000000..f52dbb9f --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Configurations/Metadata/MusicVideoMetadataConfiguration.cs @@ -0,0 +1,30 @@ +using ErsatzTV.Core.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ErsatzTV.Infrastructure.Data.Configurations +{ + public class MusicVideoMetadataConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("MusicVideoMetadata"); + + builder.HasMany(mm => mm.Artwork) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Genres) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Tags) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(mm => mm.Studios) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs index efdc9a8c..a9a4dd6b 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/LibraryRepository.cs @@ -52,7 +52,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public Task> GetAll() { using TvContext context = _dbContextFactory.CreateDbContext(); - return context.Libraries.ToListAsync(); + return context.Libraries + .AsNoTracking() + .Include(l => l.MediaSource) + .ToListAsync(); } public Task UpdateLastScan(Library library) => _dbConnection.ExecuteAsync( diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 5f0a23f8..ab0ef50a 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -104,6 +104,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(c => c.MediaItems) .ThenInclude(i => (i as Movie).MovieMetadata) .Include(c => c.MediaItems) + .ThenInclude(i => (i as MusicVideo).MusicVideoMetadata) + .Include(c => c.MediaItems) .ThenInclude(i => (i as Show).ShowMetadata) .Include(c => c.MediaItems) .ThenInclude(i => (i as Season).Show) @@ -128,6 +130,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ThenInclude(i => (i as Movie).MovieMetadata) .ThenInclude(mm => mm.Artwork) .Include(c => c.MediaItems) + .ThenInclude(i => (i as MusicVideo).MusicVideoMetadata) + .ThenInclude(mvm => mvm.Artwork) + .Include(c => c.MediaItems) .ThenInclude(i => (i as Show).ShowMetadata) .ThenInclude(sm => sm.Artwork) .Include(c => c.MediaItems) @@ -196,6 +201,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories result.AddRange(await GetShowItems(collection)); result.AddRange(await GetSeasonItems(collection)); result.AddRange(await GetEpisodeItems(collection)); + result.AddRange(await GetMusicVideoItems(collection)); return result.Distinct().ToList(); } @@ -215,6 +221,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .ToListAsync(); } + private async Task> GetMusicVideoItems(Collection collection) + { + IEnumerable ids = await _dbConnection.QueryAsync( + @"SELECT m.Id FROM CollectionItem ci + INNER JOIN MusicVideo m ON m.Id = ci.MediaItemId + WHERE ci.CollectionId = @CollectionId", + new { CollectionId = collection.Id }); + + return await _dbContext.MusicVideos + .Include(m => m.MusicVideoMetadata) + .Include(m => m.MediaVersions) + .Filter(m => ids.Contains(m.Id)) + .ToListAsync(); + } + private async Task> GetShowItems(Collection collection) { IEnumerable ids = await _dbConnection.QueryAsync( diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs index 9bffb49c..fa5efc53 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs @@ -156,6 +156,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", parameters) .ToUnit(), + MusicVideoMetadata => _dbConnection.ExecuteAsync( + @"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path) + VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)", + parameters) + .ToUnit(), _ => Task.FromResult(Unit.Default) }; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs new file mode 100644 index 00000000..2bd6d9ae --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/Repositories/MusicVideoRepository.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using Dapper; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Metadata; +using LanguageExt; +using Microsoft.EntityFrameworkCore; +using static LanguageExt.Prelude; + +namespace ErsatzTV.Infrastructure.Data.Repositories +{ + public class MusicVideoRepository : IMusicVideoRepository + { + private readonly IDbConnection _dbConnection; + private readonly IDbContextFactory _dbContextFactory; + + public MusicVideoRepository(IDbContextFactory dbContextFactory, IDbConnection dbConnection) + { + _dbContextFactory = dbContextFactory; + _dbConnection = dbConnection; + } + + public async Task> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + Option maybeId = await dbContext.MusicVideoMetadata + .Where(s => s.Artist == metadata.Artist && s.Title == metadata.Title && s.Year == metadata.Year) + .Where(s => s.MusicVideo.LibraryPathId == libraryPath.Id) + .SingleOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.MusicVideoId); + + return await maybeId.Match( + id => + { + return dbContext.MusicVideos + .AsNoTracking() + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Artwork) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Genres) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Tags) + .Include(mv => mv.MusicVideoMetadata) + .ThenInclude(mvm => mvm.Studios) + .Include(mv => mv.LibraryPath) + .ThenInclude(lp => lp.Library) + .Include(mv => mv.MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .OrderBy(mv => mv.Id) + .SingleOrDefaultAsync(mv => mv.Id == id) + .Map(Optional); + }, + () => Option.None.AsTask()); + } + + public async Task>> Add( + LibraryPath libraryPath, + string filePath, + MusicVideoMetadata metadata) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + try + { + metadata.DateAdded = DateTime.UtcNow; + metadata.Genres ??= new List(); + metadata.Tags ??= new List(); + metadata.Studios ??= new List(); + var musicVideo = new MusicVideo + { + LibraryPathId = libraryPath.Id, + MusicVideoMetadata = new List { metadata }, + MediaVersions = new List + { + new() + { + MediaFiles = new List + { + new() { Path = filePath } + }, + Streams = new List() + } + } + }; + + await dbContext.MusicVideos.AddAsync(musicVideo); + await dbContext.SaveChangesAsync(); + await dbContext.Entry(musicVideo).Reference(s => s.LibraryPath).LoadAsync(); + await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync(); + + return new MediaItemScanResult(musicVideo) { IsAdded = true }; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } + } + + public Task> FindMusicVideoPaths(LibraryPath libraryPath) => + _dbConnection.QueryAsync( + @"SELECT MF.Path + FROM MediaFile MF + INNER JOIN MediaVersion MV on MF.MediaVersionId = MV.Id + INNER JOIN MusicVideo M on MV.MusicVideoId = M.Id + INNER JOIN MediaItem MI on M.Id = MI.Id + WHERE MI.LibraryPathId = @LibraryPathId", + new { LibraryPathId = libraryPath.Id }); + + public async Task> DeleteByPath(LibraryPath libraryPath, string path) + { + List ids = await _dbConnection.QueryAsync( + @"SELECT M.Id + FROM MusicVideo M + INNER JOIN MediaItem MI on M.Id = MI.Id + INNER JOIN MediaVersion MV on M.Id = MV.EpisodeId + INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId + WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path", + new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList()); + + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + foreach (int musicVideoId in ids) + { + MusicVideo musicVideo = await dbContext.MusicVideos.FindAsync(musicVideoId); + dbContext.MusicVideos.Remove(musicVideo); + } + + await dbContext.SaveChangesAsync(); + + return ids; + } + + public Task AddGenre(MusicVideoMetadata metadata, Genre genre) => + _dbConnection.ExecuteAsync( + "INSERT INTO Genre (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)", + new { genre.Name, MetadataId = metadata.Id }).Map(result => result > 0); + + public Task AddTag(MusicVideoMetadata metadata, Tag tag) => + _dbConnection.ExecuteAsync( + "INSERT INTO Tag (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)", + new { tag.Name, MetadataId = metadata.Id }).Map(result => result > 0); + + public Task AddStudio(MusicVideoMetadata metadata, Studio studio) => + _dbConnection.ExecuteAsync( + "INSERT INTO Studio (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)", + new { studio.Name, MetadataId = metadata.Id }).Map(result => result > 0); + + public async Task> GetMusicVideosForCards(List ids) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.MusicVideoMetadata + .AsNoTracking() + .Filter(mvm => ids.Contains(mvm.MusicVideoId)) + .Include(mvm => mvm.Artwork) + .OrderBy(mvm => mvm.SortTitle) + .ToListAsync(); + } + + public async Task> GetMusicVideo(int musicVideoId) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + return await dbContext.MusicVideos + .Include(m => m.MusicVideoMetadata) + .ThenInclude(m => m.Artwork) + .Include(m => m.MusicVideoMetadata) + .ThenInclude(m => m.Genres) + .Include(m => m.MusicVideoMetadata) + .ThenInclude(m => m.Tags) + .Include(m => m.MusicVideoMetadata) + .ThenInclude(m => m.Studios) + .OrderBy(m => m.Id) + .SingleOrDefaultAsync(m => m.Id == musicVideoId) + .Map(Optional); + } + } +} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs index eb28c386..082a26e0 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/PlayoutRepository.cs @@ -65,6 +65,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(i => i.MediaItem) .ThenInclude(mi => (mi as Movie).MediaVersions) .ThenInclude(mv => mv.Streams) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MediaVersions) + .ThenInclude(mv => mv.MediaFiles) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MediaVersions) + .ThenInclude(mv => mv.Streams) .AsNoTracking() .SingleOrDefaultAsync() .Map(Optional); @@ -89,6 +95,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories .Include(i => i.MediaItem) .ThenInclude(mi => (mi as Movie).MediaVersions) .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata) + .ThenInclude(mm => mm.Artwork) + .Include(i => i.MediaItem) + .ThenInclude(mi => (mi as MusicVideo).MediaVersions) + .Include(i => i.MediaItem) .ThenInclude(mi => (mi as Episode).EpisodeMetadata) .ThenInclude(em => em.Artwork) .Include(i => i.MediaItem) diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index b2e3a982..53ed3803 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -27,6 +27,8 @@ namespace ErsatzTV.Infrastructure.Data public DbSet MediaFiles { get; set; } public DbSet Movies { get; set; } public DbSet MovieMetadata { get; set; } + public DbSet MusicVideos { get; set; } + public DbSet MusicVideoMetadata { get; set; } public DbSet Shows { get; set; } public DbSet ShowMetadata { get; set; } public DbSet Seasons { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210401105350_Add_LocalLibrary_MusicVideos.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210401105350_Add_LocalLibrary_MusicVideos.Designer.cs new file mode 100644 index 00000000..677b2704 --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210401105350_Add_LocalLibrary_MusicVideos.Designer.cs @@ -0,0 +1,1826 @@ +// +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("20210401105350_Add_LocalLibrary_MusicVideos")] + partial class Add_LocalLibrary_MusicVideos + { + 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("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("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("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .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("Name") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + 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("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("AudioCodec") + .HasColumnType("TEXT"); + + 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("Name") + .HasColumnType("TEXT"); + + b.Property("SampleAspectRatio") + .HasColumnType("TEXT"); + + b.Property("VideoCodec") + .HasColumnType("TEXT"); + + b.Property("VideoProfile") + .HasColumnType("TEXT"); + + b.Property("VideoScanKind") + .HasColumnType("INTEGER"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeId"); + + b.HasIndex("MovieId"); + + 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.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("Name") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + 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("Name") + .HasColumnType("TEXT"); + + b.Property("SeasonMetadataId") + .HasColumnType("INTEGER"); + + b.Property("ShowMetadataId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("EpisodeMetadataId"); + + b.HasIndex("MovieMetadataId"); + + 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.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.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.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); + }); + + 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.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.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.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.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.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.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/20210401105350_Add_LocalLibrary_MusicVideos.cs b/ErsatzTV.Infrastructure/Migrations/20210401105350_Add_LocalLibrary_MusicVideos.cs new file mode 100644 index 00000000..b3a0f96b --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210401105350_Add_LocalLibrary_MusicVideos.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_LocalLibrary_MusicVideos : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // create local music videos library + migrationBuilder.Sql( + @"INSERT INTO Library (Name, MediaKind, MediaSourceId) + SELECT 'Music Videos', 3, Id FROM + (SELECT LMS.Id FROM LocalMediaSource LMS + INNER JOIN Library L on L.MediaSourceId = LMS.Id + INNER JOIN LocalLibrary LL on L.Id = LL.Id + WHERE L.Name = 'Movies')"); + migrationBuilder.Sql("INSERT INTO LocalLibrary (Id) Values (last_insert_rowid())"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210401112533_Add_MusicVideo_MusicVideoMetadata.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210401112533_Add_MusicVideo_MusicVideoMetadata.Designer.cs new file mode 100644 index 00000000..208c156f --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210401112533_Add_MusicVideo_MusicVideoMetadata.Designer.cs @@ -0,0 +1,1970 @@ +// +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("20210401112533_Add_MusicVideo_MusicVideoMetadata")] + partial class Add_MusicVideo_MusicVideoMetadata + { + 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("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .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("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("AudioCodec") + .HasColumnType("TEXT"); + + 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("VideoCodec") + .HasColumnType("TEXT"); + + b.Property("VideoProfile") + .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/20210401112533_Add_MusicVideo_MusicVideoMetadata.cs b/ErsatzTV.Infrastructure/Migrations/20210401112533_Add_MusicVideo_MusicVideoMetadata.cs new file mode 100644 index 00000000..8307438a --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210401112533_Add_MusicVideo_MusicVideoMetadata.cs @@ -0,0 +1,228 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Add_MusicVideo_MusicVideoMetadata : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + "MusicVideoMetadataId", + "Tag", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "MusicVideoMetadataId", + "Studio", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "MusicVideoId", + "MediaVersion", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "MusicVideoMetadataId", + "Genre", + "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + "MusicVideoMetadataId", + "Artwork", + "INTEGER", + nullable: true); + + migrationBuilder.CreateTable( + "MusicVideo", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true) + }, + constraints: table => + { + table.PrimaryKey("PK_MusicVideo", x => x.Id); + table.ForeignKey( + "FK_MusicVideo_MediaItem_Id", + x => x.Id, + "MediaItem", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + "MusicVideoMetadata", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Album = table.Column("TEXT", nullable: true), + Plot = table.Column("TEXT", nullable: true), + Artist = table.Column("TEXT", nullable: true), + MusicVideoId = table.Column("INTEGER", nullable: false), + MetadataKind = table.Column("INTEGER", nullable: false), + Title = table.Column("TEXT", nullable: true), + OriginalTitle = table.Column("TEXT", nullable: true), + SortTitle = table.Column("TEXT", nullable: true), + Year = table.Column("INTEGER", nullable: true), + ReleaseDate = table.Column("TEXT", nullable: true), + DateAdded = table.Column("TEXT", nullable: false), + DateUpdated = table.Column("TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MusicVideoMetadata", x => x.Id); + table.ForeignKey( + "FK_MusicVideoMetadata_MusicVideo_MusicVideoId", + x => x.MusicVideoId, + "MusicVideo", + "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + "IX_Tag_MusicVideoMetadataId", + "Tag", + "MusicVideoMetadataId"); + + migrationBuilder.CreateIndex( + "IX_Studio_MusicVideoMetadataId", + "Studio", + "MusicVideoMetadataId"); + + migrationBuilder.CreateIndex( + "IX_MediaVersion_MusicVideoId", + "MediaVersion", + "MusicVideoId"); + + migrationBuilder.CreateIndex( + "IX_Genre_MusicVideoMetadataId", + "Genre", + "MusicVideoMetadataId"); + + migrationBuilder.CreateIndex( + "IX_Artwork_MusicVideoMetadataId", + "Artwork", + "MusicVideoMetadataId"); + + migrationBuilder.CreateIndex( + "IX_MusicVideoMetadata_MusicVideoId", + "MusicVideoMetadata", + "MusicVideoId"); + + migrationBuilder.AddForeignKey( + "FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId", + "Artwork", + "MusicVideoMetadataId", + "MusicVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + "FK_Genre_MusicVideoMetadata_MusicVideoMetadataId", + "Genre", + "MusicVideoMetadataId", + "MusicVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + "FK_MediaVersion_MusicVideo_MusicVideoId", + "MediaVersion", + "MusicVideoId", + "MusicVideo", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + "FK_Studio_MusicVideoMetadata_MusicVideoMetadataId", + "Studio", + "MusicVideoMetadataId", + "MusicVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + "FK_Tag_MusicVideoMetadata_MusicVideoMetadataId", + "Tag", + "MusicVideoMetadataId", + "MusicVideoMetadata", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + "FK_Artwork_MusicVideoMetadata_MusicVideoMetadataId", + "Artwork"); + + migrationBuilder.DropForeignKey( + "FK_Genre_MusicVideoMetadata_MusicVideoMetadataId", + "Genre"); + + migrationBuilder.DropForeignKey( + "FK_MediaVersion_MusicVideo_MusicVideoId", + "MediaVersion"); + + migrationBuilder.DropForeignKey( + "FK_Studio_MusicVideoMetadata_MusicVideoMetadataId", + "Studio"); + + migrationBuilder.DropForeignKey( + "FK_Tag_MusicVideoMetadata_MusicVideoMetadataId", + "Tag"); + + migrationBuilder.DropTable( + "MusicVideoMetadata"); + + migrationBuilder.DropTable( + "MusicVideo"); + + migrationBuilder.DropIndex( + "IX_Tag_MusicVideoMetadataId", + "Tag"); + + migrationBuilder.DropIndex( + "IX_Studio_MusicVideoMetadataId", + "Studio"); + + migrationBuilder.DropIndex( + "IX_MediaVersion_MusicVideoId", + "MediaVersion"); + + migrationBuilder.DropIndex( + "IX_Genre_MusicVideoMetadataId", + "Genre"); + + migrationBuilder.DropIndex( + "IX_Artwork_MusicVideoMetadataId", + "Artwork"); + + migrationBuilder.DropColumn( + "MusicVideoMetadataId", + "Tag"); + + migrationBuilder.DropColumn( + "MusicVideoMetadataId", + "Studio"); + + migrationBuilder.DropColumn( + "MusicVideoId", + "MediaVersion"); + + migrationBuilder.DropColumn( + "MusicVideoMetadataId", + "Genre"); + + migrationBuilder.DropColumn( + "MusicVideoMetadataId", + "Artwork"); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/20210402232635_Remove_MediaVersion_Codecs.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210402232635_Remove_MediaVersion_Codecs.Designer.cs new file mode 100644 index 00000000..9707602e --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210402232635_Remove_MediaVersion_Codecs.Designer.cs @@ -0,0 +1,1961 @@ +// +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("20210402232635_Remove_MediaVersion_Codecs")] + partial class Remove_MediaVersion_Codecs + { + 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("AudioVolume") + .HasColumnType("INTEGER"); + + b.Property("FrameRate") + .HasColumnType("TEXT"); + + b.Property("HardwareAcceleration") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NormalizeAudio") + .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("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/20210402232635_Remove_MediaVersion_Codecs.cs b/ErsatzTV.Infrastructure/Migrations/20210402232635_Remove_MediaVersion_Codecs.cs new file mode 100644 index 00000000..e7cbf5fb --- /dev/null +++ b/ErsatzTV.Infrastructure/Migrations/20210402232635_Remove_MediaVersion_Codecs.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace ErsatzTV.Infrastructure.Migrations +{ + public partial class Remove_MediaVersion_Codecs : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + "AudioCodec", + "MediaVersion"); + + migrationBuilder.DropColumn( + "VideoCodec", + "MediaVersion"); + + migrationBuilder.DropColumn( + "VideoProfile", + "MediaVersion"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + "AudioCodec", + "MediaVersion", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "VideoCodec", + "MediaVersion", + "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + "VideoProfile", + "MediaVersion", + "TEXT", + nullable: true); + } + } +} diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 31092933..43b95de2 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -42,6 +42,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("MovieMetadataId") .HasColumnType("INTEGER"); + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + b.Property("Path") .HasColumnType("TEXT"); @@ -59,6 +62,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MovieMetadataId"); + b.HasIndex("MusicVideoMetadataId"); + b.HasIndex("SeasonMetadataId"); b.HasIndex("ShowMetadataId"); @@ -294,6 +299,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("MovieMetadataId") .HasColumnType("INTEGER"); + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + b.Property("Name") .HasColumnType("TEXT"); @@ -309,6 +317,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MovieMetadataId"); + b.HasIndex("MusicVideoMetadataId"); + b.HasIndex("SeasonMetadataId"); b.HasIndex("ShowMetadataId"); @@ -472,9 +482,6 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AudioCodec") - .HasColumnType("TEXT"); - b.Property("DateAdded") .HasColumnType("TEXT"); @@ -496,18 +503,15 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("MovieId") .HasColumnType("INTEGER"); + b.Property("MusicVideoId") + .HasColumnType("INTEGER"); + b.Property("Name") .HasColumnType("TEXT"); b.Property("SampleAspectRatio") .HasColumnType("TEXT"); - b.Property("VideoCodec") - .HasColumnType("TEXT"); - - b.Property("VideoProfile") - .HasColumnType("TEXT"); - b.Property("VideoScanKind") .HasColumnType("INTEGER"); @@ -520,6 +524,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MovieId"); + b.HasIndex("MusicVideoId"); + b.ToTable("MediaVersion"); }); @@ -574,6 +580,57 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -913,6 +970,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("MovieMetadataId") .HasColumnType("INTEGER"); + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + b.Property("Name") .HasColumnType("TEXT"); @@ -928,6 +988,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MovieMetadataId"); + b.HasIndex("MusicVideoMetadataId"); + b.HasIndex("SeasonMetadataId"); b.HasIndex("ShowMetadataId"); @@ -949,6 +1011,9 @@ namespace ErsatzTV.Infrastructure.Migrations b.Property("MovieMetadataId") .HasColumnType("INTEGER"); + b.Property("MusicVideoMetadataId") + .HasColumnType("INTEGER"); + b.Property("Name") .HasColumnType("TEXT"); @@ -964,6 +1029,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasIndex("MovieMetadataId"); + b.HasIndex("MusicVideoMetadataId"); + b.HasIndex("SeasonMetadataId"); b.HasIndex("ShowMetadataId"); @@ -1036,6 +1103,15 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -1207,6 +1283,11 @@ namespace ErsatzTV.Infrastructure.Migrations .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") @@ -1291,6 +1372,11 @@ namespace ErsatzTV.Infrastructure.Migrations .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"); @@ -1379,6 +1465,11 @@ namespace ErsatzTV.Infrastructure.Migrations .WithMany("MediaVersions") .HasForeignKey("MovieId") .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ErsatzTV.Core.Domain.MusicVideo", null) + .WithMany("MediaVersions") + .HasForeignKey("MusicVideoId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity( @@ -1394,6 +1485,19 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -1627,6 +1731,11 @@ namespace ErsatzTV.Infrastructure.Migrations .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"); @@ -1650,6 +1759,11 @@ namespace ErsatzTV.Infrastructure.Migrations .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"); @@ -1723,6 +1837,17 @@ namespace ErsatzTV.Infrastructure.Migrations .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 => @@ -1917,6 +2042,19 @@ namespace ErsatzTV.Infrastructure.Migrations 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 => @@ -1979,6 +2117,15 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("MovieMetadata"); }); + modelBuilder.Entity( + "ErsatzTV.Core.Domain.MusicVideo", + b => + { + b.Navigation("MediaVersions"); + + b.Navigation("MusicVideoMetadata"); + }); + modelBuilder.Entity( "ErsatzTV.Core.Domain.Season", b => diff --git a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs index a61a8935..400a519e 100644 --- a/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs +++ b/ErsatzTV.Infrastructure/Plex/Models/PlexMetadataResponse.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace ErsatzTV.Infrastructure.Plex.Models { diff --git a/ErsatzTV.Infrastructure/Search/SearchIndex.cs b/ErsatzTV.Infrastructure/Search/SearchIndex.cs index 3f95e1d0..704d80b6 100644 --- a/ErsatzTV.Infrastructure/Search/SearchIndex.cs +++ b/ErsatzTV.Infrastructure/Search/SearchIndex.cs @@ -29,6 +29,7 @@ namespace ErsatzTV.Infrastructure.Search private const string IdField = "id"; private const string TypeField = "type"; + private const string ArtistField = "artist"; private const string TitleField = "title"; private const string SortTitleField = "sort_title"; private const string GenreField = "genre"; @@ -42,6 +43,7 @@ namespace ErsatzTV.Infrastructure.Search private const string MovieType = "movie"; private const string ShowType = "show"; + private const string MusicVideoType = "music_video"; private static bool _isRebuilding; @@ -93,6 +95,9 @@ namespace ErsatzTV.Infrastructure.Search case Show show: UpdateShow(show, writer); break; + case MusicVideo musicVideo: + UpdateMusicVideo(musicVideo, writer); + break; } } } @@ -121,6 +126,9 @@ namespace ErsatzTV.Infrastructure.Search case Show show: UpdateShow(show, writer); break; + case MusicVideo musicVideo: + UpdateMusicVideo(musicVideo, writer); + break; } } @@ -338,6 +346,66 @@ namespace ErsatzTV.Infrastructure.Search } } + private void UpdateMusicVideo(MusicVideo musicVideo, IndexWriter writer) + { + Option maybeMetadata = musicVideo.MusicVideoMetadata.HeadOrNone(); + if (maybeMetadata.IsSome) + { + MusicVideoMetadata metadata = maybeMetadata.ValueUnsafe(); + + try + { + var doc = new Document + { + new StringField(IdField, musicVideo.Id.ToString(), Field.Store.YES), + new StringField(TypeField, MusicVideoType, Field.Store.NO), + new TextField(ArtistField, metadata.Artist, Field.Store.NO), + new TextField(TitleField, metadata.Title, Field.Store.NO), + new StringField(SortTitleField, metadata.SortTitle.ToLowerInvariant(), Field.Store.NO), + new TextField(LibraryNameField, musicVideo.LibraryPath.Library.Name, Field.Store.NO), + new StringField(TitleAndYearField, GetTitleAndYear(metadata), Field.Store.NO), + new StringField(JumpLetterField, GetJumpLetter(metadata), Field.Store.YES) + }; + + if (metadata.ReleaseDate.HasValue) + { + doc.Add( + new StringField( + ReleaseDateField, + metadata.ReleaseDate.Value.ToString("yyyyMMdd"), + Field.Store.NO)); + } + + if (!string.IsNullOrWhiteSpace(metadata.Plot)) + { + doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); + } + + foreach (Genre genre in metadata.Genres) + { + doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); + } + + foreach (Tag tag in metadata.Tags) + { + doc.Add(new TextField(TagField, tag.Name, Field.Store.NO)); + } + + foreach (Studio studio in metadata.Studios) + { + doc.Add(new TextField(StudioField, studio.Name, Field.Store.NO)); + } + + writer.UpdateDocument(new Term(IdField, musicVideo.Id.ToString()), doc); + } + catch (Exception ex) + { + metadata.MusicVideo = null; + _logger.LogWarning(ex, "Error indexing music video with metadata {@Metadata}", metadata); + } + } + } + private SearchItem ProjectToSearchItem(Document doc) => new(Convert.ToInt32(doc.Get(IdField))); private Query ParseQuery(string searchQuery, QueryParser parser) diff --git a/ErsatzTV/Pages/CollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor index 1ace7538..013f72fe 100644 --- a/ErsatzTV/Pages/CollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -52,6 +52,10 @@ { @_data.EpisodeCards.Count Episodes } + @if (_data.MusicVideoCards.Any()) + { + @_data.MusicVideoCards.Count Music Videos + } @if (SupportsCustomOrdering()) {
@@ -165,6 +169,29 @@ } } + + @if (_data.MusicVideoCards.Any()) + { + + Music Videos + + + + @foreach (MusicVideoCardViewModel card in _data.MusicVideoCards.OrderBy(e => e.SortTitle)) + { + + } + + } @code { @@ -215,14 +242,12 @@ private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - 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)) - .ToList(); - } + 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(); SelectClicked(GetSortedItems, card, e); } @@ -240,6 +265,19 @@ } } + private async Task RemoveMusicVideoFromCollection(MediaCardViewModel vm) + { + if (vm is MusicVideoCardViewModel musicVideo) + { + var request = new RemoveItemsFromCollection(Id) + { + MediaItemIds = new List { musicVideo.MusicVideoId } + }; + + await RemoveItemsWithConfirmation("music video", $"{musicVideo.Title} ({musicVideo.Subtitle})", request); + } + } + private async Task RemoveShowFromCollection(MediaCardViewModel vm) { if (vm is TelevisionShowCardViewModel show) diff --git a/ErsatzTV/Pages/LocalLibraryPathEditor.razor b/ErsatzTV/Pages/LocalLibraryPathEditor.razor index 3c3191d3..198c77d7 100644 --- a/ErsatzTV/Pages/LocalLibraryPathEditor.razor +++ b/ErsatzTV/Pages/LocalLibraryPathEditor.razor @@ -70,11 +70,11 @@ Logger.LogError("Unexpected error saving local library path: {Error}", error.Value); return Task.CompletedTask; }, - Right: async vm => + Right: async _ => { if (Locker.LockLibrary(_library.Id)) { - await Channel.WriteAsync(new ForceScanLocalLibrary(_library.Id)); + await Channel.WriteAsync(new ForceRescanLocalLibrary(_library.Id)); NavigationManager.NavigateTo("/media/libraries"); } }); diff --git a/ErsatzTV/Pages/MultiSelectBase.cs b/ErsatzTV/Pages/MultiSelectBase.cs index 7f577ae5..4c3db105 100644 --- a/ErsatzTV/Pages/MultiSelectBase.cs +++ b/ErsatzTV/Pages/MultiSelectBase.cs @@ -107,7 +107,8 @@ namespace ErsatzTV.Pages var request = new AddItemsToCollection( collection.Id, _selectedItems.OfType().Map(m => m.MovieId).ToList(), - _selectedItems.OfType().Map(s => s.TelevisionShowId).ToList()); + _selectedItems.OfType().Map(s => s.TelevisionShowId).ToList(), + _selectedItems.OfType().Map(mv => mv.MusicVideoId).ToList()); Either addResult = await Mediator.Send(request); addResult.Match( @@ -144,6 +145,7 @@ namespace ErsatzTV.Pages itemIds.AddRange(_selectedItems.OfType().Map(s => s.TelevisionShowId)); itemIds.AddRange(_selectedItems.OfType().Map(s => s.TelevisionSeasonId)); itemIds.AddRange(_selectedItems.OfType().Map(e => e.EpisodeId)); + itemIds.AddRange(_selectedItems.OfType().Map(mv => mv.MusicVideoId)); await Mediator.Send( new RemoveItemsFromCollection(collectionId) diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor new file mode 100644 index 00000000..af9d7211 --- /dev/null +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -0,0 +1,164 @@ +@page "/media/music/videos" +@page "/media/music/videos/page/{PageNumber:int}" +@using LanguageExt.UnsafeValueAccess +@using Microsoft.AspNetCore.WebUtilities +@using Microsoft.Extensions.Primitives +@using ErsatzTV.Application.MediaCards +@using ErsatzTV.Application.MediaCollections +@using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Application.Search.Queries +@using Unit = LanguageExt.Unit +@inherits MultiSelectBase +@inject NavigationManager NavigationManager +@inject ChannelWriter Channel + + +
+ @if (IsSelectMode()) + { + @SelectionLabel() +
+ + Add To Collection + + + Clear Selection + +
+ } + else + { + @_query +
+ + + + + @Math.Min((PageNumber - 1) * PageSize + 1, _data.Count)-@Math.Min(_data.Count, PageNumber * PageSize) of @_data.Count + + + + +
+ } +
+
+ + + @foreach (MusicVideoCardViewModel card in _data.Cards.Where(m => !string.IsNullOrWhiteSpace(m.Title)).OrderBy(m => m.SortTitle)) + { + + } + + +@if (_data.PageMap.IsSome) +{ + +} + +@code { + private static int PageSize => 100; + + [Parameter] + public int PageNumber { get; set; } + + private MusicVideoCardResultsViewModel _data; + private string _query; + + protected override Task OnParametersSetAsync() + { + if (PageNumber == 0) + { + PageNumber = 1; + } + + string query = new Uri(NavigationManager.Uri).Query; + if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) + { + _query = value; + } + else + { + _query = null; + } + + return RefreshData(); + } + + protected override async Task RefreshData() + { + string searchQuery = string.IsNullOrWhiteSpace(_query) ? "type:music_video" : $"type:music_video AND ({_query})"; + _data = await Mediator.Send(new QuerySearchIndexMusicVideos(searchQuery, PageNumber, PageSize)); + } + + private void PrevPage() + { + var uri = $"/media/music/videos/page/{PageNumber - 1}"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + NavigationManager.NavigateTo(uri); + } + + private void NextPage() + { + var uri = $"/media/music/videos/page/{PageNumber + 1}"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + NavigationManager.NavigateTo(uri); + } + + private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) + { + List GetSortedItems() => _data.Cards.OrderBy(m => m.SortTitle).ToList(); + + SelectClicked(GetSortedItems, card, e); + } + + private async Task AddToCollection(MediaCardViewModel card) + { + if (card is MusicVideoCardViewModel musicVideo) + { + var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); + Either addResult = await Mediator.Send(request); + addResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}"); + Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value); + }, + Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success)); + } + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index b12ca204..466cd2ac 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -51,6 +51,15 @@ { 0 Shows } + + if (_musicVideos.Count > 0) + { + @_musicVideos.Count Music Videos + } + else + { + 0 Music Videos + } }
@@ -108,12 +117,40 @@ } } + + @if (_musicVideos.Count > 0) + { +
+ + Music Videos + + @if (_shows.Count > 50) + { + See All >> + } +
+ + + @foreach (MusicVideoCardViewModel card in _musicVideos.Cards.OrderBy(s => s.SortTitle)) + { + + } + + } @code { private string _query; private MovieCardResultsViewModel _movies; private TelevisionShowCardResultsViewModel _shows; + private MusicVideoCardResultsViewModel _musicVideos; protected override async Task OnInitializedAsync() { @@ -125,17 +162,16 @@ _movies = await Mediator.Send(new QuerySearchIndexMovies($"type:movie AND ({_query})", 1, 50)); _shows = await Mediator.Send(new QuerySearchIndexShows($"type:show AND ({_query})", 1, 50)); + _musicVideos = await Mediator.Send(new QuerySearchIndexMusicVideos($"type:music_video AND ({_query})", 1, 50)); } } private void SelectClicked(MediaCardViewModel card, MouseEventArgs e) { - List GetSortedItems() - { - return _movies.Cards.OrderBy(m => m.SortTitle) - .Append(_shows.Cards.OrderBy(s => s.SortTitle)) - .ToList(); - } + List GetSortedItems() => _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); } @@ -183,6 +219,27 @@ Right: _ => Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success)); } } + + if (card is MusicVideoCardViewModel musicVideo) + { + var parameters = new DialogParameters { { "EntityType", "music video" }, { "EntityName", musicVideo.Title } }; + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; + + IDialogReference dialog = Dialog.Show("Add To Collection", parameters, options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) + { + var request = new AddMusicVideoToCollection(collection.Id, musicVideo.MusicVideoId); + Either addResult = await Mediator.Send(request); + addResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error adding music video to collection: {error.Value}"); + Logger.LogError("Unexpected error adding music video to collection: {Error}", error.Value); + }, + Right: _ => Snackbar.Add($"Added {musicVideo.Title} to collection {collection.Name}", Severity.Success)); + } + } } private string GetMoviesLink() @@ -205,4 +262,14 @@ return uri; } + private string GetMusicVideosLink() + { + var uri = "/media/music/videos/page/1"; + if (!string.IsNullOrWhiteSpace(_query)) + { + uri = QueryHelpers.AddQueryString(uri, "query", _query); + } + return uri; + } + } \ No newline at end of file diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 6775e61a..dfeafe3c 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -47,6 +47,7 @@ Libraries TV Shows Movies + Music Videos Collections Schedules diff --git a/ErsatzTV/Shared/MediaCard.razor b/ErsatzTV/Shared/MediaCard.razor index 4b1353dd..42828d87 100644 --- a/ErsatzTV/Shared/MediaCard.razor +++ b/ErsatzTV/Shared/MediaCard.razor @@ -3,7 +3,7 @@ @inject IMediator Mediator
- @if (!string.IsNullOrWhiteSpace(Link)) + @if (SelectClicked.HasDelegate || !string.IsNullOrWhiteSpace(Link)) {
@@ -26,7 +26,7 @@
+ OnClick="@(e => IsSelectMode || string.IsNullOrWhiteSpace(Link) ? SelectClicked.InvokeAsync(e) : Task.CompletedTask)"> @if (SelectClicked.HasDelegate) { diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index dc2ca9e9..68341dce 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -198,6 +198,7 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -209,6 +210,7 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();