Browse Source

Rebuild playouts with modified collections (#39)

* rebuild playouts when items are removed from collections

* rebuild playouts when items are added to collections

* simplify logic
pull/1213/head
Jason Dove 5 years ago committed by GitHub
parent
commit
187f24ca6c
  1. 23
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs
  2. 26
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs
  3. 26
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs
  4. 25
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs
  5. 26
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs
  6. 14
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  7. 5
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  8. 11
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  9. 2
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  10. 22
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  11. 2
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  12. 5
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  13. 8
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  14. 5
      ErsatzTV/Extensions/HostExtensions.cs
  15. 1
      ErsatzTV/Pages/CollectionItems.razor
  16. 1
      ErsatzTV/Pages/MovieList.razor
  17. 1
      ErsatzTV/Pages/Search.razor
  18. 5
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  19. 5
      ErsatzTV/Pages/TelevisionSeasonList.razor
  20. 1
      ErsatzTV/Pages/TelevisionShowList.razor
  21. 2
      ErsatzTV/Shared/MainLayout.razor
  22. 2
      ErsatzTV/Shared/MediaCard.razor
  23. 5
      ErsatzTV/wwwroot/browserconfig.xml
  24. 41
      ErsatzTV/wwwroot/css/site.css

23
ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
@ -9,15 +11,18 @@ namespace ErsatzTV.Application.MediaCollections.Commands
public class public class
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>> AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public AddEpisodeToCollectionHandler( public AddEpisodeToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository) ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
@ -27,8 +32,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request)) .MapT(_ => ApplyAddTelevisionEpisodeRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request) => private async Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request)
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId); {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId))
{
// rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) => private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) =>
(await CollectionMustExist(request), await ValidateEpisode(request)) (await CollectionMustExist(request), await ValidateEpisode(request))

26
ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -7,17 +9,21 @@ using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>> public class
AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
public AddMovieToCollectionHandler( public AddMovieToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository) IMovieRepository movieRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository; _movieRepository = movieRepository;
_channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
@ -27,8 +33,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.MapT(_ => ApplyAddMoviesRequest(request)) .MapT(_ => ApplyAddMoviesRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request) => private async Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request)
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId); {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId))
{
// rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) => private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) =>
(await CollectionMustExist(request), await ValidateMovies(request)) (await CollectionMustExist(request), await ValidateMovies(request))

26
ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -7,17 +9,21 @@ using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>> public class
AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public AddSeasonToCollectionHandler( public AddSeasonToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository) ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
@ -27,8 +33,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.MapT(_ => ApplyAddTelevisionSeasonRequest(request)) .MapT(_ => ApplyAddTelevisionSeasonRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request) => private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request)
await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId); {
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId))
{
// rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return Unit.Default;
}
private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) => private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) =>
(await CollectionMustExist(request), await ValidateSeason(request)) (await CollectionMustExist(request), await ValidateSeason(request))

25
ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs

@ -1,5 +1,7 @@
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -9,15 +11,18 @@ namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>> public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public AddShowToCollectionHandler( public AddShowToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository) ITelevisionRepository televisionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{ {
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
_channel = channel;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
@ -27,8 +32,22 @@ namespace ErsatzTV.Application.MediaCollections.Commands
.MapT(_ => ApplyAddTelevisionShowRequest(request)) .MapT(_ => ApplyAddTelevisionShowRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request) private async Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request)
=> _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId); {
var result = new Unit();
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId))
{
// rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository
.PlayoutIdsUsingCollection(request.CollectionId))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return result;
}
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) => private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) =>
(await CollectionMustExist(request), await ValidateShow(request)) (await CollectionMustExist(request), await ValidateShow(request))

26
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs

@ -1,6 +1,8 @@
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@ -11,20 +13,25 @@ namespace ErsatzTV.Application.MediaCollections.Commands
public class public class
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>> RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
public RemoveItemsFromCollectionHandler( public RemoveItemsFromCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository) => IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel)
{
_mediaCollectionRepository = mediaCollectionRepository; _mediaCollectionRepository = mediaCollectionRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
RemoveItemsFromCollection request, RemoveItemsFromCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection)) .MapT(collection => ApplyRemoveItemsRequest(request, collection))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest( private async Task<Unit> ApplyRemoveItemsRequest(
RemoveItemsFromCollection request, RemoveItemsFromCollection request,
Collection collection) Collection collection)
{ {
@ -34,9 +41,16 @@ namespace ErsatzTV.Application.MediaCollections.Commands
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); itemsToRemove.ForEach(m => collection.MediaItems.Remove(m));
return itemsToRemove.Any() if (itemsToRemove.Any() && await _mediaCollectionRepository.Update(collection))
? _mediaCollectionRepository.Update(collection).ToUnit() {
: Task.FromResult(Unit.Default); // rebuild all playouts that use this collection
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
{
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
}
}
return Unit.Default;
} }
private Task<Validation<BaseError, Collection>> Validate( private Task<Validation<BaseError, Collection>> Validate(

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

@ -14,23 +14,15 @@ namespace ErsatzTV.Core.Tests.Fakes
private readonly Map<int, List<MediaItem>> _data; private readonly Map<int, List<MediaItem>> _data;
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data; public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
public Task<Collection> Add(Collection collection) => throw new NotSupportedException(); public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
public Task<Unit> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException(); public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException(); public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException(); public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException(); public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
public Task<List<Collection>> GetAll() => throw new NotSupportedException(); public Task<List<Collection>> GetAll() => throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask(); public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
public Task Update(Collection collection) => throw new NotSupportedException();
public Task Delete(int collectionId) => throw new NotSupportedException(); public Task Delete(int collectionId) => throw new NotSupportedException();
public Task<Option<List<MediaItem>>> GetSimpleMediaCollectionItems(int id) => throw new NotSupportedException(); public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
} }
} }

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

@ -8,13 +8,14 @@ namespace ErsatzTV.Core.Interfaces.Repositories
public interface IMediaCollectionRepository public interface IMediaCollectionRepository
{ {
Task<Collection> Add(Collection collection); Task<Collection> Add(Collection collection);
Task<Unit> AddMediaItem(int collectionId, int mediaItemId); Task<bool> AddMediaItem(int collectionId, int mediaItemId);
Task<Option<Collection>> Get(int id); Task<Option<Collection>> Get(int id);
Task<Option<Collection>> GetCollectionWithItems(int id); Task<Option<Collection>> GetCollectionWithItems(int id);
Task<Option<Collection>> GetCollectionWithItemsUntracked(int id); Task<Option<Collection>> GetCollectionWithItemsUntracked(int id);
Task<List<Collection>> GetAll(); Task<List<Collection>> GetAll();
Task<Option<List<MediaItem>>> GetItems(int id); Task<Option<List<MediaItem>>> GetItems(int id);
Task Update(Collection collection); Task<bool> Update(Collection collection);
Task Delete(int collectionId); Task Delete(int collectionId);
Task<List<int>> PlayoutIdsUsingCollection(int collectionId);
} }
} }

11
ErsatzTV.Core/Iptv/ChannelGuide.cs

@ -62,15 +62,18 @@ namespace ErsatzTV.Core.Iptv
string title = playoutItem.MediaItem switch string title = playoutItem.MediaItem switch
{ {
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty).IfNone("[unknown movie]"), Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty).IfNone("[unknown episode]"), .IfNone("[unknown movie]"),
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
.IfNone("[unknown episode]"),
_ => "[unknown]" _ => "[unknown]"
}; };
string description = playoutItem.MediaItem switch string description = playoutItem.MediaItem switch
{ {
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty), Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty),
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty).IfNone(string.Empty), Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty)
.IfNone(string.Empty),
_ => string.Empty _ => string.Empty
}; };
@ -108,7 +111,7 @@ namespace ErsatzTV.Core.Iptv
xml.WriteAttributeString("system", "onscreen"); xml.WriteAttributeString("system", "onscreen");
xml.WriteString($"S{s:00}E{e:00}"); xml.WriteString($"S{s:00}E{e:00}");
xml.WriteEndElement(); // episode-num xml.WriteEndElement(); // episode-num
xml.WriteStartElement("episode-num"); xml.WriteStartElement("episode-num");
xml.WriteAttributeString("system", "xmltv_ns"); xml.WriteAttributeString("system", "xmltv_ns");
xml.WriteString($"{s - 1}.{e - 1}.0/1"); xml.WriteString($"{s - 1}.{e - 1}.0/1");

2
ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs

@ -34,7 +34,7 @@ namespace ErsatzTV.Core.Metadata
return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata; return fileName != null ? GetMovieMetadata(fileName, metadata) : metadata;
} }
public string GetSortTitle(string title) public string GetSortTitle(string title)
{ {
if (string.IsNullOrWhiteSpace(title)) if (string.IsNullOrWhiteSpace(title))

22
ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs

@ -30,8 +30,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
public async Task<Unit> AddMediaItem(int collectionId, int mediaItemId) public async Task<bool> AddMediaItem(int collectionId, int mediaItemId)
{ {
var modified = false;
Option<Collection> maybeCollection = await _dbContext.Collections Option<Collection> maybeCollection = await _dbContext.Collections
.Include(c => c.MediaItems) .Include(c => c.MediaItems)
.OrderBy(c => c.Id) .OrderBy(c => c.Id)
@ -52,12 +54,12 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
async mediaItem => async mediaItem =>
{ {
collection.MediaItems.Add(mediaItem); collection.MediaItems.Add(mediaItem);
await _dbContext.SaveChangesAsync(); modified = await _dbContext.SaveChangesAsync() > 0;
}); });
} }
}); });
return Unit.Default; return modified;
} }
public Task<Option<Collection>> Get(int id) => public Task<Option<Collection>> Get(int id) =>
@ -121,10 +123,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
public Task<Option<List<MediaItem>>> GetItems(int id) => public Task<Option<List<MediaItem>>> GetItems(int id) =>
Get(id).MapT(GetItemsForCollection).Bind(x => x.Sequence()); Get(id).MapT(GetItemsForCollection).Bind(x => x.Sequence());
public Task Update(Collection collection) public Task<bool> Update(Collection collection)
{ {
_dbContext.Collections.Update(collection); _dbContext.Collections.Update(collection);
return _dbContext.SaveChangesAsync(); return _dbContext.SaveChangesAsync().Map(result => result > 0);
} }
public async Task Delete(int collectionId) public async Task Delete(int collectionId)
@ -134,6 +136,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await _dbContext.SaveChangesAsync(); await _dbContext.SaveChangesAsync();
} }
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) =>
_dbConnection.QueryAsync<int>(
@"SELECT DISTINCT p.Id
FROM Playout p
INNER JOIN ProgramSchedule PS on p.ProgramScheduleId = PS.Id
INNER JOIN ProgramScheduleItem PSI on p.Anchor_NextScheduleItemId = PSI.Id
WHERE PSI.CollectionId = @CollectionId",
new { CollectionId = collectionId })
.Map(result => result.ToList());
private async Task<List<MediaItem>> GetItemsForCollection(Collection collection) private async Task<List<MediaItem>> GetItemsForCollection(Collection collection)
{ {
var result = new List<MediaItem>(); var result = new List<MediaItem>();

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

@ -173,7 +173,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
await _dbConnection.ExecuteAsync( await _dbConnection.ExecuteAsync(
"UPDATE PlexLibrary SET ShouldSyncItems = 0 WHERE Id IN @ids", "UPDATE PlexLibrary SET ShouldSyncItems = 0 WHERE Id IN @ids",
new { ids = libraryIds }); new { ids = libraryIds });
await _dbConnection.ExecuteAsync( await _dbConnection.ExecuteAsync(
"UPDATE Library SET LastScan = null WHERE Id IN @ids", "UPDATE Library SET LastScan = null WHERE Id IN @ids",
new { ids = libraryIds }); new { ids = libraryIds });

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

@ -122,7 +122,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
} }
} }
private async Task<Either<BaseError, PlexMovie>> AddPlexMovie(TvContext context, PlexLibrary library, PlexMovie item) private async Task<Either<BaseError, PlexMovie>> AddPlexMovie(
TvContext context,
PlexLibrary library,
PlexMovie item)
{ {
try try
{ {

8
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -18,10 +18,8 @@ namespace ErsatzTV.Infrastructure.Plex
{ {
private readonly IFallbackMetadataProvider _fallbackMetadataProvider; private readonly IFallbackMetadataProvider _fallbackMetadataProvider;
public PlexServerApiClient(IFallbackMetadataProvider fallbackMetadataProvider) public PlexServerApiClient(IFallbackMetadataProvider fallbackMetadataProvider) =>
{
_fallbackMetadataProvider = fallbackMetadataProvider; _fallbackMetadataProvider = fallbackMetadataProvider;
}
public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries( public async Task<Either<BaseError, List<PlexLibrary>>> GetLibraries(
PlexConnection connection, PlexConnection connection,
@ -150,7 +148,7 @@ namespace ErsatzTV.Infrastructure.Plex
} }
private static string ConvertToSAR(double aspectRatio) => "1:1"; private static string ConvertToSAR(double aspectRatio) => "1:1";
// TODO: fix this with more detailed stats pull from plex for each item // TODO: fix this with more detailed stats pull from plex for each item
// Math.Abs(aspectRatio - 1) < 0.01 ? "1:1" : $"{(int) (aspectRatio * 100)}:100"; // Math.Abs(aspectRatio - 1) < 0.01 ? "1:1" : $"{(int) (aspectRatio * 100)}:100";
} }
} }

5
ErsatzTV/Extensions/HostExtensions.cs

@ -36,7 +36,10 @@ namespace ErsatzTV.Extensions
.IfFail( .IfFail(
ex => ex =>
{ {
LogException(ex, "Error occured while migrating database; shutting down.", scope.ServiceProvider); LogException(
ex,
"Error occured while migrating database; shutting down.",
scope.ServiceProvider);
Environment.Exit(13); Environment.Exit(13);
return unit; return unit;
}); });

1
ErsatzTV/Pages/CollectionItems.razor

@ -7,6 +7,7 @@
@inject ILogger<CollectionItems> Logger @inject ILogger<CollectionItems> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<div class="mb-6" style="display: flex; flex-direction: row;"> <div class="mb-6" style="display: flex; flex-direction: row;">
<MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText> <MudText GutterBottom="true" Typo="Typo.h2">@_data.Name</MudText>

1
ErsatzTV/Pages/MovieList.razor

@ -10,6 +10,7 @@
@inject IMediator Mediator @inject IMediator Mediator
@inject IDialogService Dialog @inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px"> <MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> <MudPaper Style="align-items: center; display: flex; justify-content: center;">

1
ErsatzTV/Pages/Search.razor

@ -11,6 +11,7 @@
@inject ILogger<Search> Logger @inject ILogger<Search> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.ExtraLarge"> <MudContainer MaxWidth="MaxWidth.ExtraLarge">
<div class="mb-6" style="display: flex; flex-direction: row;"> <div class="mb-6" style="display: flex; flex-direction: row;">

5
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -7,12 +7,13 @@
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@using Unit=LanguageExt.Unit; @using Unit = LanguageExt.Unit
@inject IMediator Mediator @inject IMediator Mediator
@inject ILogger<TelevisionEpisodeList> Logger @inject ILogger<TelevisionEpisodeList> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService Dialog @inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
@ -122,7 +123,7 @@
NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }
private async Task AddEpisodeToCollection(MediaCardViewModel card) private async Task AddEpisodeToCollection(MediaCardViewModel card)
{ {
if (card is TelevisionEpisodeCardViewModel episode) if (card is TelevisionEpisodeCardViewModel episode)

5
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -7,12 +7,13 @@
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@using Unit=LanguageExt.Unit; @using Unit = LanguageExt.Unit
@inject IMediator Mediator @inject IMediator Mediator
@inject ILogger<TelevisionSeasonList> Logger @inject ILogger<TelevisionSeasonList> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService Dialog @inject IDialogService Dialog
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Large"> <MudContainer MaxWidth="MaxWidth.Large">
<MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs> <MudBreadcrumbs Items="_breadcrumbs" Class="mb-6"></MudBreadcrumbs>
@ -116,7 +117,7 @@
NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items"); NavigationManager.NavigateTo($"/schedules/{schedule.Id}/items");
} }
} }
private async Task AddSeasonToCollection(MediaCardViewModel card) private async Task AddSeasonToCollection(MediaCardViewModel card)
{ {
if (card is TelevisionSeasonCardViewModel season) if (card is TelevisionSeasonCardViewModel season)

1
ErsatzTV/Pages/TelevisionShowList.razor

@ -8,6 +8,7 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IDialogService Dialog @inject IDialogService Dialog
@inject ChannelWriter<IBackgroundServiceRequest> Channel
<MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px"> <MudContainer MaxWidth="MaxWidth.Small" Class="mb-6" Style="max-width: 300px">
<MudPaper Style="align-items: center; display: flex; justify-content: center;"> <MudPaper Style="align-items: center; display: flex; justify-content: center;">

2
ErsatzTV/Shared/MainLayout.razor

@ -67,7 +67,7 @@
private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown"; private static readonly string InfoVersion = Assembly.GetEntryAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown";
private MudTextField<string> _textField; private MudTextField<string> _textField;
private MudTheme _ersatzTvTheme private MudTheme _ersatzTvTheme
{ {
get get

2
ErsatzTV/Shared/MediaCard.razor

@ -21,7 +21,7 @@
{ {
<MudIconButton Color="Color.Tertiary" <MudIconButton Color="Color.Tertiary"
Icon="@Icons.Material.Filled.PlaylistAdd" Icon="@Icons.Material.Filled.PlaylistAdd"
Style="bottom: 0; position: absolute; left: 0;" Style="bottom: 0; left: 0; position: absolute;"
OnClick="@(() => AddToCollectionClicked.InvokeAsync(Data))"/> OnClick="@(() => AddToCollectionClicked.InvokeAsync(Data))"/>
} }
@if (DeleteClicked.HasDelegate) @if (DeleteClicked.HasDelegate)

5
ErsatzTV/wwwroot/browserconfig.xml

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<browserconfig> <browserconfig>
<msapplication> <msapplication>
<tile> <tile>
<square150x150logo src="/mstile-150x150.png"/> <square150x150logo src="/mstile-150x150.png" />
<TileColor>#da532c</TileColor> <TileColor>#da532c</TileColor>
</tile> </tile>
</msapplication> </msapplication>
</browserconfig> </browserconfig>

41
ErsatzTV/wwwroot/css/site.css

@ -4,13 +4,9 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.media-card-container { .media-card-container { width: 152px; }
width: 152px;
}
.media-card-episode-container { .media-card-episode-container { width: 392px; }
width: 392px;
}
.media-card { .media-card {
display: flex; display: flex;
@ -22,12 +18,9 @@
width: 152px; width: 152px;
} }
.media-card-episode { .media-card-episode { width: 392px; }
width: 392px;
}
.media-card:hover { /*filter: brightness(75%);*/ .media-card:hover { /*filter: brightness(75%);*/ }
}
.media-card-title { .media-card-title {
overflow: hidden; overflow: hidden;
@ -36,9 +29,7 @@
width: 100%; width: 100%;
} }
.media-card-poster-placeholder { .media-card-poster-placeholder { font-weight: bold; }
font-weight: bold;
}
.media-card-menu { .media-card-menu {
bottom: 0; bottom: 0;
@ -47,9 +38,7 @@
right: 0; right: 0;
} }
.media-card:hover .media-card-menu { .media-card:hover .media-card-menu { display: block; }
display: block;
}
.media-card-overlay { .media-card-overlay {
background: rgba(0, 0, 0, 0.4); background: rgba(0, 0, 0, 0.4);
@ -63,26 +52,20 @@
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.media-card-overlay:hover { .media-card-overlay:hover { opacity: 1; }
opacity: 1;
}
.search-bar .mud-input { .search-bar .mud-input { height: 42px; }
height: 42px;
}
.search-bar { .search-bar {
background-color: rgba(255, 255, 255, .15); background-color: rgba(255, 255, 255, .15);
margin-bottom: 5px;
height: 42px;
border-radius: 4px; border-radius: 4px;
height: 42px;
margin-bottom: 5px;
} }
.search-bar div .mud-input-root { .search-bar div .mud-input-root { color: #fafafa; }
color: #fafafa;
}
.search-bar .mud-input.mud-input-outlined .mud-input-outlined-border { .search-bar .mud-input.mud-input-outlined .mud-input-outlined-border {
border: none; border: none;
border-radius: 4px; border-radius: 4px;
} }
Loading…
Cancel
Save