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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user