From 1f28b1a9451b36bf304157cb0543537625a35563 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Fri, 19 Feb 2021 13:31:12 -0600 Subject: [PATCH] no more duplicate television shows, even with the same show split across sources --- .../Domain/LocalTelevisionShowSource.cs | 9 ++ ErsatzTV.Core/Domain/TelevisionShow.cs | 7 +- ErsatzTV.Core/Domain/TelevisionShowSource.cs | 9 ++ .../Metadata/IFallbackMetadataProvider.cs | 9 ++ .../Interfaces/Metadata/ILocalMediaScanner.cs | 12 -- .../Metadata/ILocalMetadataProvider.cs | 5 +- .../Repositories/ITelevisionRepository.cs | 13 +- .../Metadata/FallbackMetadataProvider.cs | 9 +- ErsatzTV.Core/Metadata/LocalFolderScanner.cs | 4 + .../Metadata/LocalMetadataProvider.cs | 36 +++-- .../Metadata/TelevisionFolderScanner.cs | 102 ++++++++++---- .../TelevisionShowConfiguration.cs | 5 + .../Data/Repositories/TelevisionRepository.cs | 127 ++++++++++++------ ErsatzTV.Infrastructure/Data/TvContext.cs | 1 + ...219165123_TelevisionExpansion.Designer.cs} | 78 ++++++++--- ... => 20210219165123_TelevisionExpansion.cs} | 55 +++++--- .../Migrations/TvContextModelSnapshot.cs | 80 ++++++++--- ErsatzTV/Startup.cs | 5 +- 18 files changed, 408 insertions(+), 158 deletions(-) create mode 100644 ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs create mode 100644 ErsatzTV.Core/Domain/TelevisionShowSource.cs create mode 100644 ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs delete mode 100644 ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs rename ErsatzTV.Infrastructure/Migrations/{20210218214650_Statistics.Designer.cs => 20210219165123_TelevisionExpansion.Designer.cs} (95%) rename ErsatzTV.Infrastructure/Migrations/{20210218214650_Statistics.cs => 20210219165123_TelevisionExpansion.cs} (91%) diff --git a/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs b/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs new file mode 100644 index 000000000..055158091 --- /dev/null +++ b/ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Domain +{ + public class LocalTelevisionShowSource : TelevisionShowSource + { + public int MediaSourceId { get; set; } + public LocalMediaSource MediaSource { get; set; } + public string Path { get; set; } + } +} diff --git a/ErsatzTV.Core/Domain/TelevisionShow.cs b/ErsatzTV.Core/Domain/TelevisionShow.cs index 4f0d2ef4f..233c5bcb5 100644 --- a/ErsatzTV.Core/Domain/TelevisionShow.cs +++ b/ErsatzTV.Core/Domain/TelevisionShow.cs @@ -1,17 +1,14 @@ using System; using System.Collections.Generic; -using ErsatzTV.Core.Interfaces.Domain; namespace ErsatzTV.Core.Domain { - public class TelevisionShow : IHasAPoster + public class TelevisionShow { public int Id { get; set; } - public int MediaSourceId { get; set; } - public MediaSource Source { get; set; } + public List Sources { get; set; } public TelevisionShowMetadata Metadata { get; set; } public List Seasons { get; set; } - public string Path { get; set; } public string Poster { get; set; } public DateTime? PosterLastWriteTime { get; set; } } diff --git a/ErsatzTV.Core/Domain/TelevisionShowSource.cs b/ErsatzTV.Core/Domain/TelevisionShowSource.cs new file mode 100644 index 000000000..43f7890af --- /dev/null +++ b/ErsatzTV.Core/Domain/TelevisionShowSource.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Core.Domain +{ + public class TelevisionShowSource + { + public int Id { get; set; } + public int TelevisionShowId { get; set; } + public TelevisionShow TelevisionShow { get; set; } + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs new file mode 100644 index 000000000..4325b1fda --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs @@ -0,0 +1,9 @@ +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Core.Interfaces.Metadata +{ + public interface IFallbackMetadataProvider + { + TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder); + } +} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs deleted file mode 100644 index 7444cfd40..000000000 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading.Tasks; -using ErsatzTV.Core.Domain; -using LanguageExt; - -namespace ErsatzTV.Core.Interfaces.Metadata -{ - public interface ILocalMediaScanner - { - Task ScanMovies(LocalMediaSource localMediaSource, string ffprobePath); - Task ScanTelevision(LocalMediaSource localMediaSource, string ffprobePath); - } -} diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs index 8b5655611..317e35884 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs @@ -6,9 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Metadata { public interface ILocalMetadataProvider { + Task GetMetadataForShow(string showFolder); Task RefreshSidecarMetadata(MediaItem mediaItem, string path); - Task RefreshSidecarMetadata(TelevisionShow televisionShow, string path); + Task RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder); Task RefreshFallbackMetadata(MediaItem mediaItem); - Task RefreshFallbackMetadata(TelevisionShow televisionShow); + Task RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder); } } diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs index f9c44a4f5..25ade2b15 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs @@ -7,7 +7,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories { public interface ITelevisionRepository { - Task> GetAllByMediaSourceId(int mediaSourceId); Task Update(TelevisionShow show); Task Update(TelevisionSeason season); Task Update(TelevisionEpisodeMediaItem episode); @@ -16,7 +15,13 @@ namespace ErsatzTV.Core.Interfaces.Repositories Task> GetPagedShows(int pageNumber, int pageSize); Task GetSeasonCount(int televisionShowId); Task> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize); - Task> GetOrAddShow(int mediaSourceId, string path); + Task> GetShowByPath(int mediaSourceId, string path); + Task> GetShowByMetadata(TelevisionShowMetadata metadata); + + Task> AddShow( + int localMediaSourceId, + string showFolder, + TelevisionShowMetadata metadata); Task> GetOrAddSeason( TelevisionShow show, @@ -28,7 +33,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories int mediaSourceId, string path); - Task> FindRemovedShows(LocalMediaSource localMediaSource, List allShowFolders); - Task Delete(TelevisionShow show); + Task DeleteMissingSources(int localMediaSourceId, List allFolders); + Task DeleteEmptyShows(); } } diff --git a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs index e816b2432..726708c42 100644 --- a/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs @@ -2,16 +2,17 @@ using System.IO; using System.Text.RegularExpressions; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Metadata; namespace ErsatzTV.Core.Metadata { - public static class FallbackMetadataProvider + public class FallbackMetadataProvider : IFallbackMetadataProvider { - public static TelevisionShowMetadata GetFallbackMetadata(TelevisionShow televisionShow) + public TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder) { - string fileName = Path.GetFileName(televisionShow.Path); + string fileName = Path.GetFileName(showFolder); var metadata = new TelevisionShowMetadata - { Source = MetadataSource.Fallback, Title = fileName ?? televisionShow.Path }; + { Source = MetadataSource.Fallback, Title = fileName ?? showFolder }; return GetTelevisionShowMetadata(fileName, metadata); } diff --git a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs index f7b2b9ca0..ad258aa05 100644 --- a/ErsatzTV.Core/Metadata/LocalFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/LocalFolderScanner.cs @@ -98,5 +98,9 @@ namespace ErsatzTV.Core.Metadata return Task.CompletedTask; }); } + + protected Task> SavePosterToDisk(string posterPath, int height = 220) => + File.ReadAllBytesAsync(posterPath) + .Bind(bytes => _imageCache.ResizeAndSaveImage(bytes, height, null)); } } diff --git a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs index 113966daf..1d8ae20df 100644 --- a/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs +++ b/ErsatzTV.Core/Metadata/LocalMetadataProvider.cs @@ -16,6 +16,8 @@ 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 readonly IFallbackMetadataProvider _fallbackMetadataProvider; + private readonly ILocalFileSystem _localFileSystem; private readonly ILogger _logger; private readonly IMediaItemRepository _mediaItemRepository; @@ -24,13 +26,32 @@ namespace ErsatzTV.Core.Metadata public LocalMetadataProvider( IMediaItemRepository mediaItemRepository, ITelevisionRepository televisionRepository, + IFallbackMetadataProvider fallbackMetadataProvider, + ILocalFileSystem localFileSystem, ILogger logger) { _mediaItemRepository = mediaItemRepository; _televisionRepository = televisionRepository; + _fallbackMetadataProvider = fallbackMetadataProvider; + _localFileSystem = localFileSystem; _logger = logger; } + public Task GetMetadataForShow(string showFolder) + { + string nfoFileName = Path.Combine(showFolder, "tvshow.nfo"); + return Optional(_localFileSystem.FileExists(nfoFileName)) + .Filter(identity).AsTask() + .Bind(_ => LoadTelevisionShowMetadata(nfoFileName)) + .IfNoneAsync(() => _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder).AsTask()) + .Map( + m => + { + m.SortTitle = GetSortTitle(m.Title); + return m; + }); + } + public Task RefreshSidecarMetadata(MediaItem mediaItem, string path) => mediaItem switch { @@ -41,8 +62,8 @@ namespace ErsatzTV.Core.Metadata _ => Task.FromResult(Unit.Default) }; - public Task RefreshSidecarMetadata(TelevisionShow televisionShow, string path) => - LoadMetadata(televisionShow, path).Bind( + public Task RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder) => + LoadMetadata(televisionShow, showFolder).Bind( maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(televisionShow, metadata))); public Task RefreshFallbackMetadata(MediaItem mediaItem) => @@ -54,8 +75,9 @@ namespace ErsatzTV.Core.Metadata _ => Task.FromResult(Unit.Default) }; - public Task RefreshFallbackMetadata(TelevisionShow televisionShow) => - ApplyMetadataUpdate(televisionShow, FallbackMetadataProvider.GetFallbackMetadata(televisionShow)).ToUnit(); + public Task RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder) => + ApplyMetadataUpdate(televisionShow, _fallbackMetadataProvider.GetFallbackMetadataForShow(showFolder)) + .ToUnit(); private async Task ApplyMetadataUpdate(TelevisionEpisodeMediaItem mediaItem, TelevisionEpisodeMetadata metadata) { @@ -160,12 +182,6 @@ namespace ErsatzTV.Core.Metadata return None; } - if (!(televisionShow.Source is LocalMediaSource)) - { - _logger.LogDebug("Media source {Name} is not a local media source", televisionShow.Source.Name); - return None; - } - return await LoadTelevisionShowMetadata(nfoFileName); } diff --git a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs index c88f5e988..01264fb1e 100644 --- a/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs +++ b/ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -50,34 +49,65 @@ namespace ErsatzTV.Core.Metadata var allShowFolders = _localFileSystem.ListSubdirectories(localMediaSource.Folder) .Filter(ShouldIncludeFolder) + .OrderBy(identity) .ToList(); foreach (string showFolder in allShowFolders) { - Either maybeShow = await _televisionRepository - .GetOrAddShow(localMediaSource.Id, showFolder) - .BindT(UpdateMetadata) - .BindT(UpdatePoster); + // TODO: check all sources for latest metadata? + Either maybeShow = + await FindOrCreateShow(localMediaSource.Id, showFolder) + .BindT(show => UpdateMetadataForShow(show, showFolder)) + .BindT(show => UpdatePosterForShow(show, showFolder)); await maybeShow.Match( - show => ScanSeasons(localMediaSource, ffprobePath, show), + show => ScanSeasons(localMediaSource, ffprobePath, show, showFolder), _ => Task.FromResult(Unit.Default)); } - List removedShows = - await _televisionRepository.FindRemovedShows(localMediaSource, allShowFolders); - foreach (TelevisionShow show in removedShows) - { - _logger.LogDebug("Removing missing show at {Folder}", show.Path); - await _televisionRepository.Delete(show); - } + await _televisionRepository.DeleteMissingSources(localMediaSource.Id, allShowFolders); + await _televisionRepository.DeleteEmptyShows(); return Unit.Default; } - private async Task ScanSeasons(LocalMediaSource localMediaSource, string ffprobePath, TelevisionShow show) + private async Task> FindOrCreateShow( + int localMediaSourceId, + string showFolder) + { + Option maybeShowByPath = + await _televisionRepository.GetShowByPath(localMediaSourceId, showFolder); + return await maybeShowByPath.Match( + show => Right(show).AsTask(), + async () => + { + TelevisionShowMetadata metadata = await _localMetadataProvider.GetMetadataForShow(showFolder); + Option maybeShow = await _televisionRepository.GetShowByMetadata(metadata); + return await maybeShow.Match( + async show => + { + show.Sources.Add( + new LocalTelevisionShowSource + { + MediaSourceId = localMediaSourceId, + Path = showFolder, + TelevisionShow = show + }); + await _televisionRepository.Update(show); + return Right(show); + }, + async () => await _televisionRepository.AddShow(localMediaSourceId, showFolder, metadata)); + }); + } + + private async Task ScanSeasons( + LocalMediaSource localMediaSource, + string ffprobePath, + TelevisionShow show, + string showPath) { - foreach (string seasonFolder in _localFileSystem.ListSubdirectories(show.Path).Filter(ShouldIncludeFolder)) + foreach (string seasonFolder in _localFileSystem.ListSubdirectories(showPath).Filter(ShouldIncludeFolder) + .OrderBy(identity)) { Option maybeSeasonNumber = SeasonNumberForFolder(seasonFolder); await maybeSeasonNumber.IfSomeAsync( @@ -102,7 +132,7 @@ namespace ErsatzTV.Core.Metadata TelevisionSeason season) { foreach (string file in _localFileSystem.ListFiles(season.Path) - .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f)))) + .Filter(f => VideoFileExtensions.Contains(Path.GetExtension(f))).OrderBy(identity)) { // TODO: figure out how to rebuild playlists Either maybeEpisode = await _televisionRepository @@ -118,11 +148,13 @@ namespace ErsatzTV.Core.Metadata return Unit.Default; } - private async Task> UpdateMetadata(TelevisionShow show) + private async Task> UpdateMetadataForShow( + TelevisionShow show, + string showFolder) { try { - await LocateNfoFile(show).Match( + await LocateNfoFileForShow(showFolder).Match( async nfoFile => { if (show.Metadata == null || show.Metadata.Source == MetadataSource.Fallback || @@ -136,8 +168,8 @@ namespace ErsatzTV.Core.Metadata { if (show.Metadata == null) { - _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", show.Path); - await _localMetadataProvider.RefreshFallbackMetadata(show); + _logger.LogDebug("Refreshing {Attribute} for {Path}", "Fallback Metadata", showFolder); + await _localMetadataProvider.RefreshFallbackMetadata(show, showFolder); } }); @@ -181,18 +213,34 @@ namespace ErsatzTV.Core.Metadata } } - private async Task> UpdatePoster(TelevisionShow show) + private async Task> UpdatePosterForShow( + TelevisionShow show, + string showFolder) { try { - await LocatePoster(show).IfSomeAsync( + await LocatePosterForShow(showFolder).IfSomeAsync( async posterFile => { if (string.IsNullOrWhiteSpace(show.Poster) || show.PosterLastWriteTime < _localFileSystem.GetLastWriteTime(posterFile)) { _logger.LogDebug("Refreshing {Attribute} from {Path}", "Poster", posterFile); - await SavePosterToDisk(show, posterFile, _televisionRepository.Update, 440); + Either maybePoster = await SavePosterToDisk(posterFile, 440); + await maybePoster.Match( + poster => + { + show.Poster = poster; + return _televisionRepository.Update(show); + }, + error => + { + _logger.LogWarning( + "Unable to save poster to disk from {Path}: {Error}", + posterFile, + error.Value); + return Task.CompletedTask; + }); } }); @@ -251,18 +299,18 @@ namespace ErsatzTV.Core.Metadata } } - private Option LocateNfoFile(TelevisionShow show) => - Optional(Path.Combine(show.Path, "tvshow.nfo")) + private Option LocateNfoFileForShow(string showFolder) => + Optional(Path.Combine(showFolder, "tvshow.nfo")) .Filter(s => _localFileSystem.FileExists(s)); private Option LocateNfoFile(TelevisionEpisodeMediaItem episode) => Optional(Path.ChangeExtension(episode.Path, "nfo")) .Filter(s => _localFileSystem.FileExists(s)); - private Option LocatePoster(TelevisionShow show) => + private Option LocatePosterForShow(string showFolder) => ImageFileExtensions .Map(ext => $"poster.{ext}") - .Map(f => Path.Combine(show.Path, f)) + .Map(f => Path.Combine(showFolder, f)) .Filter(s => _localFileSystem.FileExists(s)) .HeadOrNone(); diff --git a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs index 8cdff4f64..75464a52e 100644 --- a/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs +++ b/ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs @@ -17,6 +17,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations builder.HasMany(show => show.Seasons) .WithOne(season => season.TelevisionShow); + + builder.HasMany(show => show.Sources) + .WithOne(source => source.TelevisionShow) + .HasForeignKey(source => source.TelevisionShowId) + .OnDelete(DeleteBehavior.Cascade); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 2d77380a1..ada1daa76 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -17,13 +17,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public TelevisionRepository(TvContext dbContext) => _dbContext = dbContext; - public Task> GetAllByMediaSourceId(int mediaSourceId) => - _dbContext.TelevisionShows - .Include(s => s.Metadata) - .Include(s => s.Seasons) - .ThenInclude(s => s.Episodes) - .ToListAsync(); - public async Task Update(TelevisionShow show) { _dbContext.TelevisionShows.Update(show); @@ -60,13 +53,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories _dbContext.TelevisionShows .AsNoTracking() .Include(s => s.Metadata) - .OrderBy(s => s.Metadata.SortTitle) + .OrderBy(s => s.Metadata == null ? string.Empty : s.Metadata.SortTitle) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) - .ToListAsync() - .Map( - list => list.GroupBy(s => new { s.Metadata.Title, s.Metadata.Year }).Select(g => g.First()) - .ToList()); + .ToListAsync(); public async Task GetSeasonCount(int televisionShowId) { @@ -105,15 +95,70 @@ namespace ErsatzTV.Infrastructure.Data.Repositories Task.FromResult(new List())); } - public async Task> GetOrAddShow(int mediaSourceId, string path) + public async Task> GetShowByPath(int mediaSourceId, string path) { - Option maybeExisting = await _dbContext.TelevisionShows - .Include(i => i.Metadata) - .SingleOrDefaultAsync(i => i.Path == path); + Option maybeShowId = await _dbContext.LocalTelevisionShowSources + .SingleOrDefaultAsync(s => s.MediaSourceId == mediaSourceId && s.Path == path) + .Map(Optional) + .MapT(s => s.TelevisionShowId); - return await maybeExisting.Match( - show => Right(show).AsTask(), - () => AddShow(mediaSourceId, path)); + return await maybeShowId.Match>>( + async id => await _dbContext.TelevisionShows + .Include(s => s.Metadata) + .Include(s => s.Sources) + .SingleOrDefaultAsync(s => s.Id == id), + () => Task.FromResult(Option.None)); + } + + public async Task> GetShowByMetadata(TelevisionShowMetadata metadata) + { + Option maybeShow = await _dbContext.TelevisionShows + .Include(s => s.Metadata) + .Where(s => s.Metadata.Title == metadata.Title && s.Metadata.Year == metadata.Year) + .SingleOrDefaultAsync() + .Map(Optional); + + await maybeShow.IfSomeAsync( + async show => + { + await _dbContext.Entry(show).Reference(s => s.Metadata).LoadAsync(); + await _dbContext.Entry(show).Collection(s => s.Sources).LoadAsync(); + }); + + return maybeShow; + } + + public async Task> AddShow( + int localMediaSourceId, + string showFolder, + TelevisionShowMetadata metadata) + { + try + { + var show = new TelevisionShow + { + Sources = new List(), + Metadata = metadata, + Seasons = new List() + }; + + show.Sources.Add( + new LocalTelevisionShowSource + { + MediaSourceId = localMediaSourceId, + Path = showFolder, + TelevisionShow = show + }); + + await _dbContext.TelevisionShows.AddAsync(show); + await _dbContext.SaveChangesAsync(); + + return show; + } + catch (Exception ex) + { + return BaseError.New(ex.Message); + } } public async Task> GetOrAddSeason( @@ -143,35 +188,35 @@ namespace ErsatzTV.Infrastructure.Data.Repositories () => AddEpisode(season, mediaSourceId, path)); } - public Task> FindRemovedShows( - LocalMediaSource localMediaSource, - List allShowFolders) => + public Task DeleteMissingSources(int localMediaSourceId, List allFolders) => + _dbContext.LocalTelevisionShowSources + .Where(s => s.MediaSourceId == localMediaSourceId && !allFolders.Contains(s.Path)) + .ToListAsync() + .Bind( + list => + { + _dbContext.LocalTelevisionShowSources.RemoveRange(list); + return _dbContext.SaveChangesAsync(); + }) + .ToUnit(); + + public Task DeleteEmptyShows() => _dbContext.TelevisionShows - .Filter(s => s.MediaSourceId == localMediaSource.Id) - .Filter(s => !allShowFolders.Contains(s.Path)) - .ToListAsync(); + .Where(s => s.Sources.Count == 0) + .ToListAsync() + .Bind( + list => + { + _dbContext.TelevisionShows.RemoveRange(list); + return _dbContext.SaveChangesAsync(); + }) + .ToUnit(); public Task Delete(TelevisionShow show) => _dbContext.TelevisionShows.Remove(show).AsTask() .Bind(_ => _dbContext.SaveChangesAsync()) .ToUnit(); - private async Task> AddShow(int mediaSourceId, string path) - { - try - { - var show = new TelevisionShow - { MediaSourceId = mediaSourceId, Path = path, Seasons = new List() }; - await _dbContext.TelevisionShows.AddAsync(show); - await _dbContext.SaveChangesAsync(); - return show; - } - catch (Exception ex) - { - return BaseError.New(ex.Message); - } - } - private async Task> AddSeason( TelevisionShow show, string path, diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 28f11f058..ebab22ffe 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -31,6 +31,7 @@ namespace ErsatzTV.Infrastructure.Data public DbSet FFmpegProfiles { get; set; } public DbSet Resolutions { get; set; } public DbSet TelevisionShows { get; set; } + public DbSet LocalTelevisionShowSources { get; set; } public DbSet TelevisionShowMetadata { get; set; } public DbSet TelevisionSeasons { get; set; } diff --git a/ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.Designer.cs b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs similarity index 95% rename from ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.Designer.cs rename to ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs index 72749e306..5951589c4 100644 --- a/ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.Designer.cs +++ b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs @@ -9,8 +9,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace ErsatzTV.Infrastructure.Migrations { [DbContext(typeof(TvContext))] - [Migration("20210218214650_Statistics")] - partial class Statistics + [Migration("20210219165123_TelevisionExpansion")] + partial class TelevisionExpansion { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -553,12 +553,6 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("MediaSourceId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - b.Property("Poster") .HasColumnType("TEXT"); @@ -567,8 +561,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasKey("Id"); - b.HasIndex("MediaSourceId"); - b.ToTable("TelevisionShows"); }); @@ -607,6 +599,28 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("TelevisionShowMetadata"); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionShowSource"); + + b.HasDiscriminator("Discriminator").HasValue("TelevisionShowSource"); + }); + modelBuilder.Entity("MediaItemSimpleMediaCollection", b => { b.Property("ItemsId") @@ -730,6 +744,21 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleOneItems"); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasBaseType("ErsatzTV.Core.Domain.TelevisionShowSource"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasIndex("MediaSourceId"); + + b.HasDiscriminator().HasValue("LocalTelevisionShowSource"); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => { b.HasOne("ErsatzTV.Core.Domain.FFmpegProfile", "FFmpegProfile") @@ -994,22 +1023,22 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("TelevisionShow"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShow", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") - .WithMany() - .HasForeignKey("MediaSourceId") + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Source"); + b.Navigation("TelevisionShow"); }); - modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowMetadata", b => + modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b => { b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") - .WithOne("Metadata") - .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") + .WithMany("Sources") + .HasForeignKey("TelevisionShowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1129,6 +1158,17 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); + modelBuilder.Entity("ErsatzTV.Core.Domain.LocalTelevisionShowSource", b => + { + b.HasOne("ErsatzTV.Core.Domain.LocalMediaSource", "MediaSource") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => { b.Navigation("Playouts"); @@ -1158,6 +1198,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Metadata"); b.Navigation("Seasons"); + + b.Navigation("Sources"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => diff --git a/ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.cs b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs similarity index 91% rename from ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.cs rename to ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs index 89b133f11..581b0537a 100644 --- a/ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.cs +++ b/ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace ErsatzTV.Infrastructure.Migrations { - public partial class Statistics : Migration + public partial class TelevisionExpansion : Migration { protected override void Up(MigrationBuilder migrationBuilder) { @@ -117,21 +117,10 @@ namespace ErsatzTV.Infrastructure.Migrations { Id = table.Column("INTEGER", nullable: false) .Annotation("Sqlite:Autoincrement", true), - MediaSourceId = table.Column("INTEGER", nullable: false), - Path = table.Column("TEXT", nullable: true), Poster = table.Column("TEXT", nullable: true), PosterLastWriteTime = table.Column("TEXT", nullable: true) }, - constraints: table => - { - table.PrimaryKey("PK_TelevisionShows", x => x.Id); - table.ForeignKey( - "FK_TelevisionShows_MediaSources_MediaSourceId", - x => x.MediaSourceId, - "MediaSources", - "Id", - onDelete: ReferentialAction.Cascade); - }); + constraints: table => { table.PrimaryKey("PK_TelevisionShows", x => x.Id); }); migrationBuilder.CreateTable( "MovieMetadata", @@ -210,6 +199,34 @@ namespace ErsatzTV.Infrastructure.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + "TelevisionShowSource", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + TelevisionShowId = table.Column("INTEGER", nullable: false), + Discriminator = table.Column("TEXT", nullable: false), + MediaSourceId = table.Column("INTEGER", nullable: true), + Path = table.Column("TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TelevisionShowSource", x => x.Id); + table.ForeignKey( + "FK_TelevisionShowSource_LocalMediaSources_MediaSourceId", + x => x.MediaSourceId, + "LocalMediaSources", + "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + "FK_TelevisionShowSource_TelevisionShows_TelevisionShowId", + x => x.TelevisionShowId, + "TelevisionShows", + "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( "TelevisionEpisodes", table => new @@ -291,9 +308,14 @@ namespace ErsatzTV.Infrastructure.Migrations unique: true); migrationBuilder.CreateIndex( - "IX_TelevisionShows_MediaSourceId", - "TelevisionShows", + "IX_TelevisionShowSource_MediaSourceId", + "TelevisionShowSource", "MediaSourceId"); + + migrationBuilder.CreateIndex( + "IX_TelevisionShowSource_TelevisionShowId", + "TelevisionShowSource", + "TelevisionShowId"); } protected override void Down(MigrationBuilder migrationBuilder) @@ -307,6 +329,9 @@ namespace ErsatzTV.Infrastructure.Migrations migrationBuilder.DropTable( "TelevisionShowMetadata"); + migrationBuilder.DropTable( + "TelevisionShowSource"); + migrationBuilder.DropTable( "Movies"); diff --git a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs index 54c642edf..8ce8c69ab 100644 --- a/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs +++ b/ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs @@ -593,12 +593,6 @@ namespace ErsatzTV.Infrastructure.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("MediaSourceId") - .HasColumnType("INTEGER"); - - b.Property("Path") - .HasColumnType("TEXT"); - b.Property("Poster") .HasColumnType("TEXT"); @@ -607,8 +601,6 @@ namespace ErsatzTV.Infrastructure.Migrations b.HasKey("Id"); - b.HasIndex("MediaSourceId"); - b.ToTable("TelevisionShows"); }); @@ -649,6 +641,30 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("TelevisionShowMetadata"); }); + modelBuilder.Entity( + "ErsatzTV.Core.Domain.TelevisionShowSource", + b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TelevisionShowId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TelevisionShowId"); + + b.ToTable("TelevisionShowSource"); + + b.HasDiscriminator("Discriminator").HasValue("TelevisionShowSource"); + }); + modelBuilder.Entity( "MediaItemSimpleMediaCollection", b => @@ -794,6 +810,23 @@ namespace ErsatzTV.Infrastructure.Migrations b.ToTable("ProgramScheduleOneItems"); }); + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalTelevisionShowSource", + b => + { + b.HasBaseType("ErsatzTV.Core.Domain.TelevisionShowSource"); + + b.Property("MediaSourceId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasIndex("MediaSourceId"); + + b.HasDiscriminator().HasValue("LocalTelevisionShowSource"); + }); + modelBuilder.Entity( "ErsatzTV.Core.Domain.Channel", b => @@ -1098,25 +1131,25 @@ namespace ErsatzTV.Infrastructure.Migrations }); modelBuilder.Entity( - "ErsatzTV.Core.Domain.TelevisionShow", + "ErsatzTV.Core.Domain.TelevisionShowMetadata", b => { - b.HasOne("ErsatzTV.Core.Domain.MediaSource", "Source") - .WithMany() - .HasForeignKey("MediaSourceId") + b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") + .WithOne("Metadata") + .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Source"); + b.Navigation("TelevisionShow"); }); modelBuilder.Entity( - "ErsatzTV.Core.Domain.TelevisionShowMetadata", + "ErsatzTV.Core.Domain.TelevisionShowSource", b => { b.HasOne("ErsatzTV.Core.Domain.TelevisionShow", "TelevisionShow") - .WithOne("Metadata") - .HasForeignKey("ErsatzTV.Core.Domain.TelevisionShowMetadata", "TelevisionShowId") + .WithMany("Sources") + .HasForeignKey("TelevisionShowId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -1258,6 +1291,19 @@ namespace ErsatzTV.Infrastructure.Migrations .IsRequired(); }); + modelBuilder.Entity( + "ErsatzTV.Core.Domain.LocalTelevisionShowSource", + b => + { + b.HasOne("ErsatzTV.Core.Domain.LocalMediaSource", "MediaSource") + .WithMany() + .HasForeignKey("MediaSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MediaSource"); + }); + modelBuilder.Entity("ErsatzTV.Core.Domain.Channel", b => { b.Navigation("Playouts"); }); modelBuilder.Entity( @@ -1287,6 +1333,8 @@ namespace ErsatzTV.Infrastructure.Migrations b.Navigation("Metadata"); b.Navigation("Seasons"); + + b.Navigation("Sources"); }); modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b => { b.Navigation("Metadata"); }); diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 0736c2839..ef11f331a 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -176,15 +176,12 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); - // services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); - // services.AddScoped(); services.AddScoped(); services.AddScoped();