rework television media (#26)

* rework television media

* refactor poster saving

* television and movie views are working again

* remove dead code

* use paper styling for all cards

* add show poster, plot to seasons page

* remove missing shows; cleanup interfaces

* fix split show display (same show in different folders/sources)

* add placeholder "add to schedule" button

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

* stop releasing CLI for now

* use season number as season placeholder

* add television shows to collections

* add television seasons to collections

* add television episodes to collections

* add movies to collections

* remove movies, shows, seasons, episodes from collections

* fix page width and menus

* fix buffer size defaults

* fix chronological episode ordering

* allow deleting media collections

* don't get stuck building a playout with an empty collection

* schedule editing and playouts work again

* minor cleanup

* remove dead code

* fix bugs with viewing movies as they are loading

* add scanner tests; support nested movie folders

* update collections docs

* rearrange order of schedule items

* add show and season to schedule

* delete schedules that use legacy collections, reset all posters

* move cleanup to new migration

* load fallback metadata when nfo fails; don't require metadata in ui

* update readme and screenshots
This commit is contained in:
Jason Dove
2021-02-22 00:54:41 +00:00
committed by GitHub
parent 98cf922b3c
commit 871a031467
259 changed files with 13493 additions and 4771 deletions
@@ -38,6 +38,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(c => c.Playouts)
.ThenInclude(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as TelevisionEpisodeMediaItem).Metadata)
.Include(c => c.Playouts)
.ThenInclude(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as MovieMediaItem).Metadata)
.ToListAsync();
public async Task Update(Channel channel)
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@@ -28,17 +27,42 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
_dbContext.MediaCollections.SingleOrDefaultAsync(c => c.Id == id).Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollection(int id) =>
Get(id).Map(c => c.OfType<SimpleMediaCollection>().HeadOrNone());
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Items)
.ThenInclude(i => i.Source)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<TelevisionMediaCollection>> GetTelevisionMediaCollection(int id) =>
Get(id).Map(c => c.OfType<TelevisionMediaCollection>().HeadOrNone());
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItems(int id) =>
_dbContext.SimpleMediaCollections
.Include(s => s.Movies)
.ThenInclude(m => m.Source)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<Option<SimpleMediaCollection>> GetSimpleMediaCollectionWithItemsUntracked(int id) =>
_dbContext.SimpleMediaCollections
.AsNoTracking()
.Include(s => s.Movies)
.ThenInclude(i => i.Source)
.Include(s => s.Movies)
.ThenInclude(m => m.Metadata)
.Include(s => s.TelevisionShows)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionSeasons)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(s => s.Metadata)
.Include(s => s.TelevisionEpisodes)
.ThenInclude(e => e.Season)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(c => c.Id == id)
.Map(Optional);
public Task<List<SimpleMediaCollection>> GetSimpleMediaCollections() =>
_dbContext.SimpleMediaCollections.ToListAsync();
@@ -46,75 +70,23 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<MediaCollection>> GetAll() =>
_dbContext.MediaCollections.ToListAsync();
public Task<List<MediaCollectionSummary>> GetSummaries(string searchString) =>
_dbContext.MediaCollectionSummaries.FromSqlRaw(
@"SELECT mc.Id, mc.Name, Count(mismc.ItemsId) AS ItemCount, true AS IsSimple
FROM MediaCollections mc
INNER JOIN SimpleMediaCollections smc ON smc.Id = mc.Id
LEFT OUTER JOIN MediaItemSimpleMediaCollection mismc ON mismc.SimpleMediaCollectionsId = mc.Id
WHERE mc.Name LIKE {0}
GROUP BY mc.Id, mc.Name
UNION ALL
SELECT mc.Id, mc.Name, Count(mi.Id) AS ItemCount, false AS IsSimple
FROM MediaCollections mc
INNER JOIN TelevisionMediaCollections tmc ON tmc.Id = mc.Id
LEFT OUTER JOIN MediaItems mi ON (tmc.SeasonNumber IS NULL OR mi.Metadata_SeasonNumber = tmc.SeasonNumber)
AND mi.Metadata_Title = tmc.ShowTitle
WHERE mc.Name LIKE {0}
GROUP BY mc.Id, mc.Name",
$"%{searchString}%").ToListAsync();
public Task<Option<List<MediaItem>>> GetItems(int id) =>
Get(id).MapT(
collection => collection switch
{
SimpleMediaCollection s => SimpleItems(s),
TelevisionMediaCollection t => TelevisionItems(t),
_ => throw new NotSupportedException($"Unsupported collection type {collection.GetType().Name}")
}).Bind(x => x.Sequence());
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) =>
GetSimpleMediaCollection(id).MapT(SimpleItems).Bind(x => x.Sequence());
public Task<Option<List<MediaItem>>> GetTelevisionMediaCollectionItems(int id) =>
GetTelevisionMediaCollection(id).MapT(TelevisionItems).Bind(x => x.Sequence());
public Task Update(SimpleMediaCollection collection)
{
_dbContext.SimpleMediaCollections.Update(collection);
return _dbContext.SaveChangesAsync();
}
public async Task<bool> InsertOrIgnore(TelevisionMediaCollection collection)
{
if (!_dbContext.TelevisionMediaCollections.Any(
existing => existing.ShowTitle == collection.ShowTitle &&
existing.SeasonNumber == collection.SeasonNumber))
{
await _dbContext.TelevisionMediaCollections.AddAsync(collection);
return await _dbContext.SaveChangesAsync() > 0;
}
// no change
return false;
}
public Task<Unit> ReplaceItems(int collectionId, List<MediaItem> mediaItems) =>
GetSimpleMediaCollection(collectionId).IfSomeAsync(
async c =>
{
await SimpleItems(c);
c.Items.Clear();
foreach (MediaItem mediaItem in mediaItems)
{
c.Items.Add(mediaItem);
}
_dbContext.SimpleMediaCollections.Update(c);
await _dbContext.SaveChangesAsync();
});
public async Task Delete(int mediaCollectionId)
{
MediaCollection mediaCollection = await _dbContext.MediaCollections.FindAsync(mediaCollectionId);
@@ -122,33 +94,77 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await _dbContext.SaveChangesAsync();
}
public async Task DeleteEmptyTelevisionCollections()
{
List<int> ids = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"SELECT mc.Id FROM MediaCollections mc
INNER JOIN TelevisionMediaCollections t on mc.Id = t.Id
WHERE NOT EXISTS
(SELECT 1 FROM MediaItems mi WHERE t.ShowTitle = mi.Metadata_Title AND (t.SeasonNumber IS NULL OR t.SeasonNumber = mi.Metadata_SeasonNumber))")
.Map(i => i.Id)
.ToListAsync();
List<MediaCollection> toDelete =
await _dbContext.MediaCollections.Where(mc => ids.Contains(mc.Id)).ToListAsync();
_dbContext.MediaCollections.RemoveRange(toDelete);
await _dbContext.SaveChangesAsync();
}
private async Task<List<MediaItem>> SimpleItems(SimpleMediaCollection collection)
{
await _dbContext.Entry(collection).Collection(c => c.Items).LoadAsync();
return collection.Items.ToList();
var result = new List<MediaItem>();
await _dbContext.Entry(collection).Collection(c => c.Movies).LoadAsync();
result.AddRange(collection.Movies);
result.AddRange(await GetTelevisionShowItems(collection));
result.AddRange(await GetTelevisionSeasonItems(collection));
result.AddRange(await GetTelevisionEpisodeItems(collection));
return result.Distinct().ToList();
}
private Task<List<MediaItem>> TelevisionItems(TelevisionMediaCollection collection) =>
_dbContext.MediaItems
.Filter(c => c.Metadata.Title == collection.ShowTitle)
.Filter(c => collection.SeasonNumber == null || c.Metadata.SeasonNumber == collection.SeasonNumber)
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionShowItems(SimpleMediaCollection collection)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
inner join SimpleMediaCollectionShows s on s.TelevisionShowsId = ts.Id
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => showItemIds.Contains(mi.Id))
.ToListAsync();
}
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionSeasonItems(SimpleMediaCollection collection)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join SimpleMediaCollectionSeasons s on s.TelevisionSeasonsId = tsn.Id
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => seasonItemIds.Contains(mi.Id))
.ToListAsync();
}
private async Task<List<TelevisionEpisodeMediaItem>> GetTelevisionEpisodeItems(SimpleMediaCollection collection)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> episodeItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select s.TelevisionEpisodesId as Id
from SimpleMediaCollectionEpisodes s
where s.SimpleMediaCollectionsId = {0}",
collection.Id)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => episodeItemIds.Contains(mi.Id))
.ToListAsync();
}
}
}
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
@@ -16,13 +15,6 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public MediaItemRepository(TvContext dbContext) => _dbContext = dbContext;
public async Task<int> Add(MediaItem mediaItem)
{
await _dbContext.MediaItems.AddAsync(mediaItem);
await _dbContext.SaveChangesAsync();
return mediaItem.Id;
}
public Task<Option<MediaItem>> Get(int id) =>
_dbContext.MediaItems
.Include(i => i.Source)
@@ -33,81 +25,29 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<MediaItem>> Search(string searchString)
{
IQueryable<MediaItem> data = from c in _dbContext.MediaItems.Include(c => c.Source) select c;
IQueryable<TelevisionEpisodeMediaItem> episodeData =
from c in _dbContext.TelevisionEpisodeMediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(searchString))
{
data = data.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
episodeData = episodeData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
}
return data.ToListAsync();
IQueryable<MovieMediaItem> movieData =
from c in _dbContext.MovieMediaItems.Include(c => c.Source) select c;
if (!string.IsNullOrEmpty(searchString))
{
movieData = movieData.Where(c => EF.Functions.Like(c.Metadata.Title, $"%{searchString}%"));
}
return episodeData.OfType<MediaItem>().Concat(movieData.OfType<MediaItem>()).ToListAsync();
}
public Task<List<MediaItemSummary>> GetPageByType(MediaType mediaType, int pageNumber, int pageSize) =>
mediaType switch
{
MediaType.Movie => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT
Id AS MediaItemId,
Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle,
substr(Metadata_Aired, 1, 4) AS Subtitle,
Poster
FROM MediaItems WHERE Metadata_MediaType=2
ORDER BY Metadata_SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.ToListAsync(),
MediaType.TvShow => _dbContext.MediaItemSummaries.FromSqlRaw(
@"SELECT
min(Id) AS MediaItemId,
Metadata_Title AS Title,
Metadata_SortTitle AS SortTitle,
count(*) || ' Episodes' AS Subtitle,
max(Poster) AS Poster
FROM MediaItems WHERE Metadata_MediaType=1
GROUP BY Metadata_Title, Metadata_SortTitle
ORDER BY Metadata_SortTitle
LIMIT {0} OFFSET {1}",
pageSize,
(pageNumber - 1) * pageSize)
.AsNoTracking()
.ToListAsync(),
_ => Task.FromResult(new List<MediaItemSummary>())
};
public Task<int> GetCountByType(MediaType mediaType) =>
mediaType switch
{
MediaType.Movie => _dbContext.MediaItems
.Filter(i => i.Metadata.MediaType == mediaType)
.CountAsync(),
MediaType.TvShow => _dbContext.MediaItems
.Filter(i => i.Metadata.MediaType == mediaType)
.GroupBy(i => new { i.Metadata.Title, i.Metadata.SortTitle })
.CountAsync(),
_ => Task.FromResult(0)
};
public Task<List<MediaItem>> GetAllByMediaSourceId(int mediaSourceId) =>
_dbContext.MediaItems
.Filter(i => i.MediaSourceId == mediaSourceId)
.ToListAsync();
public async Task<bool> Update(MediaItem mediaItem)
{
_dbContext.MediaItems.Update(mediaItem);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task Delete(int mediaItemId)
{
MediaItem mediaItem = await _dbContext.MediaItems.FindAsync(mediaItemId);
_dbContext.MediaItems.Remove(mediaItem);
await _dbContext.SaveChangesAsync();
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class MovieRepository : IMovieRepository
{
private readonly TvContext _dbContext;
public MovieRepository(TvContext dbContext) => _dbContext = dbContext;
public Task<Option<MovieMediaItem>> GetMovie(int movieId) =>
_dbContext.MovieMediaItems
.Include(m => m.Metadata)
.SingleOrDefaultAsync(m => m.Id == movieId)
.Map(Optional);
public async Task<Either<BaseError, MovieMediaItem>> GetOrAdd(int mediaSourceId, string path)
{
Option<MovieMediaItem> maybeExisting = await _dbContext.MovieMediaItems
.Include(i => i.Metadata)
.SingleOrDefaultAsync(i => i.Path == path);
return await maybeExisting.Match(
mediaItem => Right<BaseError, MovieMediaItem>(mediaItem).AsTask(),
async () => await AddMovie(mediaSourceId, path));
}
public async Task<bool> Update(MovieMediaItem movie)
{
_dbContext.MovieMediaItems.Update(movie);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<int> GetMovieCount() =>
_dbContext.MovieMediaItems
.AsNoTracking()
.CountAsync();
public Task<List<MovieMediaItem>> GetPagedMovies(int pageNumber, int pageSize) =>
_dbContext.MovieMediaItems
.Include(s => s.Metadata)
.OrderBy(s => s.Metadata.SortTitle)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
private async Task<Either<BaseError, MovieMediaItem>> AddMovie(int mediaSourceId, string path)
{
try
{
var movie = new MovieMediaItem { MediaSourceId = mediaSourceId, Path = path };
await _dbContext.MovieMediaItems.AddAsync(movie);
await _dbContext.SaveChangesAsync();
return movie;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}
@@ -35,6 +35,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaCollection)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TelevisionShow)
.OrderBy(p => p.Id) // https://github.com/dotnet/efcore/issues/22579#issuecomment-694772289
.SingleOrDefaultAsync(p => p.Id == id);
@@ -55,7 +58,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<List<PlayoutItem>> GetPlayoutItems(int playoutId) =>
_dbContext.PlayoutItems
.Include(i => i.MediaItem)
.ThenInclude(mi => mi.Metadata)
.ThenInclude(m => (m as MovieMediaItem).Metadata)
.Include(i => i.MediaItem)
.ThenInclude(m => (m as TelevisionEpisodeMediaItem).Metadata)
.Filter(i => i.PlayoutId == playoutId)
.ToListAsync();
@@ -57,7 +57,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
{
await _dbContext.Entry(programSchedule).Collection(s => s.Items).LoadAsync();
await _dbContext.Entry(programSchedule).Collection(s => s.Items).Query()
.Include(i => i.MediaCollection).LoadAsync();
.Include(i => i.MediaCollection)
.Include(i => i.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Include(i => i.TelevisionSeason)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.LoadAsync();
return programSchedule.Items;
}).Sequence();
}
@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Infrastructure.Data.Repositories
{
public class TelevisionRepository : ITelevisionRepository
{
private readonly TvContext _dbContext;
public TelevisionRepository(TvContext dbContext) => _dbContext = dbContext;
public async Task<bool> Update(TelevisionShow show)
{
_dbContext.TelevisionShows.Update(show);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Update(TelevisionSeason season)
{
_dbContext.TelevisionSeasons.Update(season);
return await _dbContext.SaveChangesAsync() > 0;
}
public async Task<bool> Update(TelevisionEpisodeMediaItem episode)
{
_dbContext.TelevisionEpisodeMediaItems.Update(episode);
return await _dbContext.SaveChangesAsync() > 0;
}
public Task<List<TelevisionShow>> GetAllShows() =>
_dbContext.TelevisionShows
.AsNoTracking()
.Include(s => s.Metadata)
.ToListAsync();
public Task<Option<TelevisionShow>> GetShow(int televisionShowId) =>
_dbContext.TelevisionShows
.AsNoTracking()
.Filter(s => s.Id == televisionShowId)
.Include(s => s.Metadata)
.SingleOrDefaultAsync()
.Map(Optional);
public Task<int> GetShowCount() =>
_dbContext.TelevisionShows
.AsNoTracking()
.CountAsync();
public Task<List<TelevisionShow>> GetPagedShows(int pageNumber, int pageSize) =>
_dbContext.TelevisionShows
.AsNoTracking()
.Include(s => s.Metadata)
.OrderBy(s => s.Metadata == null ? string.Empty : s.Metadata.SortTitle)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public Task<List<TelevisionSeason>> GetAllSeasons() =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.ToListAsync();
public Task<Option<TelevisionSeason>> GetSeason(int televisionSeasonId) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.SingleOrDefaultAsync(s => s.Id == televisionSeasonId)
.Map(Optional);
public Task<int> GetSeasonCount(int televisionShowId) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Where(s => s.TelevisionShowId == televisionShowId)
.CountAsync();
public Task<List<TelevisionSeason>> GetPagedSeasons(int televisionShowId, int pageNumber, int pageSize) =>
_dbContext.TelevisionSeasons
.AsNoTracking()
.Where(s => s.TelevisionShowId == televisionShowId)
.Include(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.OrderBy(s => s.Number)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public Task<Option<TelevisionEpisodeMediaItem>> GetEpisode(int televisionEpisodeId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(s => s.Season)
.Include(s => s.Metadata)
.SingleOrDefaultAsync(s => s.Id == televisionEpisodeId)
.Map(Optional);
public Task<int> GetEpisodeCount(int televisionSeasonId) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Where(e => e.SeasonId == televisionSeasonId)
.CountAsync();
public Task<List<TelevisionEpisodeMediaItem>> GetPagedEpisodes(
int televisionSeasonId,
int pageNumber,
int pageSize) =>
_dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Include(e => e.Season)
.ThenInclude(s => s.TelevisionShow)
.ThenInclude(s => s.Metadata)
.Where(e => e.SeasonId == televisionSeasonId)
.OrderBy(s => s.Metadata.Episode)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public async Task<Option<TelevisionShow>> GetShowByPath(int mediaSourceId, string path)
{
Option<int> maybeShowId = await _dbContext.LocalTelevisionShowSources
.SingleOrDefaultAsync(s => s.MediaSourceId == mediaSourceId && s.Path == path)
.Map(Optional)
.MapT(s => s.TelevisionShowId);
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(
TelevisionShow show,
string path,
int seasonNumber)
{
Option<TelevisionSeason> maybeExisting = await _dbContext.TelevisionSeasons
.SingleOrDefaultAsync(i => i.Path == path);
return await maybeExisting.Match(
season => Right<BaseError, TelevisionSeason>(season).AsTask(),
() => AddSeason(show, path, seasonNumber));
}
public async Task<Either<BaseError, TelevisionEpisodeMediaItem>> GetOrAddEpisode(
TelevisionSeason season,
int mediaSourceId,
string path)
{
Option<TelevisionEpisodeMediaItem> maybeExisting = await _dbContext.TelevisionEpisodeMediaItems
.Include(i => i.Metadata)
.Include(i => i.Statistics)
.SingleOrDefaultAsync(i => i.Path == path);
return await maybeExisting.Match(
episode => Right<BaseError, TelevisionEpisodeMediaItem>(episode).AsTask(),
() => AddEpisode(season, mediaSourceId, path));
}
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
.Where(s => s.Sources.Count == 0)
.ToListAsync()
.Bind(
list =>
{
_dbContext.TelevisionShows.RemoveRange(list);
return _dbContext.SaveChangesAsync();
})
.ToUnit();
public async Task<List<TelevisionEpisodeMediaItem>> GetShowItems(int televisionShowId)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId
where ts.Id = {0}",
televisionShowId)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => showItemIds.Contains(mi.Id))
.ToListAsync();
}
public async Task<List<TelevisionEpisodeMediaItem>> GetSeasonItems(int televisionSeasonId)
{
// TODO: would be nice to get the media items in one go, but ef...
List<int> seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw(
@"select tmi.Id
from TelevisionEpisodes tmi
inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId
where tsn.Id = {0}",
televisionSeasonId)
.Select(i => i.Id)
.ToListAsync();
return await _dbContext.TelevisionEpisodeMediaItems
.AsNoTracking()
.Include(e => e.Metadata)
.Where(mi => seasonItemIds.Contains(mi.Id))
.ToListAsync();
}
private async Task<Either<BaseError, TelevisionSeason>> AddSeason(
TelevisionShow show,
string path,
int seasonNumber)
{
try
{
var season = new TelevisionSeason
{
TelevisionShowId = show.Id, Path = path, Number = seasonNumber,
Episodes = new List<TelevisionEpisodeMediaItem>()
};
await _dbContext.TelevisionSeasons.AddAsync(season);
await _dbContext.SaveChangesAsync();
return season;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
private async Task<Either<BaseError, TelevisionEpisodeMediaItem>> AddEpisode(
TelevisionSeason season,
int mediaSourceId,
string path)
{
try
{
var episode = new TelevisionEpisodeMediaItem
{
MediaSourceId = mediaSourceId,
SeasonId = season.Id,
Path = path
};
await _dbContext.TelevisionEpisodeMediaItems.AddAsync(episode);
await _dbContext.SaveChangesAsync();
return episode;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}