Browse Source

no more duplicate television shows, even with the same show split across sources

pull/26/head
Jason Dove 6 years ago
parent
commit
1f28b1a945
  1. 9
      ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs
  2. 7
      ErsatzTV.Core/Domain/TelevisionShow.cs
  3. 9
      ErsatzTV.Core/Domain/TelevisionShowSource.cs
  4. 9
      ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs
  5. 12
      ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs
  6. 5
      ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs
  7. 13
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  8. 9
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  9. 4
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  10. 36
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  11. 102
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  12. 5
      ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs
  13. 127
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs
  14. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  15. 78
      ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs
  16. 55
      ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs
  17. 80
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  18. 5
      ErsatzTV/Startup.cs

9
ErsatzTV.Core/Domain/LocalTelevisionShowSource.cs

@ -0,0 +1,9 @@ @@ -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; }
}
}

7
ErsatzTV.Core/Domain/TelevisionShow.cs

@ -1,17 +1,14 @@ @@ -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<TelevisionShowSource> Sources { get; set; }
public TelevisionShowMetadata Metadata { get; set; }
public List<TelevisionSeason> Seasons { get; set; }
public string Path { get; set; }
public string Poster { get; set; }
public DateTime? PosterLastWriteTime { get; set; }
}

9
ErsatzTV.Core/Domain/TelevisionShowSource.cs

@ -0,0 +1,9 @@ @@ -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; }
}
}

9
ErsatzTV.Core/Interfaces/Metadata/IFallbackMetadataProvider.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface IFallbackMetadataProvider
{
TelevisionShowMetadata GetFallbackMetadataForShow(string showFolder);
}
}

12
ErsatzTV.Core/Interfaces/Metadata/ILocalMediaScanner.cs

@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ILocalMediaScanner
{
Task<Unit> ScanMovies(LocalMediaSource localMediaSource, string ffprobePath);
Task<Unit> ScanTelevision(LocalMediaSource localMediaSource, string ffprobePath);
}
}

5
ErsatzTV.Core/Interfaces/Metadata/ILocalMetadataProvider.cs

@ -6,9 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Metadata @@ -6,9 +6,10 @@ namespace ErsatzTV.Core.Interfaces.Metadata
{
public interface ILocalMetadataProvider
{
Task<TelevisionShowMetadata> GetMetadataForShow(string showFolder);
Task<Unit> RefreshSidecarMetadata(MediaItem mediaItem, string path);
Task<Unit> RefreshSidecarMetadata(TelevisionShow televisionShow, string path);
Task<Unit> RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder);
Task<Unit> RefreshFallbackMetadata(MediaItem mediaItem);
Task<Unit> RefreshFallbackMetadata(TelevisionShow televisionShow);
Task<Unit> RefreshFallbackMetadata(TelevisionShow televisionShow, string showFolder);
}
}

13
ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs

@ -7,7 +7,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -7,7 +7,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
{
public interface ITelevisionRepository
{
Task<List<TelevisionShow>> GetAllByMediaSourceId(int mediaSourceId);
Task<bool> Update(TelevisionShow show);
Task<bool> Update(TelevisionSeason season);
Task<bool> Update(TelevisionEpisodeMediaItem episode);
@ -16,7 +15,13 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -16,7 +15,13 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<List<TelevisionShow>> GetPagedShows(int pageNumber, int pageSize);
Task<int> GetSeasonCount(int televisionShowId);
Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize);
Task<Either<BaseError, TelevisionShow>> GetOrAddShow(int mediaSourceId, string path);
Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path);
Task<Option<TelevisionShow>> GetShowByMetadata(TelevisionShowMetadata metadata);
Task<Either<BaseError, TelevisionShow>> AddShow(
int localMediaSourceId,
string showFolder,
TelevisionShowMetadata metadata);
Task<Either<BaseError, TelevisionSeason>> GetOrAddSeason(
TelevisionShow show,
@ -28,7 +33,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories @@ -28,7 +33,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
int mediaSourceId,
string path);
Task<List<TelevisionShow>> FindRemovedShows(LocalMediaSource localMediaSource, List<string> allShowFolders);
Task<Unit> Delete(TelevisionShow show);
Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> allFolders);
Task<Unit> DeleteEmptyShows();
}
}

9
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -2,16 +2,17 @@ @@ -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);
}

4
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -98,5 +98,9 @@ namespace ErsatzTV.Core.Metadata @@ -98,5 +98,9 @@ namespace ErsatzTV.Core.Metadata
return Task.CompletedTask;
});
}
protected Task<Either<BaseError, string>> SavePosterToDisk(string posterPath, int height = 220) =>
File.ReadAllBytesAsync(posterPath)
.Bind(bytes => _imageCache.ResizeAndSaveImage(bytes, height, null));
}
}

36
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -16,6 +16,8 @@ namespace ErsatzTV.Core.Metadata @@ -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<LocalMetadataProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository;
@ -24,13 +26,32 @@ namespace ErsatzTV.Core.Metadata @@ -24,13 +26,32 @@ namespace ErsatzTV.Core.Metadata
public LocalMetadataProvider(
IMediaItemRepository mediaItemRepository,
ITelevisionRepository televisionRepository,
IFallbackMetadataProvider fallbackMetadataProvider,
ILocalFileSystem localFileSystem,
ILogger<LocalMetadataProvider> logger)
{
_mediaItemRepository = mediaItemRepository;
_televisionRepository = televisionRepository;
_fallbackMetadataProvider = fallbackMetadataProvider;
_localFileSystem = localFileSystem;
_logger = logger;
}
public Task<TelevisionShowMetadata> 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<Unit> RefreshSidecarMetadata(MediaItem mediaItem, string path) =>
mediaItem switch
{
@ -41,8 +62,8 @@ namespace ErsatzTV.Core.Metadata @@ -41,8 +62,8 @@ namespace ErsatzTV.Core.Metadata
_ => Task.FromResult(Unit.Default)
};
public Task<Unit> RefreshSidecarMetadata(TelevisionShow televisionShow, string path) =>
LoadMetadata(televisionShow, path).Bind(
public Task<Unit> RefreshSidecarMetadata(TelevisionShow televisionShow, string showFolder) =>
LoadMetadata(televisionShow, showFolder).Bind(
maybeMetadata => maybeMetadata.IfSomeAsync(metadata => ApplyMetadataUpdate(televisionShow, metadata)));
public Task<Unit> RefreshFallbackMetadata(MediaItem mediaItem) =>
@ -54,8 +75,9 @@ namespace ErsatzTV.Core.Metadata @@ -54,8 +75,9 @@ namespace ErsatzTV.Core.Metadata
_ => Task.FromResult(Unit.Default)
};
public Task<Unit> RefreshFallbackMetadata(TelevisionShow televisionShow) =>
ApplyMetadataUpdate(televisionShow, FallbackMetadataProvider.GetFallbackMetadata(televisionShow)).ToUnit();
public Task<Unit> 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 @@ -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);
}

102
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -1,5 +1,4 @@ @@ -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 @@ -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<BaseError, TelevisionShow> maybeShow = await _televisionRepository
.GetOrAddShow(localMediaSource.Id, showFolder)
.BindT(UpdateMetadata)
.BindT(UpdatePoster);
// TODO: check all sources for latest metadata?
Either<BaseError, TelevisionShow> 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<TelevisionShow> 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<Unit> ScanSeasons(LocalMediaSource localMediaSource, string ffprobePath, TelevisionShow show)
private async Task<Either<BaseError, TelevisionShow>> FindOrCreateShow(
int localMediaSourceId,
string showFolder)
{
Option<TelevisionShow> maybeShowByPath =
await _televisionRepository.GetShowByPath(localMediaSourceId, showFolder);
return await maybeShowByPath.Match(
show => Right<BaseError, TelevisionShow>(show).AsTask(),
async () =>
{
TelevisionShowMetadata metadata = await _localMetadataProvider.GetMetadataForShow(showFolder);
Option<TelevisionShow> 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<BaseError, TelevisionShow>(show);
},
async () => await _televisionRepository.AddShow(localMediaSourceId, showFolder, metadata));
});
}
private async Task<Unit> 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<int> maybeSeasonNumber = SeasonNumberForFolder(seasonFolder);
await maybeSeasonNumber.IfSomeAsync(
@ -102,7 +132,7 @@ namespace ErsatzTV.Core.Metadata @@ -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<BaseError, TelevisionEpisodeMediaItem> maybeEpisode = await _televisionRepository
@ -118,11 +148,13 @@ namespace ErsatzTV.Core.Metadata @@ -118,11 +148,13 @@ namespace ErsatzTV.Core.Metadata
return Unit.Default;
}
private async Task<Either<BaseError, TelevisionShow>> UpdateMetadata(TelevisionShow show)
private async Task<Either<BaseError, TelevisionShow>> 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 @@ -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 @@ -181,18 +213,34 @@ namespace ErsatzTV.Core.Metadata
}
}
private async Task<Either<BaseError, TelevisionShow>> UpdatePoster(TelevisionShow show)
private async Task<Either<BaseError, TelevisionShow>> 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<BaseError, string> 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 @@ -251,18 +299,18 @@ namespace ErsatzTV.Core.Metadata
}
}
private Option<string> LocateNfoFile(TelevisionShow show) =>
Optional(Path.Combine(show.Path, "tvshow.nfo"))
private Option<string> LocateNfoFileForShow(string showFolder) =>
Optional(Path.Combine(showFolder, "tvshow.nfo"))
.Filter(s => _localFileSystem.FileExists(s));
private Option<string> LocateNfoFile(TelevisionEpisodeMediaItem episode) =>
Optional(Path.ChangeExtension(episode.Path, "nfo"))
.Filter(s => _localFileSystem.FileExists(s));
private Option<string> LocatePoster(TelevisionShow show) =>
private Option<string> 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();

5
ErsatzTV.Infrastructure/Data/Configurations/TelevisionShowConfiguration.cs

@ -17,6 +17,11 @@ namespace ErsatzTV.Infrastructure.Data.Configurations @@ -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);
}
}
}

127
ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

@ -17,13 +17,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -17,13 +17,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public TelevisionRepository(TvContext dbContext) => _dbContext = dbContext;
public Task<List<TelevisionShow>> GetAllByMediaSourceId(int mediaSourceId) =>
_dbContext.TelevisionShows
.Include(s => s.Metadata)
.Include(s => s.Seasons)
.ThenInclude(s => s.Episodes)
.ToListAsync();
public async Task<bool> Update(TelevisionShow show)
{
_dbContext.TelevisionShows.Update(show);
@ -60,13 +53,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -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<int> GetSeasonCount(int televisionShowId)
{
@ -105,15 +95,70 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -105,15 +95,70 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Task.FromResult(new List<TelevisionSeason>()));
}
public async Task<Either<BaseError, TelevisionShow>> GetOrAddShow(int mediaSourceId, string path)
public async Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path)
{
Option<TelevisionShow> maybeExisting = await _dbContext.TelevisionShows
.Include(i => i.Metadata)
.SingleOrDefaultAsync(i => i.Path == path);
Option<int> maybeShowId = await _dbContext.LocalTelevisionShowSources
.SingleOrDefaultAsync(s => s.MediaSourceId == mediaSourceId && s.Path == path)
.Map(Optional)
.MapT(s => s.TelevisionShowId);
return await maybeExisting.Match(
show => Right<BaseError, TelevisionShow>(show).AsTask(),
() => AddShow(mediaSourceId, path));
return await maybeShowId.Match<Task<Option<TelevisionShow>>>(
async id => await _dbContext.TelevisionShows
.Include(s => s.Metadata)
.Include(s => s.Sources)
.SingleOrDefaultAsync(s => s.Id == id),
() => Task.FromResult(Option<TelevisionShow>.None));
}
public async Task<Option<TelevisionShow>> GetShowByMetadata(TelevisionShowMetadata metadata)
{
Option<TelevisionShow> 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<Either<BaseError, TelevisionShow>> AddShow(
int localMediaSourceId,
string showFolder,
TelevisionShowMetadata metadata)
{
try
{
var show = new TelevisionShow
{
Sources = new List<TelevisionShowSource>(),
Metadata = metadata,
Seasons = new List<TelevisionSeason>()
};
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<Either<BaseError, TelevisionSeason>> GetOrAddSeason(
@ -143,35 +188,35 @@ namespace ErsatzTV.Infrastructure.Data.Repositories @@ -143,35 +188,35 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
() => AddEpisode(season, mediaSourceId, path));
}
public Task<List<TelevisionShow>> FindRemovedShows(
LocalMediaSource localMediaSource,
List<string> allShowFolders) =>
public Task<Unit> DeleteMissingSources(int localMediaSourceId, List<string> 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<Unit> 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<Unit> Delete(TelevisionShow show) =>
_dbContext.TelevisionShows.Remove(show).AsTask()
.Bind(_ => _dbContext.SaveChangesAsync())
.ToUnit();
private async Task<Either<BaseError, TelevisionShow>> AddShow(int mediaSourceId, string path)
{
try
{
var show = new TelevisionShow
{ MediaSourceId = mediaSourceId, Path = path, Seasons = new List<TelevisionSeason>() };
await _dbContext.TelevisionShows.AddAsync(show);
await _dbContext.SaveChangesAsync();
return show;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private async Task<Either<BaseError, TelevisionSeason>> AddSeason(
TelevisionShow show,
string path,

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -31,6 +31,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -31,6 +31,7 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<FFmpegProfile> FFmpegProfiles { get; set; }
public DbSet<Resolution> Resolutions { get; set; }
public DbSet<TelevisionShow> TelevisionShows { get; set; }
public DbSet<LocalTelevisionShowSource> LocalTelevisionShowSources { get; set; }
public DbSet<TelevisionShowMetadata> TelevisionShowMetadata { get; set; }
public DbSet<TelevisionSeason> TelevisionSeasons { get; set; }

78
ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.Designer.cs → ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.Designer.cs generated

@ -9,8 +9,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -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 @@ -553,12 +553,6 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
@ -567,8 +561,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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 @@ -607,6 +599,28 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("TelevisionShowMetadata");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.TelevisionShowSource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("TelevisionShowId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("TelevisionShowId");
b.ToTable("TelevisionShowSource");
b.HasDiscriminator<string>("Discriminator").HasValue("TelevisionShowSource");
});
modelBuilder.Entity("MediaItemSimpleMediaCollection", b =>
{
b.Property<int>("ItemsId")
@ -730,6 +744,21 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("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 @@ -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 @@ -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 @@ -1158,6 +1198,8 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Navigation("Metadata");
b.Navigation("Seasons");
b.Navigation("Sources");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMediaItem", b =>

55
ErsatzTV.Infrastructure/Migrations/20210218214650_Statistics.cs → ErsatzTV.Infrastructure/Migrations/20210219165123_TelevisionExpansion.cs

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; @@ -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 @@ -117,21 +117,10 @@ namespace ErsatzTV.Infrastructure.Migrations
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
MediaSourceId = table.Column<int>("INTEGER", nullable: false),
Path = table.Column<string>("TEXT", nullable: true),
Poster = table.Column<string>("TEXT", nullable: true),
PosterLastWriteTime = table.Column<DateTime>("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 @@ -210,6 +199,34 @@ namespace ErsatzTV.Infrastructure.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
"TelevisionShowSource",
table => new
{
Id = table.Column<int>("INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TelevisionShowId = table.Column<int>("INTEGER", nullable: false),
Discriminator = table.Column<string>("TEXT", nullable: false),
MediaSourceId = table.Column<int>("INTEGER", nullable: true),
Path = table.Column<string>("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 @@ -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 @@ -307,6 +329,9 @@ namespace ErsatzTV.Infrastructure.Migrations
migrationBuilder.DropTable(
"TelevisionShowMetadata");
migrationBuilder.DropTable(
"TelevisionShowSource");
migrationBuilder.DropTable(
"Movies");

80
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -593,12 +593,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -593,12 +593,6 @@ namespace ErsatzTV.Infrastructure.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("Path")
.HasColumnType("TEXT");
b.Property<string>("Poster")
.HasColumnType("TEXT");
@ -607,8 +601,6 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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 @@ -649,6 +641,30 @@ namespace ErsatzTV.Infrastructure.Migrations
b.ToTable("TelevisionShowMetadata");
});
modelBuilder.Entity(
"ErsatzTV.Core.Domain.TelevisionShowSource",
b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Discriminator")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("TelevisionShowId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("TelevisionShowId");
b.ToTable("TelevisionShowSource");
b.HasDiscriminator<string>("Discriminator").HasValue("TelevisionShowSource");
});
modelBuilder.Entity(
"MediaItemSimpleMediaCollection",
b =>
@ -794,6 +810,23 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -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<int>("MediaSourceId")
.HasColumnType("INTEGER");
b.Property<string>("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 @@ -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 @@ -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 @@ -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"); });

5
ErsatzTV/Startup.cs

@ -176,15 +176,12 @@ namespace ErsatzTV @@ -176,15 +176,12 @@ namespace ErsatzTV
services.AddScoped<IFFmpegLocator, FFmpegLocator>();
services.AddScoped<ISmartCollectionBuilder, SmartCollectionBuilder>();
services.AddScoped<ILocalMetadataProvider, LocalMetadataProvider>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
services.AddScoped<ILocalPosterProvider, LocalPosterProvider>();
// services.AddScoped<ILocalMediaScanner, LocalMediaScanner>();
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
services.AddScoped<IImageCache, ImageCache>();
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
// services.AddScoped<ILocalMediaSourcePlanner, LocalMediaSourcePlanner>();
// services.AddScoped<ILocalMediaSourceMovieScanner, LocalMediaSourceMovieScanner>();
// services.AddScoped<ILocalMediaSourceTelevisionScanner, LocalMediaSourceTelevisionScanner>();
services.AddScoped<IMovieFolderScanner, MovieFolderScanner>();
services.AddScoped<ITelevisionFolderScanner, TelevisionFolderScanner>();

Loading…
Cancel
Save