Browse Source

optimize local scanning (#84)

* optimize local scanning

* fix artwork updates

* fix adding genres and tags

* fix movie fallback metadata
pull/85/head
Jason Dove 5 years ago committed by GitHub
parent
commit
739d074bc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs
  2. 1
      ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs
  3. 5
      ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs
  4. 1
      ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs
  5. 4
      ErsatzTV.Core/Interfaces/Repositories/ITelevisionRepository.cs
  6. 3
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  7. 21
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  8. 39
      ErsatzTV.Core/Metadata/LocalMetadataProvider.cs
  9. 5
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  10. 9
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  11. 24
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  12. 4
      ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs
  13. 2
      ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs
  14. 5
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  15. 41
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  16. 44
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  17. 273
      ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs

14
ErsatzTV.Core.Tests/Fakes/FakeTelevisionRepository.cs

@ -11,12 +11,6 @@ namespace ErsatzTV.Core.Tests.Fakes
{ {
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException(); public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
public Task<bool> Update(Show show) => throw new NotSupportedException();
public Task<bool> Update(Season season) => throw new NotSupportedException();
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
public Task<List<Show>> GetAllShows() => throw new NotSupportedException(); public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException(); public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
@ -86,5 +80,13 @@ namespace ErsatzTV.Core.Tests.Fakes
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) => public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
throw new NotSupportedException(); throw new NotSupportedException();
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
public Task<bool> Update(Show show) => throw new NotSupportedException();
public Task<bool> Update(Season season) => throw new NotSupportedException();
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
} }
} }

1
ErsatzTV.Core.Tests/Metadata/MovieFolderScannerTests.cs

@ -417,6 +417,7 @@ namespace ErsatzTV.Core.Tests.Metadata
_movieRepository.Object, _movieRepository.Object,
_localStatisticsProvider.Object, _localStatisticsProvider.Object,
_localMetadataProvider.Object, _localMetadataProvider.Object,
new Mock<IMetadataRepository>().Object,
_imageCache.Object, _imageCache.Object,
new Mock<ILogger<MovieFolderScanner>>().Object new Mock<ILogger<MovieFolderScanner>>().Object
); );

5
ErsatzTV.Core/Interfaces/Repositories/IMetadataRepository.cs

@ -7,7 +7,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public interface IMetadataRepository public interface IMetadataRepository
{ {
Task<Unit> RemoveGenre(Genre genre); Task<Unit> RemoveGenre(Genre genre);
Task<Unit> UpdateStatistics(MediaVersion mediaVersion); Task<bool> Update(Domain.Metadata metadata);
Task<bool> Add(Domain.Metadata metadata);
Task<bool> UpdateLocalStatistics(MediaVersion mediaVersion);
Task<Unit> UpdatePlexStatistics(MediaVersion mediaVersion);
Task<Unit> UpdateArtworkPath(Artwork artwork); Task<Unit> UpdateArtworkPath(Artwork artwork);
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork); Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind); Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);

1
ErsatzTV.Core/Interfaces/Repositories/IMovieRepository.cs

@ -11,7 +11,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Option<Movie>> GetMovie(int movieId); Task<Option<Movie>> GetMovie(int movieId);
Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path); Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path);
Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item); Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item);
Task<bool> Update(Movie movie);
Task<int> GetMovieCount(); Task<int> GetMovieCount();
Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize); Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize);
Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath); Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath);

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

@ -8,9 +8,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public interface ITelevisionRepository public interface ITelevisionRepository
{ {
Task<bool> AllShowsExist(List<int> showIds); Task<bool> AllShowsExist(List<int> showIds);
Task<bool> Update(Show show);
Task<bool> Update(Season season);
Task<bool> Update(Episode episode);
Task<List<Show>> GetAllShows(); Task<List<Show>> GetAllShows();
Task<Option<Show>> GetShow(int showId); Task<Option<Show>> GetShow(int showId);
Task<int> GetShowCount(); Task<int> GetShowCount();
@ -39,5 +36,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
Task<Unit> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys); Task<Unit> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys); Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys); Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber);
} }
} }

3
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
@ -86,6 +87,8 @@ namespace ErsatzTV.Core.Metadata
metadata.Title = match.Groups[1].Value; metadata.Title = match.Groups[1].Value;
metadata.Year = int.Parse(match.Groups[2].Value); metadata.Year = int.Parse(match.Groups[2].Value);
metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1); metadata.ReleaseDate = new DateTime(int.Parse(match.Groups[2].Value), 1, 1);
metadata.Genres = new List<Genre>();
metadata.Tags = new List<Tag>();
} }
} }
catch (Exception) catch (Exception)

21
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -7,9 +7,9 @@ using System.Threading.Tasks;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Core.Metadata namespace ErsatzTV.Core.Metadata
{ {
@ -48,17 +48,20 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMetadataRepository _metadataRepository;
static LocalFolderScanner() => Crypto = new SHA1CryptoServiceProvider(); static LocalFolderScanner() => Crypto = new SHA1CryptoServiceProvider();
protected LocalFolderScanner( protected LocalFolderScanner(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
ILogger logger) ILogger logger)
{ {
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_localStatisticsProvider = localStatisticsProvider; _localStatisticsProvider = localStatisticsProvider;
_metadataRepository = metadataRepository;
_imageCache = imageCache; _imageCache = imageCache;
_logger = logger; _logger = logger;
} }
@ -99,17 +102,16 @@ namespace ErsatzTV.Core.Metadata
} }
} }
protected bool RefreshArtwork(string artworkFile, Domain.Metadata metadata, ArtworkKind artworkKind) protected async Task<bool> RefreshArtwork(string artworkFile, Domain.Metadata metadata, ArtworkKind artworkKind)
{ {
DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile); DateTime lastWriteTime = _localFileSystem.GetLastWriteTime(artworkFile);
metadata.Artwork ??= new List<Artwork>(); metadata.Artwork ??= new List<Artwork>();
Option<Artwork> maybeArtwork = Option<Artwork> maybeArtwork = metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind);
Optional(metadata.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == artworkKind);
bool shouldRefresh = maybeArtwork.Match( bool shouldRefresh = maybeArtwork.Match(
artwork => artwork.DateUpdated < lastWriteTime, artwork => lastWriteTime.Subtract(artwork.DateUpdated) > TimeSpan.FromSeconds(1),
true); true);
if (shouldRefresh) if (shouldRefresh)
@ -117,13 +119,14 @@ namespace ErsatzTV.Core.Metadata
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile); _logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind); string cacheName = _imageCache.CopyArtworkToCache(artworkFile, artworkKind);
maybeArtwork.Match( await maybeArtwork.Match(
artwork => async artwork =>
{ {
artwork.Path = cacheName; artwork.Path = cacheName;
artwork.DateUpdated = lastWriteTime; artwork.DateUpdated = lastWriteTime;
await _metadataRepository.UpdateArtworkPath(artwork);
}, },
() => async () =>
{ {
var artwork = new Artwork var artwork = new Artwork
{ {
@ -132,8 +135,8 @@ namespace ErsatzTV.Core.Metadata
DateUpdated = lastWriteTime, DateUpdated = lastWriteTime,
ArtworkKind = artworkKind ArtworkKind = artworkKind
}; };
metadata.Artwork.Add(artwork); metadata.Artwork.Add(artwork);
await _metadataRepository.AddArtwork(metadata, artwork);
}); });
return true; return true;

39
ErsatzTV.Core/Metadata/LocalMetadataProvider.cs

@ -23,16 +23,19 @@ namespace ErsatzTV.Core.Metadata
private readonly ILogger<LocalMetadataProvider> _logger; private readonly ILogger<LocalMetadataProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
private readonly IMetadataRepository _metadataRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public LocalMetadataProvider( public LocalMetadataProvider(
IMediaItemRepository mediaItemRepository, IMediaItemRepository mediaItemRepository,
IMetadataRepository metadataRepository,
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
IFallbackMetadataProvider fallbackMetadataProvider, IFallbackMetadataProvider fallbackMetadataProvider,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILogger<LocalMetadataProvider> logger) ILogger<LocalMetadataProvider> logger)
{ {
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
_metadataRepository = metadataRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
@ -92,8 +95,11 @@ namespace ErsatzTV.Core.Metadata
private async Task ApplyMetadataUpdate(Episode episode, Tuple<EpisodeMetadata, int> metadataEpisodeNumber) private async Task ApplyMetadataUpdate(Episode episode, Tuple<EpisodeMetadata, int> metadataEpisodeNumber)
{ {
(EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber; (EpisodeMetadata metadata, int episodeNumber) = metadataEpisodeNumber;
episode.EpisodeNumber = episodeNumber; if (episode.EpisodeNumber != episodeNumber)
Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match( {
await _televisionRepository.SetEpisodeNumber(episode, episodeNumber);
}
await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone().Match(
existing => existing =>
{ {
existing.Outline = metadata.Outline; existing.Outline = metadata.Outline;
@ -109,20 +115,22 @@ namespace ErsatzTV.Core.Metadata
existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) existing.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title) ? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle; : metadata.SortTitle;
return _metadataRepository.Update(existing);
}, },
() => () =>
{ {
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title) ? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle; : metadata.SortTitle;
metadata.EpisodeId = episode.Id;
episode.EpisodeMetadata = new List<EpisodeMetadata> { metadata }; episode.EpisodeMetadata = new List<EpisodeMetadata> { metadata };
});
await _televisionRepository.Update(episode); return _metadataRepository.Add(metadata);
});
} }
private async Task ApplyMetadataUpdate(Movie movie, MovieMetadata metadata) private Task ApplyMetadataUpdate(Movie movie, MovieMetadata metadata) =>
{
Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match( Optional(movie.MovieMetadata).Flatten().HeadOrNone().Match(
existing => existing =>
{ {
@ -163,20 +171,21 @@ namespace ErsatzTV.Core.Metadata
{ {
existing.Tags.Add(tag); existing.Tags.Add(tag);
} }
return _metadataRepository.Update(existing);
}, },
() => () =>
{ {
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title) ? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle; : metadata.SortTitle;
metadata.MovieId = movie.Id;
movie.MovieMetadata = new List<MovieMetadata> { metadata }; movie.MovieMetadata = new List<MovieMetadata> { metadata };
});
await _mediaItemRepository.Update(movie); return _metadataRepository.Add(metadata);
} });
private async Task ApplyMetadataUpdate(Show show, ShowMetadata metadata) private Task ApplyMetadataUpdate(Show show, ShowMetadata metadata) =>
{
Optional(show.ShowMetadata).Flatten().HeadOrNone().Match( Optional(show.ShowMetadata).Flatten().HeadOrNone().Match(
existing => existing =>
{ {
@ -217,17 +226,19 @@ namespace ErsatzTV.Core.Metadata
{ {
existing.Tags.Add(tag); existing.Tags.Add(tag);
} }
return _metadataRepository.Update(existing);
}, },
() => () =>
{ {
metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle) metadata.SortTitle = string.IsNullOrWhiteSpace(metadata.SortTitle)
? _fallbackMetadataProvider.GetSortTitle(metadata.Title) ? _fallbackMetadataProvider.GetSortTitle(metadata.Title)
: metadata.SortTitle; : metadata.SortTitle;
metadata.ShowId = show.Id;
show.ShowMetadata = new List<ShowMetadata> { metadata }; show.ShowMetadata = new List<ShowMetadata> { metadata };
});
await _televisionRepository.Update(show); return _metadataRepository.Add(metadata);
} });
private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName) private async Task<Option<MovieMetadata>> LoadMetadata(Movie mediaItem, string nfoFileName)
{ {

5
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -18,13 +18,16 @@ namespace ErsatzTV.Core.Metadata
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<LocalStatisticsProvider> _logger; private readonly ILogger<LocalStatisticsProvider> _logger;
private readonly IMediaItemRepository _mediaItemRepository; private readonly IMediaItemRepository _mediaItemRepository;
private readonly IMetadataRepository _metadataRepository;
public LocalStatisticsProvider( public LocalStatisticsProvider(
IMediaItemRepository mediaItemRepository, IMediaItemRepository mediaItemRepository,
IMetadataRepository metadataRepository,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ILogger<LocalStatisticsProvider> logger) ILogger<LocalStatisticsProvider> logger)
{ {
_mediaItemRepository = mediaItemRepository; _mediaItemRepository = mediaItemRepository;
_metadataRepository = metadataRepository;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_logger = logger; _logger = logger;
} }
@ -79,7 +82,7 @@ namespace ErsatzTV.Core.Metadata
mediaItemVersion.VideoProfile = version.VideoProfile; mediaItemVersion.VideoProfile = version.VideoProfile;
mediaItemVersion.VideoScanKind = version.VideoScanKind; mediaItemVersion.VideoScanKind = version.VideoScanKind;
return await _mediaItemRepository.Update(mediaItem) && durationChange; return await _metadataRepository.UpdateLocalStatistics(mediaItemVersion) && durationChange;
} }
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath) private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)

9
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -27,9 +27,10 @@ namespace ErsatzTV.Core.Metadata
IMovieRepository movieRepository, IMovieRepository movieRepository,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider, ILocalMetadataProvider localMetadataProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
ILogger<MovieFolderScanner> logger) ILogger<MovieFolderScanner> logger)
: base(localFileSystem, localStatisticsProvider, imageCache, logger) : base(localFileSystem, localStatisticsProvider, metadataRepository, imageCache, logger)
{ {
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_movieRepository = movieRepository; _movieRepository = movieRepository;
@ -74,7 +75,6 @@ namespace ErsatzTV.Core.Metadata
foreach (string file in allFiles.OrderBy(identity)) foreach (string file in allFiles.OrderBy(identity))
{ {
// TODO: optimize dbcontext use here, do we need tracking? can we make partial updates with dapper?
// TODO: figure out how to rebuild playlists // TODO: figure out how to rebuild playlists
Either<BaseError, Movie> maybeMovie = await _movieRepository Either<BaseError, Movie> maybeMovie = await _movieRepository
.GetOrAdd(libraryPath, file) .GetOrAdd(libraryPath, file)
@ -144,10 +144,7 @@ namespace ErsatzTV.Core.Metadata
async posterFile => async posterFile =>
{ {
MovieMetadata metadata = movie.MovieMetadata.Head(); MovieMetadata metadata = movie.MovieMetadata.Head();
if (RefreshArtwork(posterFile, metadata, artworkKind)) await RefreshArtwork(posterFile, metadata, artworkKind);
{
await _movieRepository.Update(movie);
}
}); });
return movie; return movie;

24
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -26,10 +26,12 @@ namespace ErsatzTV.Core.Metadata
ITelevisionRepository televisionRepository, ITelevisionRepository televisionRepository,
ILocalStatisticsProvider localStatisticsProvider, ILocalStatisticsProvider localStatisticsProvider,
ILocalMetadataProvider localMetadataProvider, ILocalMetadataProvider localMetadataProvider,
IMetadataRepository metadataRepository,
IImageCache imageCache, IImageCache imageCache,
ILogger<TelevisionFolderScanner> logger) : base( ILogger<TelevisionFolderScanner> logger) : base(
localFileSystem, localFileSystem,
localStatisticsProvider, localStatisticsProvider,
metadataRepository,
imageCache, imageCache,
logger) logger)
{ {
@ -224,10 +226,7 @@ namespace ErsatzTV.Core.Metadata
async posterFile => async posterFile =>
{ {
ShowMetadata metadata = show.ShowMetadata.Head(); ShowMetadata metadata = show.ShowMetadata.Head();
if (RefreshArtwork(posterFile, metadata, artworkKind)) await RefreshArtwork(posterFile, metadata, artworkKind);
{
await _televisionRepository.Update(show);
}
}); });
return show; return show;
@ -245,18 +244,8 @@ namespace ErsatzTV.Core.Metadata
await LocatePoster(season, seasonFolder).IfSomeAsync( await LocatePoster(season, seasonFolder).IfSomeAsync(
async posterFile => async posterFile =>
{ {
season.SeasonMetadata ??= new List<SeasonMetadata>();
if (!season.SeasonMetadata.Any())
{
season.SeasonMetadata.Add(new SeasonMetadata { SeasonId = season.Id });
}
SeasonMetadata metadata = season.SeasonMetadata.Head(); SeasonMetadata metadata = season.SeasonMetadata.Head();
await RefreshArtwork(posterFile, metadata, ArtworkKind.Poster);
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Poster))
{
await _televisionRepository.Update(season);
}
}); });
return season; return season;
@ -275,10 +264,7 @@ namespace ErsatzTV.Core.Metadata
async posterFile => async posterFile =>
{ {
EpisodeMetadata metadata = episode.EpisodeMetadata.Head(); EpisodeMetadata metadata = episode.EpisodeMetadata.Head();
if (RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail)) await RefreshArtwork(posterFile, metadata, ArtworkKind.Thumbnail);
{
await _televisionRepository.Update(episode);
}
}); });
return episode; return episode;

4
ErsatzTV.Core/Plex/PlexMovieLibraryScanner.cs

@ -97,7 +97,7 @@ namespace ErsatzTV.Core.Plex
existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
existingVersion.DateUpdated = incomingVersion.DateUpdated; existingVersion.DateUpdated = incomingVersion.DateUpdated;
await _metadataRepository.UpdateStatistics(existingVersion); await _metadataRepository.UpdatePlexStatistics(existingVersion);
}, },
_ => Task.CompletedTask); _ => Task.CompletedTask);
} }
@ -127,6 +127,8 @@ namespace ErsatzTV.Core.Plex
existingMetadata.Genres.Add(genre); existingMetadata.Genres.Add(genre);
await _movieRepository.AddGenre(existingMetadata, genre); await _movieRepository.AddGenre(existingMetadata, genre);
} }
// TODO: update other metadata?
} }
return existing; return existing;

2
ErsatzTV.Core/Plex/PlexTelevisionLibraryScanner.cs

@ -257,7 +257,7 @@ namespace ErsatzTV.Core.Plex
existingVersion.VideoScanKind = mediaVersion.VideoScanKind; existingVersion.VideoScanKind = mediaVersion.VideoScanKind;
existingVersion.DateUpdated = incomingVersion.DateUpdated; existingVersion.DateUpdated = incomingVersion.DateUpdated;
await _metadataRepository.UpdateStatistics(existingVersion); await _metadataRepository.UpdatePlexStatistics(existingVersion);
}, },
_ => Task.CompletedTask); _ => Task.CompletedTask);
} }

5
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -171,8 +171,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Unit> DeleteAllPlex() public async Task<Unit> DeleteAllPlex()
{ {
await using TvContext context = _dbContextFactory.CreateDbContext(); await using TvContext context = _dbContextFactory.CreateDbContext();
List<PlexMediaSource> allMediaSources = await context.PlexMediaSources.ToListAsync(); List<PlexMediaSource> allMediaSources = await context.PlexMediaSources.ToListAsync();
context.PlexMediaSources.RemoveRange(allMediaSources); context.PlexMediaSources.RemoveRange(allMediaSources);
List<PlexLibrary> allPlexLibraries = await context.PlexLibraries.ToListAsync();
context.PlexLibraries.RemoveRange(allPlexLibraries);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
return Unit.Default; return Unit.Default;
} }

41
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -4,19 +4,56 @@ using Dapper;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Infrastructure.Data.Repositories namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
public class MetadataRepository : IMetadataRepository public class MetadataRepository : IMetadataRepository
{ {
private readonly IDbConnection _dbConnection; private readonly IDbConnection _dbConnection;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MetadataRepository(IDbConnection dbConnection) => _dbConnection = dbConnection; public MetadataRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
{
_dbContextFactory = dbContextFactory;
_dbConnection = dbConnection;
}
public Task<Unit> RemoveGenre(Genre genre) => public Task<Unit> RemoveGenre(Genre genre) =>
_dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id }).ToUnit(); _dbConnection.ExecuteAsync("DELETE FROM Genre WHERE Id = @GenreId", new { GenreId = genre.Id }).ToUnit();
public Task<Unit> UpdateStatistics(MediaVersion mediaVersion) => public async Task<bool> Update(Metadata metadata)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
dbContext.Entry(metadata).State = EntityState.Modified;
return await dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Add(Metadata metadata)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
dbContext.Entry(metadata).State = EntityState.Added;
foreach (Genre genre in metadata.Genres)
{
dbContext.Entry(genre).State = EntityState.Added;
}
foreach (Tag tag in metadata.Tags)
{
dbContext.Entry(tag).State = EntityState.Added;
}
return await dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> UpdateLocalStatistics(MediaVersion mediaVersion)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
dbContext.Entry(mediaVersion).State = EntityState.Modified;
return await dbContext.SaveChangesAsync() > 0;
}
public Task<Unit> UpdatePlexStatistics(MediaVersion mediaVersion) =>
_dbConnection.ExecuteAsync( _dbConnection.ExecuteAsync(
@"UPDATE MediaVersion SET @"UPDATE MediaVersion SET
SampleAspectRatio = @SampleAspectRatio, SampleAspectRatio = @SampleAspectRatio,

44
ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs

@ -16,15 +16,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public class MovieRepository : IMovieRepository public class MovieRepository : IMovieRepository
{ {
private readonly IDbConnection _dbConnection; private readonly IDbConnection _dbConnection;
private readonly TvContext _dbContext;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public MovieRepository( public MovieRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
TvContext dbContext,
IDbContextFactory<TvContext> dbContextFactory,
IDbConnection dbConnection)
{ {
_dbContext = dbContext;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_dbConnection = dbConnection; _dbConnection = dbConnection;
} }
@ -52,7 +47,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path) public async Task<Either<BaseError, Movie>> GetOrAdd(LibraryPath libraryPath, string path)
{ {
Option<Movie> maybeExisting = await _dbContext.Movies await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<Movie> maybeExisting = await dbContext.Movies
.Include(i => i.MovieMetadata) .Include(i => i.MovieMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
.Include(i => i.MovieMetadata) .Include(i => i.MovieMetadata)
@ -67,7 +63,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeExisting.Match( return await maybeExisting.Match(
mediaItem => Right<BaseError, Movie>(mediaItem).AsTask(), mediaItem => Right<BaseError, Movie>(mediaItem).AsTask(),
async () => await AddMovie(libraryPath.Id, path)); async () => await AddMovie(dbContext, libraryPath.Id, path));
} }
public async Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item) public async Task<Either<BaseError, PlexMovie>> GetOrAdd(PlexLibrary library, PlexMovie item)
@ -89,26 +85,24 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
async () => await AddPlexMovie(context, library, item)); async () => await AddPlexMovie(context, library, item));
} }
public async Task<bool> Update(Movie movie)
{
_dbContext.Movies.Update(movie);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<int> GetMovieCount() => public Task<int> GetMovieCount() =>
_dbConnection.QuerySingleAsync<int>(@"SELECT COUNT(DISTINCT MovieId) FROM MovieMetadata"); _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT(DISTINCT MovieId) FROM MovieMetadata");
public Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize) => public async Task<List<MovieMetadata>> GetPagedMovies(int pageNumber, int pageSize)
_dbContext.MovieMetadata.FromSqlRaw( {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.MovieMetadata.FromSqlRaw(
@"SELECT * FROM MovieMetadata WHERE Id IN @"SELECT * FROM MovieMetadata WHERE Id IN
(SELECT Id FROM MovieMetadata GROUP BY MovieId, MetadataKind HAVING MetadataKind = MAX(MetadataKind)) (SELECT Id FROM MovieMetadata GROUP BY MovieId, MetadataKind HAVING MetadataKind = MAX(MetadataKind))
ORDER BY SortTitle ORDER BY SortTitle
LIMIT {0} OFFSET {1}", LIMIT {0} OFFSET {1}",
pageSize, pageSize,
(pageNumber - 1) * pageSize) (pageNumber - 1) * pageSize)
.AsNoTracking()
.Include(mm => mm.Artwork) .Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle) .OrderBy(mm => mm.SortTitle)
.ToListAsync(); .ToListAsync();
}
public Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath) => public Task<IEnumerable<string>> FindMoviePaths(LibraryPath libraryPath) =>
_dbConnection.QueryAsync<string>( _dbConnection.QueryAsync<string>(
@ -122,6 +116,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Unit> DeleteByPath(LibraryPath libraryPath, string path) public async Task<Unit> DeleteByPath(LibraryPath libraryPath, string path)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>( IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@"SELECT M.Id @"SELECT M.Id
FROM Movie M FROM Movie M
@ -133,11 +128,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
foreach (int movieId in ids) foreach (int movieId in ids)
{ {
Movie movie = await _dbContext.Movies.FindAsync(movieId); Movie movie = await dbContext.Movies.FindAsync(movieId);
_dbContext.Movies.Remove(movie); dbContext.Movies.Remove(movie);
} }
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return Unit.Default; return Unit.Default;
} }
@ -156,7 +151,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE lp.LibraryId = @LibraryId AND pm.Key not in @Keys)", WHERE lp.LibraryId = @LibraryId AND pm.Key not in @Keys)",
new { LibraryId = library.Id, Keys = movieKeys }).ToUnit(); new { LibraryId = library.Id, Keys = movieKeys }).ToUnit();
private async Task<Either<BaseError, Movie>> AddMovie(int libraryPathId, string path) private static async Task<Either<BaseError, Movie>> AddMovie(
TvContext dbContext,
int libraryPathId,
string path)
{ {
try try
{ {
@ -174,9 +172,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
}; };
await _dbContext.Movies.AddAsync(movie); await dbContext.Movies.AddAsync(movie);
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await _dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync(); await dbContext.Entry(movie).Reference(m => m.LibraryPath).LoadAsync();
return movie; return movie;
} }
catch (Exception ex) catch (Exception ex)

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

@ -16,15 +16,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public class TelevisionRepository : ITelevisionRepository public class TelevisionRepository : ITelevisionRepository
{ {
private readonly IDbConnection _dbConnection; private readonly IDbConnection _dbConnection;
private readonly TvContext _dbContext;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public TelevisionRepository( public TelevisionRepository(IDbConnection dbConnection, IDbContextFactory<TvContext> dbContextFactory)
TvContext dbContext,
IDbConnection dbConnection,
IDbContextFactory<TvContext> dbContextFactory)
{ {
_dbContext = dbContext;
_dbConnection = dbConnection; _dbConnection = dbConnection;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
} }
@ -35,33 +30,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
new { ShowIds = showIds }) new { ShowIds = showIds })
.Map(c => c == showIds.Count); .Map(c => c == showIds.Count);
public async Task<bool> Update(Show show) public async Task<List<Show>> GetAllShows()
{
_dbContext.Shows.Update(show);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Update(Season season)
{ {
_dbContext.Seasons.Update(season); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await _dbContext.SaveChangesAsync() > 0; return await dbContext.Shows
}
public async Task<bool> Update(Episode episode)
{
_dbContext.Episodes.Update(episode);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<List<Show>> GetAllShows() =>
_dbContext.Shows
.AsNoTracking() .AsNoTracking()
.Include(s => s.ShowMetadata) .Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
.ToListAsync(); .ToListAsync();
}
public Task<Option<Show>> GetShow(int showId) => public async Task<Option<Show>> GetShow(int showId)
_dbContext.Shows {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Shows
.AsNoTracking() .AsNoTracking()
.Filter(s => s.Id == showId) .Filter(s => s.Id == showId)
.Include(s => s.ShowMetadata) .Include(s => s.ShowMetadata)
@ -73,27 +55,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.OrderBy(s => s.Id) .OrderBy(s => s.Id)
.SingleOrDefaultAsync() .SingleOrDefaultAsync()
.Map(Optional); .Map(Optional);
}
public Task<int> GetShowCount() => public async Task<int> GetShowCount()
_dbContext.ShowMetadata {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ShowMetadata
.AsNoTracking() .AsNoTracking()
.GroupBy(sm => new { sm.Title, sm.Year }) .GroupBy(sm => new { sm.Title, sm.Year })
.CountAsync(); .CountAsync();
}
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) => public async Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize)
_dbContext.ShowMetadata.FromSqlRaw( {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.ShowMetadata.FromSqlRaw(
@"SELECT * FROM ShowMetadata WHERE Id IN @"SELECT * FROM ShowMetadata WHERE Id IN
(SELECT MIN(Id) FROM ShowMetadata GROUP BY Title, Year, MetadataKind HAVING MetadataKind = MAX(MetadataKind)) (SELECT MIN(Id) FROM ShowMetadata GROUP BY Title, Year, MetadataKind HAVING MetadataKind = MAX(MetadataKind))
ORDER BY SortTitle ORDER BY SortTitle
LIMIT {0} OFFSET {1}", LIMIT {0} OFFSET {1}",
pageSize, pageSize,
(pageNumber - 1) * pageSize) (pageNumber - 1) * pageSize)
.AsNoTracking()
.Include(mm => mm.Artwork) .Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle) .OrderBy(mm => mm.SortTitle)
.ToListAsync(); .ToListAsync();
}
public Task<List<Season>> GetAllSeasons() => public async Task<List<Season>> GetAllSeasons()
_dbContext.Seasons {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.Include(s => s.SeasonMetadata) .Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
@ -101,9 +93,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ThenInclude(s => s.ShowMetadata) .ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
.ToListAsync(); .ToListAsync();
}
public Task<Option<Season>> GetSeason(int seasonId) => public async Task<Option<Season>> GetSeason(int seasonId)
_dbContext.Seasons {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.Include(s => s.SeasonMetadata) .Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
@ -113,11 +108,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.OrderBy(s => s.Id) .OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == seasonId) .SingleOrDefaultAsync(s => s.Id == seasonId)
.Map(Optional); .Map(Optional);
}
public Task<int> GetSeasonCount(int showId) => public async Task<int> GetSeasonCount(int showId)
_dbContext.Seasons {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.CountAsync(s => s.ShowId == showId); .CountAsync(s => s.ShowId == showId);
}
public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) public async Task<List<Season>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize)
{ {
@ -129,7 +128,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
new { ShowId = televisionShowId }) new { ShowId = televisionShowId })
.Map(results => results.ToList()); .Map(results => results.ToList());
return await _dbContext.Seasons await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Seasons
.AsNoTracking() .AsNoTracking()
.Where(s => showIds.Contains(s.ShowId)) .Where(s => showIds.Contains(s.ShowId))
.Include(s => s.SeasonMetadata) .Include(s => s.SeasonMetadata)
@ -142,8 +142,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync(); .ToListAsync();
} }
public Task<Option<Episode>> GetEpisode(int episodeId) => public async Task<Option<Episode>> GetEpisode(int episodeId)
_dbContext.Episodes {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Episodes
.AsNoTracking() .AsNoTracking()
.Include(e => e.Season) .Include(e => e.Season)
.Include(e => e.EpisodeMetadata) .Include(e => e.EpisodeMetadata)
@ -151,14 +153,20 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.OrderBy(s => s.Id) .OrderBy(s => s.Id)
.SingleOrDefaultAsync(s => s.Id == episodeId) .SingleOrDefaultAsync(s => s.Id == episodeId)
.Map(Optional); .Map(Optional);
}
public Task<int> GetEpisodeCount(int seasonId) => public async Task<int> GetEpisodeCount(int seasonId)
_dbContext.Episodes {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Episodes
.AsNoTracking() .AsNoTracking()
.CountAsync(e => e.SeasonId == seasonId); .CountAsync(e => e.SeasonId == seasonId);
}
public Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize) => public async Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize)
_dbContext.EpisodeMetadata {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.EpisodeMetadata
.AsNoTracking() .AsNoTracking()
.Filter(em => em.Episode.SeasonId == seasonId) .Filter(em => em.Episode.SeasonId == seasonId)
.Include(em => em.Artwork) .Include(em => em.Artwork)
@ -170,10 +178,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Skip((pageNumber - 1) * pageSize) .Skip((pageNumber - 1) * pageSize)
.Take(pageSize) .Take(pageSize)
.ToListAsync(); .ToListAsync();
}
public async Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) public async Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata)
{ {
Option<int> maybeId = await _dbContext.ShowMetadata await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<int> maybeId = await dbContext.ShowMetadata
.Where(s => s.Title == metadata.Title && s.Year == metadata.Year) .Where(s => s.Title == metadata.Title && s.Year == metadata.Year)
.Where(s => s.Show.LibraryPathId == libraryPathId) .Where(s => s.Show.LibraryPathId == libraryPathId)
.SingleOrDefaultAsync() .SingleOrDefaultAsync()
@ -183,7 +193,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeId.Match( return await maybeId.Match(
id => id =>
{ {
return _dbContext.Shows return dbContext.Shows
.AsNoTracking()
.Include(s => s.ShowMetadata) .Include(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
.Include(s => s.ShowMetadata) .Include(s => s.ShowMetadata)
@ -199,6 +210,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Either<BaseError, Show>> AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) public async Task<Either<BaseError, Show>> AddShow(int libraryPathId, string showFolder, ShowMetadata metadata)
{ {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
try try
{ {
metadata.DateAdded = DateTime.UtcNow; metadata.DateAdded = DateTime.UtcNow;
@ -211,8 +224,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
Seasons = new List<Season>() Seasons = new List<Season>()
}; };
await _dbContext.Shows.AddAsync(show); await dbContext.Shows.AddAsync(show);
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return show; return show;
} }
@ -224,14 +237,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public async Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber) public async Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber)
{ {
Option<Season> maybeExisting = await _dbContext.Seasons await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<Season> maybeExisting = await dbContext.Seasons
.Include(s => s.SeasonMetadata) .Include(s => s.SeasonMetadata)
.ThenInclude(sm => sm.Artwork) .ThenInclude(sm => sm.Artwork)
.OrderBy(s => s.ShowId)
.ThenBy(s => s.SeasonNumber)
.SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber); .SingleOrDefaultAsync(s => s.ShowId == show.Id && s.SeasonNumber == seasonNumber);
return await maybeExisting.Match( return await maybeExisting.Match(
season => Right<BaseError, Season>(season).AsTask(), season => Right<BaseError, Season>(season).AsTask(),
() => AddSeason(show, libraryPathId, seasonNumber)); () => AddSeason(dbContext, show, libraryPathId, seasonNumber));
} }
public async Task<Either<BaseError, Episode>> GetOrAddEpisode( public async Task<Either<BaseError, Episode>> GetOrAddEpisode(
@ -239,7 +255,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
LibraryPath libraryPath, LibraryPath libraryPath,
string path) string path)
{ {
Option<Episode> maybeExisting = await _dbContext.Episodes await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<Episode> maybeExisting = await dbContext.Episodes
.Include(i => i.EpisodeMetadata) .Include(i => i.EpisodeMetadata)
.ThenInclude(em => em.Artwork) .ThenInclude(em => em.Artwork)
.Include(i => i.MediaVersions) .Include(i => i.MediaVersions)
@ -249,7 +266,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeExisting.Match( return await maybeExisting.Match(
episode => Right<BaseError, Episode>(episode).AsTask(), episode => Right<BaseError, Episode>(episode).AsTask(),
() => AddEpisode(season, libraryPath.Id, path)); () => AddEpisode(dbContext, season, libraryPath.Id, path));
} }
public Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath) => public Task<IEnumerable<string>> FindEpisodePaths(LibraryPath libraryPath) =>
@ -273,47 +290,46 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path", WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
new { LibraryPathId = libraryPath.Id, Path = path }); new { LibraryPathId = libraryPath.Id, Path = path });
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
foreach (int episodeId in ids) foreach (int episodeId in ids)
{ {
Episode episode = await _dbContext.Episodes.FindAsync(episodeId); Episode episode = await dbContext.Episodes.FindAsync(episodeId);
_dbContext.Episodes.Remove(episode); dbContext.Episodes.Remove(episode);
} }
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return Unit.Default; return Unit.Default;
} }
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) => public async Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath)
_dbContext.Seasons {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
List<Season> seasons = await dbContext.Seasons
.Filter(s => s.LibraryPathId == libraryPath.Id) .Filter(s => s.LibraryPathId == libraryPath.Id)
.Filter(s => s.Episodes.Count == 0) .Filter(s => s.Episodes.Count == 0)
.ToListAsync() .ToListAsync();
.Bind( dbContext.Seasons.RemoveRange(seasons);
list => await dbContext.SaveChangesAsync();
{ return Unit.Default;
_dbContext.Seasons.RemoveRange(list); }
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public Task<Unit> DeleteEmptyShows(LibraryPath libraryPath) => public async Task<Unit> DeleteEmptyShows(LibraryPath libraryPath)
_dbContext.Shows {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
List<Show> shows = await dbContext.Shows
.Filter(s => s.LibraryPathId == libraryPath.Id) .Filter(s => s.LibraryPathId == libraryPath.Id)
.Filter(s => s.Seasons.Count == 0) .Filter(s => s.Seasons.Count == 0)
.ToListAsync() .ToListAsync();
.Bind( dbContext.Shows.RemoveRange(shows);
list => await dbContext.SaveChangesAsync();
{ return Unit.Default;
_dbContext.Shows.RemoveRange(list); }
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public async Task<Either<BaseError, PlexShow>> GetOrAddPlexShow(PlexLibrary library, PlexShow item) public async Task<Either<BaseError, PlexShow>> GetOrAddPlexShow(PlexLibrary library, PlexShow item)
{ {
await using TvContext context = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<PlexShow> maybeExisting = await context.PlexShows Option<PlexShow> maybeExisting = await dbContext.PlexShows
.AsNoTracking() .AsNoTracking()
.Include(i => i.ShowMetadata) .Include(i => i.ShowMetadata)
.ThenInclude(mm => mm.Genres) .ThenInclude(mm => mm.Genres)
@ -324,13 +340,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeExisting.Match( return await maybeExisting.Match(
plexShow => Right<BaseError, PlexShow>(plexShow).AsTask(), plexShow => Right<BaseError, PlexShow>(plexShow).AsTask(),
async () => await AddPlexShow(context, library, item)); async () => await AddPlexShow(dbContext, library, item));
} }
public async Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) public async Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item)
{ {
await using TvContext context = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<PlexSeason> maybeExisting = await context.PlexSeasons Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
.AsNoTracking() .AsNoTracking()
.Include(i => i.SeasonMetadata) .Include(i => i.SeasonMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
@ -339,13 +355,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeExisting.Match( return await maybeExisting.Match(
plexSeason => Right<BaseError, PlexSeason>(plexSeason).AsTask(), plexSeason => Right<BaseError, PlexSeason>(plexSeason).AsTask(),
async () => await AddPlexSeason(context, library, item)); async () => await AddPlexSeason(dbContext, library, item));
} }
public async Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) public async Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item)
{ {
await using TvContext context = _dbContextFactory.CreateDbContext(); await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<PlexEpisode> maybeExisting = await context.PlexEpisodes Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
.AsNoTracking() .AsNoTracking()
.Include(i => i.EpisodeMetadata) .Include(i => i.EpisodeMetadata)
.ThenInclude(mm => mm.Artwork) .ThenInclude(mm => mm.Artwork)
@ -356,7 +372,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
return await maybeExisting.Match( return await maybeExisting.Match(
plexEpisode => Right<BaseError, PlexEpisode>(plexEpisode).AsTask(), plexEpisode => Right<BaseError, PlexEpisode>(plexEpisode).AsTask(),
async () => await AddPlexEpisode(context, library, item)); async () => await AddPlexEpisode(dbContext, library, item));
} }
public Task<Unit> AddGenre(ShowMetadata metadata, Genre genre) => public Task<Unit> AddGenre(ShowMetadata metadata, Genre genre) =>
@ -372,7 +388,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId INNER JOIN LibraryPath lp ON lp.Id = m.LibraryPathId
WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys)", WHERE lp.LibraryId = @LibraryId AND ps.Key not in @Keys)",
new { LibraryId = library.Id, Keys = showKeys }).ToUnit(); new { LibraryId = library.Id, Keys = showKeys }).ToUnit();
public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) => public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) =>
_dbConnection.ExecuteAsync( _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN @"DELETE FROM MediaItem WHERE Id IN
@ -382,7 +398,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
INNER JOIN PlexShow P on P.Id = s.ShowId INNER JOIN PlexShow P on P.Id = s.ShowId
WHERE P.Key = @ShowKey AND ps.Key not in @Keys)", WHERE P.Key = @ShowKey AND ps.Key not in @Keys)",
new { ShowKey = showKey, Keys = seasonKeys }).ToUnit(); new { ShowKey = showKey, Keys = seasonKeys }).ToUnit();
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) => public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
_dbConnection.ExecuteAsync( _dbConnection.ExecuteAsync(
@"DELETE FROM MediaItem WHERE Id IN @"DELETE FROM MediaItem WHERE Id IN
@ -393,6 +409,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)", WHERE P.Key = @SeasonKey AND pe.Key not in @Keys)",
new { SeasonKey = seasonKey, Keys = episodeKeys }).ToUnit(); new { SeasonKey = seasonKey, Keys = episodeKeys }).ToUnit();
public async Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber)
{
episode.EpisodeNumber = episodeNumber;
await _dbConnection.ExecuteAsync(
@"UPDATE Episode SET EpisodeNumber = @EpisodeNumber WHERE Id = @Id",
new { EpisodeNumber = episodeNumber, Id = episode.Id });
return Unit.Default;
}
public async Task<List<Episode>> GetShowItems(int showId) public async Task<List<Episode>> GetShowItems(int showId)
{ {
IEnumerable<int> ids = await _dbConnection.QueryAsync<int>( IEnumerable<int> ids = await _dbConnection.QueryAsync<int>(
@ -402,7 +427,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
WHERE Show.Id = @ShowId", WHERE Show.Id = @ShowId",
new { ShowId = showId }); new { ShowId = showId });
return await _dbContext.Episodes await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Episodes
.AsNoTracking()
.Include(e => e.EpisodeMetadata) .Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions) .Include(e => e.MediaVersions)
.Include(e => e.Season) .Include(e => e.Season)
@ -410,15 +437,23 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.ToListAsync(); .ToListAsync();
} }
public Task<List<Episode>> GetSeasonItems(int seasonId) => public async Task<List<Episode>> GetSeasonItems(int seasonId)
_dbContext.Episodes {
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Episodes
.AsNoTracking()
.Include(e => e.EpisodeMetadata) .Include(e => e.EpisodeMetadata)
.Include(e => e.MediaVersions) .Include(e => e.MediaVersions)
.Include(e => e.Season) .Include(e => e.Season)
.Filter(e => e.SeasonId == seasonId) .Filter(e => e.SeasonId == seasonId)
.ToListAsync(); .ToListAsync();
}
private async Task<Either<BaseError, Season>> AddSeason(Show show, int libraryPathId, int seasonNumber) private static async Task<Either<BaseError, Season>> AddSeason(
TvContext dbContext,
Show show,
int libraryPathId,
int seasonNumber)
{ {
try try
{ {
@ -428,10 +463,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
ShowId = show.Id, ShowId = show.Id,
SeasonNumber = seasonNumber, SeasonNumber = seasonNumber,
Episodes = new List<Episode>(), Episodes = new List<Episode>(),
SeasonMetadata = new List<SeasonMetadata>() SeasonMetadata = new List<SeasonMetadata>
{
new()
}
}; };
await _dbContext.Seasons.AddAsync(season); await dbContext.Seasons.AddAsync(season);
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return season; return season;
} }
catch (Exception ex) catch (Exception ex)
@ -440,7 +478,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
private async Task<Either<BaseError, Episode>> AddEpisode(Season season, int libraryPathId, string path) private static async Task<Either<BaseError, Episode>> AddEpisode(
TvContext dbContext,
Season season,
int libraryPathId,
string path)
{ {
try try
{ {
@ -448,7 +490,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
LibraryPathId = libraryPathId, LibraryPathId = libraryPathId,
SeasonId = season.Id, SeasonId = season.Id,
EpisodeMetadata = new List<EpisodeMetadata>(), EpisodeMetadata = new List<EpisodeMetadata>
{
new()
{
DateUpdated = DateTime.MinValue,
MetadataKind = MetadataKind.Fallback
}
},
MediaVersions = new List<MediaVersion> MediaVersions = new List<MediaVersion>
{ {
new() new()
@ -460,8 +509,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
}; };
await _dbContext.Episodes.AddAsync(episode); await dbContext.Episodes.AddAsync(episode);
await _dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
return episode; return episode;
} }
catch (Exception ex) catch (Exception ex)
@ -470,8 +519,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
private async Task<Either<BaseError, PlexShow>> AddPlexShow( private static async Task<Either<BaseError, PlexShow>> AddPlexShow(
TvContext context, TvContext dbContext,
PlexLibrary library, PlexLibrary library,
PlexShow item) PlexShow item)
{ {
@ -479,9 +528,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
item.LibraryPathId = library.Paths.Head().Id; item.LibraryPathId = library.Paths.Head().Id;
await context.PlexShows.AddAsync(item); await dbContext.PlexShows.AddAsync(item);
await context.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await context.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
return item; return item;
} }
catch (Exception ex) catch (Exception ex)
@ -490,8 +539,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
private async Task<Either<BaseError, PlexSeason>> AddPlexSeason( private static async Task<Either<BaseError, PlexSeason>> AddPlexSeason(
TvContext context, TvContext dbContext,
PlexLibrary library, PlexLibrary library,
PlexSeason item) PlexSeason item)
{ {
@ -499,9 +548,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
item.LibraryPathId = library.Paths.Head().Id; item.LibraryPathId = library.Paths.Head().Id;
await context.PlexSeasons.AddAsync(item); await dbContext.PlexSeasons.AddAsync(item);
await context.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await context.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
return item; return item;
} }
catch (Exception ex) catch (Exception ex)
@ -510,8 +559,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
private async Task<Either<BaseError, PlexEpisode>> AddPlexEpisode( private static async Task<Either<BaseError, PlexEpisode>> AddPlexEpisode(
TvContext context, TvContext dbContext,
PlexLibrary library, PlexLibrary library,
PlexEpisode item) PlexEpisode item)
{ {
@ -519,9 +568,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{ {
item.LibraryPathId = library.Paths.Head().Id; item.LibraryPathId = library.Paths.Head().Id;
await context.PlexEpisodes.AddAsync(item); await dbContext.PlexEpisodes.AddAsync(item);
await context.SaveChangesAsync(); await dbContext.SaveChangesAsync();
await context.Entry(item).Reference(i => i.LibraryPath).LoadAsync(); await dbContext.Entry(item).Reference(i => i.LibraryPath).LoadAsync();
return item; return item;
} }
catch (Exception ex) catch (Exception ex)

Loading…
Cancel
Save