Browse Source

rebuild playouts when items are added to collections

pull/39/head
Jason Dove 5 years ago
parent
commit
47c920b654
  1. 2
      ErsatzTV.Application/MediaCollections/CollectionUpdateResult.cs
  2. 4
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs
  3. 21
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs
  4. 4
      ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs
  5. 21
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs
  6. 4
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs
  7. 21
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs
  8. 4
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs
  9. 21
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs
  10. 3
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs
  11. 8
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs
  12. 2
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  13. 2
      ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs
  14. 11
      ErsatzTV.Core/Iptv/ChannelGuide.cs
  15. 2
      ErsatzTV.Core/Metadata/FallbackMetadataProvider.cs
  16. 8
      ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs
  17. 2
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  18. 5
      ErsatzTV.Infrastructure/Data/Repositories/MovieRepository.cs
  19. 8
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  20. 5
      ErsatzTV/Extensions/HostExtensions.cs
  21. 3
      ErsatzTV/Pages/CollectionItems.razor
  22. 17
      ErsatzTV/Pages/MovieList.razor
  23. 25
      ErsatzTV/Pages/Search.razor
  24. 16
      ErsatzTV/Pages/TelevisionEpisodeList.razor
  25. 16
      ErsatzTV/Pages/TelevisionSeasonList.razor
  26. 14
      ErsatzTV/Pages/TelevisionShowList.razor
  27. 2
      ErsatzTV/Shared/MainLayout.razor
  28. 2
      ErsatzTV/Shared/MediaCard.razor
  29. 5
      ErsatzTV/wwwroot/browserconfig.xml
  30. 41
      ErsatzTV/wwwroot/css/site.css

2
ErsatzTV.Application/MediaCollections/CollectionUpdateResult.cs

@ -2,7 +2,7 @@
namespace ErsatzTV.Application.MediaCollections namespace ErsatzTV.Application.MediaCollections
{ {
public record CollectionUpdateResult public class CollectionUpdateResult
{ {
public List<int> ModifiedPlayoutIds { get; set; } public List<int> ModifiedPlayoutIds { get; set; }
} }

4
ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs

@ -1,7 +1,9 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record AddEpisodeToCollection(int CollectionId, int EpisodeId) : MediatR.IRequest<Either<BaseError, Unit>>; public record AddEpisodeToCollection
(int CollectionId, int EpisodeId) : IRequest<Either<BaseError, CollectionUpdateResult>>;
} }

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

@ -3,11 +3,14 @@ using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class public class
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>> AddEpisodeToCollectionHandler : IRequestHandler<AddEpisodeToCollection,
Either<BaseError, CollectionUpdateResult>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
@ -20,15 +23,25 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, CollectionUpdateResult>> Handle(
AddEpisodeToCollection request, AddEpisodeToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request)) .MapT(_ => ApplyAddTelevisionEpisodeRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request) => private async Task<CollectionUpdateResult> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request)
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId); {
var result = new CollectionUpdateResult();
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId))
{
result.ModifiedPlayoutIds =
await _mediaCollectionRepository.PlayoutIdsUsingCollection(request.CollectionId);
}
return result;
}
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))

4
ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs

@ -1,7 +1,9 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record AddMovieToCollection(int CollectionId, int MovieId) : MediatR.IRequest<Either<BaseError, Unit>>; public record AddMovieToCollection
(int CollectionId, int MovieId) : IRequest<Either<BaseError, CollectionUpdateResult>>;
} }

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

@ -4,10 +4,13 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>> public class
AddMovieToCollectionHandler : IRequestHandler<AddMovieToCollection, Either<BaseError, CollectionUpdateResult>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
@ -20,15 +23,25 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_movieRepository = movieRepository; _movieRepository = movieRepository;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, CollectionUpdateResult>> Handle(
AddMovieToCollection request, AddMovieToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => ApplyAddMoviesRequest(request)) .MapT(_ => ApplyAddMoviesRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request) => private async Task<CollectionUpdateResult> ApplyAddMoviesRequest(AddMovieToCollection request)
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId); {
var result = new CollectionUpdateResult();
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId))
{
result.ModifiedPlayoutIds =
await _mediaCollectionRepository.PlayoutIdsUsingCollection(request.CollectionId);
}
return result;
}
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))

4
ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs

@ -1,7 +1,9 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record AddSeasonToCollection(int CollectionId, int SeasonId) : MediatR.IRequest<Either<BaseError, Unit>>; public record AddSeasonToCollection
(int CollectionId, int SeasonId) : IRequest<Either<BaseError, CollectionUpdateResult>>;
} }

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

@ -4,10 +4,13 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>> public class
AddSeasonToCollectionHandler : IRequestHandler<AddSeasonToCollection, Either<BaseError, CollectionUpdateResult>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
@ -20,15 +23,25 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, CollectionUpdateResult>> Handle(
AddSeasonToCollection request, AddSeasonToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => ApplyAddTelevisionSeasonRequest(request)) .MapT(_ => ApplyAddTelevisionSeasonRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request) => private async Task<CollectionUpdateResult> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request)
await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId); {
var result = new CollectionUpdateResult();
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId))
{
result.ModifiedPlayoutIds =
await _mediaCollectionRepository.PlayoutIdsUsingCollection(request.CollectionId);
}
return result;
}
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))

4
ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs

@ -1,7 +1,9 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record AddShowToCollection(int CollectionId, int ShowId) : MediatR.IRequest<Either<BaseError, Unit>>; public record AddShowToCollection
(int CollectionId, int ShowId) : IRequest<Either<BaseError, CollectionUpdateResult>>;
} }

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

@ -4,10 +4,13 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt; using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>> public class
AddShowToCollectionHandler : IRequestHandler<AddShowToCollection, Either<BaseError, CollectionUpdateResult>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
@ -20,15 +23,25 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_televisionRepository = televisionRepository; _televisionRepository = televisionRepository;
} }
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, CollectionUpdateResult>> Handle(
AddShowToCollection request, AddShowToCollection request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => ApplyAddTelevisionShowRequest(request)) .MapT(_ => ApplyAddTelevisionShowRequest(request))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request) private async Task<CollectionUpdateResult> ApplyAddTelevisionShowRequest(AddShowToCollection request)
=> _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId); {
var result = new CollectionUpdateResult();
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId))
{
result.ModifiedPlayoutIds =
await _mediaCollectionRepository.PlayoutIdsUsingCollection(request.CollectionId);
}
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))

3
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs

@ -1,10 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt; using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands namespace ErsatzTV.Application.MediaCollections.Commands
{ {
public record RemoveItemsFromCollection(int MediaCollectionId) : MediatR.IRequest<Either<BaseError, CollectionUpdateResult>> public record RemoveItemsFromCollection(int MediaCollectionId) : IRequest<Either<BaseError, CollectionUpdateResult>>
{ {
public List<int> MediaItemIds { get; set; } = new(); public List<int> MediaItemIds { get; set; } = new();
} }

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

@ -36,15 +36,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m)); itemsToRemove.ForEach(m => collection.MediaItems.Remove(m));
var result = new CollectionUpdateResult();
if (itemsToRemove.Any() && await _mediaCollectionRepository.Update(collection)) if (itemsToRemove.Any() && await _mediaCollectionRepository.Update(collection))
{ {
return new CollectionUpdateResult result.ModifiedPlayoutIds = await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id);
{
ModifiedPlayoutIds = await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id)
};
} }
return new CollectionUpdateResult(); return result;
} }
private Task<Validation<BaseError, Collection>> Validate( private Task<Validation<BaseError, Collection>> Validate(

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

@ -15,7 +15,7 @@ namespace ErsatzTV.Core.Tests.Fakes
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();

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

@ -8,7 +8,7 @@ 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);

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))

8
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) =>

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;
}); });

3
ErsatzTV/Pages/CollectionItems.razor

@ -9,6 +9,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>
@ -169,7 +170,7 @@
{ {
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds) foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{ {
await Mediator.Send(new BuildPlayout(playoutId, true)); await Channel.WriteAsync(new BuildPlayout(playoutId, true));
} }
}, },
_ => Task.CompletedTask); _ => Task.CompletedTask);

17
ErsatzTV/Pages/MovieList.razor

@ -4,12 +4,13 @@
@using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using Unit = LanguageExt.Unit @using ErsatzTV.Application.Playouts.Commands
@inject ILogger<MovieList> Logger @inject ILogger<MovieList> Logger
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@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;">
@ -73,14 +74,22 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMovieToCollection(collection.Id, movie.MovieId); var request = new AddMovieToCollection(collection.Id, movie.MovieId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( await addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding movie to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding movie to collection: {error.Value}");
Logger.LogError("Unexpected error adding movie to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding movie to collection: {Error}", error.Value);
return Task.CompletedTask;
}, },
Right: _ => Snackbar.Add($"Added {movie.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {movie.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
} }

25
ErsatzTV/Pages/Search.razor

@ -3,14 +3,15 @@
@using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.Playouts.Commands
@using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.WebUtilities
@using Microsoft.Extensions.Primitives @using Microsoft.Extensions.Primitives
@using Unit = LanguageExt.Unit
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IMediator Mediator @inject IMediator Mediator
@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;">
@ -74,14 +75,21 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddMovieToCollection(collection.Id, movie.MovieId); var request = new AddMovieToCollection(collection.Id, movie.MovieId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding movie to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding movie to collection: {error.Value}");
Logger.LogError("Unexpected error adding movie to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding movie to collection: {Error}", error.Value);
}, },
Right: _ => Snackbar.Add($"Added {movie.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {movie.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
@ -95,14 +103,21 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddShowToCollection(collection.Id, show.TelevisionShowId); var request = new AddShowToCollection(collection.Id, show.TelevisionShowId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding show to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding show to collection: {error.Value}");
Logger.LogError("Unexpected error adding show to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding show to collection: {Error}", error.Value);
}, },
Right: _ => Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
} }

16
ErsatzTV/Pages/TelevisionEpisodeList.razor

@ -5,14 +5,15 @@
@using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.Playouts.Commands
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@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)
@ -135,14 +136,21 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId); var request = new AddEpisodeToCollection(collection.Id, episode.EpisodeId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding episode to collection: {error.Value}");
Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding episode to collection: {Error}", error.Value);
}, },
Right: _ => Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {episode.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
} }

16
ErsatzTV/Pages/TelevisionSeasonList.razor

@ -5,14 +5,15 @@
@using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using ErsatzTV.Application.Playouts.Commands
@using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules
@using ErsatzTV.Application.ProgramSchedules.Commands @using ErsatzTV.Application.ProgramSchedules.Commands
@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)
@ -129,14 +130,21 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId); var request = new AddSeasonToCollection(collection.Id, season.TelevisionSeasonId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding season to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding season to collection: {error.Value}");
Logger.LogError("Unexpected error adding season to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding season to collection: {Error}", error.Value);
}, },
Right: _ => Snackbar.Add($"Added {season.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {season.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
} }

14
ErsatzTV/Pages/TelevisionShowList.razor

@ -3,11 +3,12 @@
@using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections
@using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.MediaCollections.Commands
@using Unit = LanguageExt.Unit @using ErsatzTV.Application.Playouts.Commands
@inject ILogger<TelevisionShowList> Logger @inject ILogger<TelevisionShowList> Logger
@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;">
@ -69,14 +70,21 @@
if (!result.Cancelled && result.Data is MediaCollectionViewModel collection) if (!result.Cancelled && result.Data is MediaCollectionViewModel collection)
{ {
var request = new AddShowToCollection(collection.Id, show.TelevisionShowId); var request = new AddShowToCollection(collection.Id, show.TelevisionShowId);
Either<BaseError, Unit> addResult = await Mediator.Send(request); Either<BaseError, CollectionUpdateResult> addResult = await Mediator.Send(request);
addResult.Match( addResult.Match(
Left: error => Left: error =>
{ {
Snackbar.Add($"Unexpected error adding show to collection: {error.Value}"); Snackbar.Add($"Unexpected error adding show to collection: {error.Value}");
Logger.LogError("Unexpected error adding show to collection: {Error}", error.Value); Logger.LogError("Unexpected error adding show to collection: {Error}", error.Value);
}, },
Right: _ => Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success)); Right: async collectionUpdateResult =>
{
foreach (int playoutId in collectionUpdateResult.ModifiedPlayoutIds)
{
await Channel.WriteAsync(new BuildPlayout(playoutId, true));
}
Snackbar.Add($"Added {show.Title} to collection {collection.Name}", Severity.Success);
});
} }
} }
} }

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