rework television media (#26)

* rework television media

* refactor poster saving

* television and movie views are working again

* remove dead code

* use paper styling for all cards

* add show poster, plot to seasons page

* remove missing shows; cleanup interfaces

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

* add placeholder "add to schedule" button

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

* stop releasing CLI for now

* use season number as season placeholder

* add television shows to collections

* add television seasons to collections

* add television episodes to collections

* add movies to collections

* remove movies, shows, seasons, episodes from collections

* fix page width and menus

* fix buffer size defaults

* fix chronological episode ordering

* allow deleting media collections

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

* schedule editing and playouts work again

* minor cleanup

* remove dead code

* fix bugs with viewing movies as they are loading

* add scanner tests; support nested movie folders

* update collections docs

* rearrange order of schedule items

* add show and season to schedule

* delete schedules that use legacy collections, reset all posters

* move cleanup to new migration

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

* update readme and screenshots
This commit is contained in:
Jason Dove
2021-02-22 00:54:41 +00:00
committed by GitHub
parent 98cf922b3c
commit 871a031467
259 changed files with 13493 additions and 4771 deletions
+5 -5
View File
@@ -39,24 +39,24 @@ jobs:
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
#release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
#dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*"
#7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
tar czvf "${release_name_cli}.tar.gz" "$release_name_cli"
#tar czvf "${release_name_cli}.tar.gz" "$release_name_cli"
fi
# Delete output directory
rm -r "$release_name"
rm -r "$release_name_cli"
#rm -r "$release_name_cli"
- name: Publish
uses: softprops/action-gh-release@v1
-9
View File
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application
{
public interface IMediaCard
{
string Title { get; }
string SortTitle { get; }
string Subtitle { get; }
}
}
+60
View File
@@ -0,0 +1,60 @@
using System;
using System.Linq;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCards
{
internal static class Mapper
{
internal static TelevisionShowCardViewModel ProjectToViewModel(TelevisionShow televisionShow) =>
new(
televisionShow.Id,
televisionShow.Metadata?.Title,
televisionShow.Metadata?.Year.ToString(),
televisionShow.Metadata?.SortTitle,
televisionShow.Poster);
internal static TelevisionSeasonCardViewModel ProjectToViewModel(TelevisionSeason televisionSeason) =>
new(
televisionSeason.TelevisionShow.Metadata?.Title,
televisionSeason.Id,
televisionSeason.Number,
GetSeasonName(televisionSeason.Number),
string.Empty,
GetSeasonName(televisionSeason.Number),
televisionSeason.Poster,
televisionSeason.Number == 0 ? "S" : televisionSeason.Number.ToString());
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
TelevisionEpisodeMediaItem televisionEpisode) =>
new(
televisionEpisode.Id,
televisionEpisode.Metadata?.Aired ?? DateTime.MinValue,
televisionEpisode.Season.TelevisionShow.Metadata.Title,
televisionEpisode.Metadata?.Title,
$"Episode {televisionEpisode.Metadata?.Episode}",
televisionEpisode.Metadata?.Episode.ToString(),
televisionEpisode.Poster,
televisionEpisode.Metadata?.Episode.ToString());
internal static MovieCardViewModel ProjectToViewModel(MovieMediaItem movie) =>
new(
movie.Id,
movie.Metadata?.Title,
movie.Metadata?.Year?.ToString(),
movie.Metadata?.SortTitle,
movie.Poster);
internal static SimpleMediaCollectionCardResultsViewModel
ProjectToViewModel(SimpleMediaCollection collection) =>
new(
collection.Name,
collection.Movies.Map(ProjectToViewModel).ToList(),
collection.TelevisionShows.Map(ProjectToViewModel).ToList(),
collection.TelevisionSeasons.Map(ProjectToViewModel).ToList(),
collection.TelevisionEpisodes.Map(ProjectToViewModel).ToList());
private static string GetSeasonName(int number) =>
number == 0 ? "Specials" : $"Season {number}";
}
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.MediaCards
{
public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster);
}
@@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards);
}
@@ -0,0 +1,11 @@
namespace ErsatzTV.Application.MediaCards
{
public record MovieCardViewModel
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
{
}
}
@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetMovieCards(int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
}
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class
GetMovieCardsHandler : IRequestHandler<GetMovieCards, MovieCardResultsViewModel>
{
private readonly IMovieRepository _movieRepository;
public GetMovieCardsHandler(IMovieRepository movieRepository) => _movieRepository = movieRepository;
public async Task<MovieCardResultsViewModel> Handle(
GetMovieCards request,
CancellationToken cancellationToken)
{
int count = await _movieRepository.GetMovieCount();
List<MovieCardViewModel> results = await _movieRepository
.GetPagedMovies(request.PageNumber, request.PageSize)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new MovieCardResultsViewModel(count, results);
}
}
}
@@ -0,0 +1,9 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetSimpleMediaCollectionCards
(int Id) : IRequest<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>;
}
@@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class GetSimpleMediaCollectionCardsHandler : IRequestHandler<GetSimpleMediaCollectionCards,
Either<BaseError, SimpleMediaCollectionCardResultsViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionCardsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Either<BaseError, SimpleMediaCollectionCardResultsViewModel>> Handle(
GetSimpleMediaCollectionCards request,
CancellationToken cancellationToken) =>
(await _mediaCollectionRepository.GetSimpleMediaCollectionWithItemsUntracked(request.Id))
.ToEither(BaseError.New("Unable to load collection"))
.Map(ProjectToViewModel);
}
}
@@ -0,0 +1,7 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionEpisodeCards
(int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest<TelevisionEpisodeCardResultsViewModel>;
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class
GetTelevisionEpisodeCardsHandler : IRequestHandler<GetTelevisionEpisodeCards,
TelevisionEpisodeCardResultsViewModel>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionEpisodeCardsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public async Task<TelevisionEpisodeCardResultsViewModel> Handle(
GetTelevisionEpisodeCards request,
CancellationToken cancellationToken)
{
int count = await _televisionRepository.GetEpisodeCount(request.TelevisionSeasonId);
List<TelevisionEpisodeCardViewModel> results = await _televisionRepository
.GetPagedEpisodes(request.TelevisionSeasonId, request.PageNumber, request.PageSize)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new TelevisionEpisodeCardResultsViewModel(count, results);
}
}
}
@@ -0,0 +1,7 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionSeasonCards
(int TelevisionShowId, int PageNumber, int PageSize) : IRequest<TelevisionSeasonCardResultsViewModel>;
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class
GetTelevisionSeasonCardsHandler : IRequestHandler<GetTelevisionSeasonCards, TelevisionSeasonCardResultsViewModel
>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionSeasonCardsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public async Task<TelevisionSeasonCardResultsViewModel> Handle(
GetTelevisionSeasonCards request,
CancellationToken cancellationToken)
{
int count = await _televisionRepository.GetSeasonCount(request.TelevisionShowId);
List<TelevisionSeasonCardViewModel> results = await _televisionRepository
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new TelevisionSeasonCardResultsViewModel(count, results);
}
}
}
@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionShowCards(int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class
GetTelevisionShowCardsHandler : IRequestHandler<GetTelevisionShowCards, TelevisionShowCardResultsViewModel>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionShowCardsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public async Task<TelevisionShowCardResultsViewModel> Handle(
GetTelevisionShowCards request,
CancellationToken cancellationToken)
{
int count = await _televisionRepository.GetShowCount();
List<TelevisionShowCardViewModel> results = await _televisionRepository
.GetPagedShows(request.PageNumber, request.PageSize)
.Map(list => list.Map(ProjectToViewModel).ToList());
return new TelevisionShowCardResultsViewModel(count, results);
}
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record SimpleMediaCollectionCardResultsViewModel(
string Name,
List<MovieCardViewModel> MovieCards,
List<TelevisionShowCardViewModel> ShowCards,
List<TelevisionSeasonCardViewModel> SeasonCards,
List<TelevisionEpisodeCardViewModel> EpisodeCards);
}
@@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionEpisodeCardResultsViewModel(int Count, List<TelevisionEpisodeCardViewModel> Cards);
}
@@ -0,0 +1,21 @@
using System;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionEpisodeCardViewModel
(
int EpisodeId,
DateTime Aired,
string ShowTitle,
string Title,
string Subtitle,
string SortTitle,
string Poster,
string Placeholder) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
{
}
}
@@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionSeasonCardResultsViewModel(int Count, List<TelevisionSeasonCardViewModel> Cards);
}
@@ -0,0 +1,19 @@
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionSeasonCardViewModel
(
string ShowTitle,
int TelevisionSeasonId,
int TelevisionSeasonNumber,
string Title,
string Subtitle,
string SortTitle,
string Poster,
string Placeholder) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
{
}
}
@@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionShowCardResultsViewModel(int Count, List<TelevisionShowCardViewModel> Cards);
}
@@ -0,0 +1,11 @@
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionShowCardViewModel
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
Title,
Subtitle,
SortTitle,
Poster)
{
}
}
@@ -1,9 +0,0 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddItemsToSimpleMediaCollection
(int MediaCollectionId, List<int> ItemIds) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,79 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddItemsToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddItemsToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public AddItemsToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_mediaItemRepository = mediaItemRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddItemsToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddItemsRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddItemsRequest(RequestParameters parameters)
{
foreach (MediaItem item in parameters.ItemsToAdd.Where(
item => parameters.Collection.Items.All(i => i.Id != item.Id)))
{
parameters.Collection.Items.Add(item);
}
await _mediaCollectionRepository.Update(parameters.Collection);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddItemsToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateItems(request))
.Apply(
(simpleMediaCollectionToUpdate, itemsToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, itemsToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddItemsToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, List<MediaItem>>> ValidateItems(
AddItemsToSimpleMediaCollection request) =>
LoadAllMediaItems(request)
.Map(v => v.ToValidation<BaseError>("MediaItem does not exist"));
private async Task<Option<List<MediaItem>>> LoadAllMediaItems(AddItemsToSimpleMediaCollection request)
{
var items = (await request.ItemIds.Map(async id => await _mediaItemRepository.Get(id)).Sequence())
.ToList();
if (items.Any(i => i.IsNone))
{
return None;
}
return items.Somes().ToList();
}
private record RequestParameters(SimpleMediaCollection Collection, List<MediaItem> ItemsToAdd);
}
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddMovieToSimpleMediaCollection
(int MediaCollectionId, int MovieId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,62 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddMovieToSimpleMediaCollectionHandler : MediatR.IRequestHandler<AddMovieToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
public AddMovieToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddMovieToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddMoviesRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddMoviesRequest(RequestParameters parameters)
{
parameters.Collection.Movies.Add(parameters.MovieToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddMovieToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateMovies(request))
.Apply(
(simpleMediaCollectionToUpdate, movieToAdd) =>
new RequestParameters(simpleMediaCollectionToUpdate, movieToAdd));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddMovieToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, MovieMediaItem>> ValidateMovies(
AddMovieToSimpleMediaCollection request) =>
LoadMovie(request)
.Map(v => v.ToValidation<BaseError>("MovieMediaItem does not exist"));
private Task<Option<MovieMediaItem>> LoadMovie(AddMovieToSimpleMediaCollection request) =>
_movieRepository.GetMovie(request.MovieId);
private record RequestParameters(SimpleMediaCollection Collection, MovieMediaItem MovieToAdd);
}
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddTelevisionEpisodeToSimpleMediaCollection
(int MediaCollectionId, int TelevisionEpisodeId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,68 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddTelevisionEpisodeToSimpleMediaCollectionHandler : MediatR.IRequestHandler<
AddTelevisionEpisodeToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddTelevisionEpisodeToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddTelevisionEpisodeToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddTelevisionEpisodeRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(RequestParameters parameters)
{
if (parameters.Collection.TelevisionEpisodes.All(s => s.Id != parameters.EpisodeToAdd.Id))
{
parameters.Collection.TelevisionEpisodes.Add(parameters.EpisodeToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
}
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddTelevisionEpisodeToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateEpisode(request))
.Apply(
(simpleMediaCollectionToUpdate, episode) =>
new RequestParameters(simpleMediaCollectionToUpdate, episode));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddTelevisionEpisodeToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, TelevisionEpisodeMediaItem>> ValidateEpisode(
AddTelevisionEpisodeToSimpleMediaCollection request) =>
LoadTelevisionEpisode(request)
.Map(v => v.ToValidation<BaseError>("TelevisionEpisode does not exist"));
private Task<Option<TelevisionEpisodeMediaItem>> LoadTelevisionEpisode(
AddTelevisionEpisodeToSimpleMediaCollection request) =>
_televisionRepository.GetEpisode(request.TelevisionEpisodeId);
private record RequestParameters(SimpleMediaCollection Collection, TelevisionEpisodeMediaItem EpisodeToAdd);
}
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddTelevisionSeasonToSimpleMediaCollection
(int MediaCollectionId, int TelevisionSeasonId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,68 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddTelevisionSeasonToSimpleMediaCollectionHandler : MediatR.IRequestHandler<
AddTelevisionSeasonToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddTelevisionSeasonToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddTelevisionSeasonToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddTelevisionSeasonRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionSeasonRequest(RequestParameters parameters)
{
if (parameters.Collection.TelevisionSeasons.All(s => s.Id != parameters.SeasonToAdd.Id))
{
parameters.Collection.TelevisionSeasons.Add(parameters.SeasonToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
}
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddTelevisionSeasonToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateSeason(request))
.Apply(
(simpleMediaCollectionToUpdate, season) =>
new RequestParameters(simpleMediaCollectionToUpdate, season));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddTelevisionSeasonToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, TelevisionSeason>> ValidateSeason(
AddTelevisionSeasonToSimpleMediaCollection request) =>
LoadTelevisionSeason(request)
.Map(v => v.ToValidation<BaseError>("TelevisionSeason does not exist"));
private Task<Option<TelevisionSeason>> LoadTelevisionSeason(
AddTelevisionSeasonToSimpleMediaCollection request) =>
_televisionRepository.GetSeason(request.TelevisionSeasonId);
private record RequestParameters(SimpleMediaCollection Collection, TelevisionSeason SeasonToAdd);
}
}
@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record AddTelevisionShowToSimpleMediaCollection
(int MediaCollectionId, int TelevisionShowId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -0,0 +1,67 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddTelevisionShowToSimpleMediaCollectionHandler : MediatR.IRequestHandler<
AddTelevisionShowToSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddTelevisionShowToSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddTelevisionShowToSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(ApplyAddTelevisionShowRequest)
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionShowRequest(RequestParameters parameters)
{
if (parameters.Collection.TelevisionShows.All(s => s.Id != parameters.ShowToAdd.Id))
{
parameters.Collection.TelevisionShows.Add(parameters.ShowToAdd);
await _mediaCollectionRepository.Update(parameters.Collection);
}
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>>
Validate(AddTelevisionShowToSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), await ValidateShow(request))
.Apply(
(simpleMediaCollectionToUpdate, show) =>
new RequestParameters(simpleMediaCollectionToUpdate, show));
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
AddTelevisionShowToSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Task<Validation<BaseError, TelevisionShow>> ValidateShow(
AddTelevisionShowToSimpleMediaCollection request) =>
LoadTelevisionShow(request)
.Map(v => v.ToValidation<BaseError>("TelevisionShow does not exist"));
private Task<Option<TelevisionShow>> LoadTelevisionShow(AddTelevisionShowToSimpleMediaCollection request) =>
_televisionRepository.GetShow(request.TelevisionShowId);
private record RequestParameters(SimpleMediaCollection Collection, TelevisionShow ShowToAdd);
}
}
@@ -29,7 +29,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) =>
ValidateName(request).MapT(name => new SimpleMediaCollection { Name = name });
ValidateName(request).MapT(
name => new SimpleMediaCollection
{
Name = name,
Movies = new List<MovieMediaItem>(),
TelevisionShows = new List<TelevisionShow>(),
TelevisionEpisodes = new List<TelevisionEpisodeMediaItem>(),
TelevisionSeasons = new List<TelevisionSeason>()
});
private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection)
{
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record RemoveItemsFromSimpleMediaCollection
(int MediaCollectionId) : MediatR.IRequest<Either<BaseError, Unit>>
{
public List<int> MovieIds { get; set; } = new();
public List<int> TelevisionShowIds { get; set; } = new();
public List<int> TelevisionSeasonIds { get; set; } = new();
public List<int> TelevisionEpisodeIds { get; set; } = new();
}
}
@@ -0,0 +1,74 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
RemoveItemsFromSimpleMediaCollectionHandler : MediatR.IRequestHandler<
RemoveItemsFromSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public RemoveItemsFromSimpleMediaCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Either<BaseError, Unit>> Handle(
RemoveItemsFromSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection))
.Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest(
RemoveItemsFromSimpleMediaCollection request,
SimpleMediaCollection collection)
{
var moviesToRemove = collection.Movies
.Filter(m => request.MovieIds.Contains(m.Id))
.ToList();
moviesToRemove.ForEach(m => collection.Movies.Remove(m));
var showsToRemove = collection.TelevisionShows
.Filter(s => request.TelevisionShowIds.Contains(s.Id))
.ToList();
showsToRemove.ForEach(s => collection.TelevisionShows.Remove(s));
var seasonsToRemove = collection.TelevisionSeasons
.Filter(s => request.TelevisionSeasonIds.Contains(s.Id))
.ToList();
seasonsToRemove.ForEach(s => collection.TelevisionSeasons.Remove(s));
var episodesToRemove = collection.TelevisionEpisodes
.Filter(e => request.TelevisionEpisodeIds.Contains(e.Id))
.ToList();
episodesToRemove.ForEach(e => collection.TelevisionEpisodes.Remove(e));
if (moviesToRemove.Any() || showsToRemove.Any() || seasonsToRemove.Any() || episodesToRemove.Any())
{
return _mediaCollectionRepository.Update(collection).ToUnit();
}
return Task.FromResult(Unit.Default);
}
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(
RemoveItemsFromSimpleMediaCollection request) =>
SimpleMediaCollectionMustExist(request);
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
RemoveItemsFromSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollectionWithItems(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
}
}
@@ -1,11 +0,0 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record ReplaceSimpleMediaCollectionItems
(int MediaCollectionId, List<int> MediaItemIds) : IRequest<Either<BaseError, List<MediaItemViewModel>>>;
}
@@ -1,65 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using LanguageExt.UnsafeValueAccess;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class ReplaceSimpleMediaCollectionItemsHandler : IRequestHandler<ReplaceSimpleMediaCollectionItems,
Either<BaseError, List<MediaItemViewModel>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaItemRepository _mediaItemRepository;
public ReplaceSimpleMediaCollectionItemsHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMediaItemRepository mediaItemRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_mediaItemRepository = mediaItemRepository;
}
public Task<Either<BaseError, List<MediaItemViewModel>>> Handle(
ReplaceSimpleMediaCollectionItems request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(mediaItems => PersistItems(request, mediaItems))
.Bind(v => v.ToEitherAsync());
private async Task<List<MediaItemViewModel>> PersistItems(
ReplaceSimpleMediaCollectionItems request,
List<MediaItem> mediaItems)
{
await _mediaCollectionRepository.ReplaceItems(request.MediaCollectionId, mediaItems);
return mediaItems.Map(MediaItems.Mapper.ProjectToViewModel).ToList();
}
private Task<Validation<BaseError, List<MediaItem>>> Validate(ReplaceSimpleMediaCollectionItems request) =>
MediaCollectionMustExist(request).BindT(_ => MediaItemsMustExist(request));
private async Task<Validation<BaseError, SimpleMediaCollection>> MediaCollectionMustExist(
ReplaceSimpleMediaCollectionItems request) =>
(await _mediaCollectionRepository.GetSimpleMediaCollection(request.MediaCollectionId))
.ToValidation<BaseError>("[MediaCollectionId] does not exist.");
private async Task<Validation<BaseError, List<MediaItem>>> MediaItemsMustExist(
ReplaceSimpleMediaCollectionItems replaceItems)
{
var allMediaItems = (await replaceItems.MediaItemIds.Map(i => _mediaItemRepository.Get(i)).Sequence())
.ToList();
if (allMediaItems.Any(x => x.IsNone))
{
return BaseError.New("[MediaItemId] does not exist");
}
return allMediaItems.Sequence().ValueUnsafe().ToList();
}
}
}
@@ -1,5 +1,4 @@
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections
{
@@ -7,13 +6,5 @@ namespace ErsatzTV.Application.MediaCollections
{
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) =>
new(mediaCollection.Id, mediaCollection.Name);
internal static MediaCollectionSummaryViewModel ProjectToViewModel(
MediaCollectionSummary mediaCollectionSummary) =>
new(
mediaCollectionSummary.Id,
mediaCollectionSummary.Name,
mediaCollectionSummary.ItemCount,
mediaCollectionSummary.IsSimple);
}
}
@@ -1,4 +1,10 @@
namespace ErsatzTV.Application.MediaCollections
using ErsatzTV.Application.MediaCards;
namespace ErsatzTV.Application.MediaCollections
{
public record MediaCollectionViewModel(int Id, string Name);
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
Name,
string.Empty,
Name,
string.Empty);
}
@@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>;
}
@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class
GetMediaCollectionSummariesHandler : IRequestHandler<GetMediaCollectionSummaries,
List<MediaCollectionSummaryViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetMediaCollectionSummariesHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<List<MediaCollectionSummaryViewModel>> Handle(
GetMediaCollectionSummaries request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetSummaries(request.SearchString)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}
@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetSimpleMediaCollectionWithItemsById
(int Id) : IRequest<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>;
}
@@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.MediaCollections.Mapper;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class GetSimpleMediaCollectionWithItemsByIdHandler : IRequestHandler<GetSimpleMediaCollectionWithItemsById,
Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionWithItemsByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>> Handle(
GetSimpleMediaCollectionWithItemsById request,
CancellationToken cancellationToken)
{
Option<SimpleMediaCollection> maybeCollection =
await _mediaCollectionRepository.GetSimpleMediaCollectionWithItems(request.Id);
return maybeCollection.Match<Option<Tuple<MediaCollectionViewModel, List<MediaItemSearchResultViewModel>>>>(
c => Tuple(ProjectToViewModel(c), c.Items.Map(ProjectToSearchViewModel).ToList()),
None);
}
}
}
@@ -1,6 +0,0 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemResults(int Count, List<AggregateMediaItemViewModel> DataPage);
}
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemViewModel(
int MediaItemId,
string Title,
string Subtitle,
string SortTitle,
string Poster);
}
@@ -1,8 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Commands
{
public record CreateMediaItem(int MediaSourceId, string Path) : IRequest<Either<BaseError, MediaItemViewModel>>;
}
@@ -1,101 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class CreateMediaItemHandler : IRequestHandler<CreateMediaItem, Either<BaseError, MediaItemViewModel>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly ILocalPosterProvider _localPosterProvider;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly IMediaItemRepository _mediaItemRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
public CreateMediaItemHandler(
IMediaItemRepository mediaItemRepository,
IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository,
ISmartCollectionBuilder smartCollectionBuilder,
ILocalMetadataProvider localMetadataProvider,
ILocalStatisticsProvider localStatisticsProvider,
ILocalPosterProvider localPosterProvider)
{
_mediaItemRepository = mediaItemRepository;
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_smartCollectionBuilder = smartCollectionBuilder;
_localMetadataProvider = localMetadataProvider;
_localStatisticsProvider = localStatisticsProvider;
_localPosterProvider = localPosterProvider;
}
public Task<Either<BaseError, MediaItemViewModel>> Handle(
CreateMediaItem request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistMediaItem)
.Bind(v => v.ToEitherAsync());
private async Task<MediaItemViewModel> PersistMediaItem(RequestParameters parameters)
{
await _mediaItemRepository.Add(parameters.MediaItem);
await _localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem);
// TODO: reimplement this
// await _localMetadataProvider.RefreshMetadata(parameters.MediaItem);
// await _localPosterProvider.RefreshPoster(parameters.MediaItem);
// await _smartCollectionBuilder.RefreshSmartCollections(parameters.MediaItem);
return ProjectToViewModel(parameters.MediaItem);
}
private async Task<Validation<BaseError, RequestParameters>> Validate(CreateMediaItem request) =>
(await ValidateMediaSource(request), PathMustExist(request), await ValidateFFprobePath())
.Apply(
(mediaSourceId, path, ffprobePath) => new RequestParameters(
ffprobePath,
new MediaItem
{
MediaSourceId = mediaSourceId,
Path = path
}));
private async Task<Validation<BaseError, int>> ValidateMediaSource(CreateMediaItem createMediaItem) =>
(await MediaSourceMustExist(createMediaItem)).Bind(MediaSourceMustBeLocal);
private async Task<Validation<BaseError, MediaSource>> MediaSourceMustExist(CreateMediaItem createMediaItem) =>
(await _mediaSourceRepository.Get(createMediaItem.MediaSourceId))
.ToValidation<BaseError>($"[MediaSource] {createMediaItem.MediaSourceId} does not exist.");
private Validation<BaseError, int> MediaSourceMustBeLocal(MediaSource mediaSource) =>
Some(mediaSource)
.Filter(ms => ms is LocalMediaSource)
.ToValidation<BaseError>($"[MediaSource] {mediaSource.Id} must be a local media source")
.Map(ms => ms.Id);
private Validation<BaseError, string> PathMustExist(CreateMediaItem createMediaItem) =>
Some(createMediaItem.Path)
.Filter(File.Exists)
.ToValidation<BaseError>("[Path] does not exist on the file system");
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(string FFprobePath, MediaItem MediaItem);
}
}
@@ -1,9 +0,0 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Commands
{
public record DeleteMediaItem(int MediaItemId) : IRequest<Either<BaseError, Task>>;
}
@@ -1,31 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class DeleteMediaItemHandler : IRequestHandler<DeleteMediaItem, Either<BaseError, Task>>
{
private readonly IMediaItemRepository _mediaItemRepository;
public DeleteMediaItemHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<Either<BaseError, Task>> Handle(
DeleteMediaItem request,
CancellationToken cancellationToken) =>
(await MediaItemMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
private Task DoDeletion(int mediaItemId) => _mediaItemRepository.Delete(mediaItemId);
private async Task<Validation<BaseError, int>> MediaItemMustExist(DeleteMediaItem deleteMediaItem) =>
(await _mediaItemRepository.Get(deleteMediaItem.MediaItemId))
.ToValidation<BaseError>($"MediaItem {deleteMediaItem.MediaItemId} does not exist.")
.Map(c => c.Id);
}
}
@@ -1,8 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItem(int MediaItemId) : MediatR.IRequest<Either<BaseError, Unit>>,
IBackgroundServiceRequest;
}
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemCollections : RefreshMediaItem
{
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId)
{
}
}
}
@@ -1,46 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class
RefreshMediaItemCollectionsHandler : MediatR.IRequestHandler<RefreshMediaItemCollections,
Either<BaseError, Unit>>
{
private readonly IMediaItemRepository _mediaItemRepository;
private readonly ISmartCollectionBuilder _smartCollectionBuilder;
public RefreshMediaItemCollectionsHandler(
IMediaItemRepository mediaItemRepository,
ISmartCollectionBuilder smartCollectionBuilder)
{
_mediaItemRepository = mediaItemRepository;
_smartCollectionBuilder = smartCollectionBuilder;
}
public Task<Either<BaseError, Unit>> Handle(
RefreshMediaItemCollections request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(RefreshCollections)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemCollections request) =>
MediaItemMustExist(request);
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
RefreshMediaItemCollections refreshMediaItemCollections) =>
_mediaItemRepository.Get(refreshMediaItemCollections.MediaItemId)
.Map(
maybeItem => maybeItem.ToValidation<BaseError>(
$"[MediaItem] {refreshMediaItemCollections.MediaItemId} does not exist."));
private Task<Unit> RefreshCollections(MediaItem mediaItem) =>
_smartCollectionBuilder.RefreshSmartCollections(mediaItem).ToUnit();
}
}
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemMetadata : RefreshMediaItem
{
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId)
{
}
}
}
@@ -1,53 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class
RefreshMediaItemMetadataHandler : MediatR.IRequestHandler<RefreshMediaItemMetadata, Either<BaseError, Unit>>
{
private readonly ILocalMetadataProvider _localMetadataProvider;
private readonly IMediaItemRepository _mediaItemRepository;
public RefreshMediaItemMetadataHandler(
IMediaItemRepository mediaItemRepository,
ILocalMetadataProvider localMetadataProvider)
{
_mediaItemRepository = mediaItemRepository;
_localMetadataProvider = localMetadataProvider;
}
public Task<Either<BaseError, Unit>> Handle(
RefreshMediaItemMetadata request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(RefreshMetadata)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemMetadata request) =>
MediaItemMustExist(request).BindT(PathMustExist);
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
RefreshMediaItemMetadata refreshMediaItemMetadata) =>
_mediaItemRepository.Get(refreshMediaItemMetadata.MediaItemId)
.Map(
maybeItem => maybeItem.ToValidation<BaseError>(
$"[MediaItem] {refreshMediaItemMetadata.MediaItemId} does not exist."));
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
Some(mediaItem)
.Filter(item => File.Exists(item.Path))
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
private Task<Unit> RefreshMetadata(MediaItem mediaItem) => Task.CompletedTask.ToUnit();
// TODO: reimplement this
// _localMetadataProvider.RefreshMetadata(mediaItem).ToUnit();
}
}
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemPoster : RefreshMediaItem
{
public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId)
{
}
}
}
@@ -1,45 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class
RefreshMediaItemPosterHandler : MediatR.IRequestHandler<RefreshMediaItemPoster,
Either<BaseError, Unit>>
{
private readonly ILocalPosterProvider _localPosterProvider;
private readonly IMediaItemRepository _mediaItemRepository;
public RefreshMediaItemPosterHandler(
IMediaItemRepository mediaItemRepository,
ILocalPosterProvider localPosterProvider)
{
_mediaItemRepository = mediaItemRepository;
_localPosterProvider = localPosterProvider;
}
public Task<Either<BaseError, Unit>> Handle(
RefreshMediaItemPoster request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(RefreshPoster)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, MediaItem>> Validate(RefreshMediaItemPoster request) =>
MediaItemMustExist(request);
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(RefreshMediaItemPoster request) =>
_mediaItemRepository.Get(request.MediaItemId)
.Map(
maybeItem => maybeItem.ToValidation<BaseError>(
$"[MediaItem] {request.MediaItemId} does not exist."));
private Task<Unit> RefreshPoster(MediaItem mediaItem) =>
_localPosterProvider.RefreshPoster(mediaItem).ToUnit();
}
}
@@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemStatistics : RefreshMediaItem
{
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId)
{
}
}
}
@@ -1,65 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaItems.Commands
{
public class
RefreshMediaItemStatisticsHandler : MediatR.IRequestHandler<RefreshMediaItemStatistics, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly IMediaItemRepository _mediaItemRepository;
public RefreshMediaItemStatisticsHandler(
IMediaItemRepository mediaItemRepository,
IConfigElementRepository configElementRepository,
ILocalStatisticsProvider localStatisticsProvider)
{
_mediaItemRepository = mediaItemRepository;
_configElementRepository = configElementRepository;
_localStatisticsProvider = localStatisticsProvider;
}
public Task<Either<BaseError, Unit>> Handle(
RefreshMediaItemStatistics request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(RefreshStatistics)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, RefreshParameters>> Validate(RefreshMediaItemStatistics request) =>
(await MediaItemMustExist(request).BindT(PathMustExist), await ValidateFFprobePath())
.Apply((mediaItem, ffprobePath) => new RefreshParameters(mediaItem, ffprobePath));
private Task<Validation<BaseError, MediaItem>> MediaItemMustExist(
RefreshMediaItemStatistics refreshMediaItemStatistics) =>
_mediaItemRepository.Get(refreshMediaItemStatistics.MediaItemId)
.Map(
maybeItem => maybeItem.ToValidation<BaseError>(
$"[MediaItem] {refreshMediaItemStatistics.MediaItemId} does not exist."));
private Validation<BaseError, MediaItem> PathMustExist(MediaItem mediaItem) =>
Some(mediaItem)
.Filter(item => File.Exists(item.Path))
.ToValidation<BaseError>($"[Path] '{mediaItem.Path}' does not exist on the file system");
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private Task<Unit> RefreshStatistics(RefreshParameters parameters) =>
_localStatisticsProvider.RefreshStatistics(parameters.FFprobePath, parameters.MediaItem).ToUnit();
private record RefreshParameters(MediaItem MediaItem, string FFprobePath);
}
}
+31 -11
View File
@@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
using System;
using System.IO;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaItems
{
@@ -12,25 +13,44 @@ namespace ErsatzTV.Application.MediaItems
mediaItem.Path);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => ProjectToSearchViewModel(e),
MovieMediaItem m => ProjectToSearchViewModel(m),
_ => throw new ArgumentOutOfRangeException()
};
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(TelevisionEpisodeMediaItem mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
mediaItem.Metadata.MediaType.ToString(),
"TV Show",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(MovieMediaItem mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
"Movie",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
private static string GetDisplayTitle(this MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow &&
Optional(mediaItem.Metadata.SeasonNumber).IsSome &&
Optional(mediaItem.Metadata.EpisodeNumber).IsSome
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
: mediaItem.Metadata.Title;
private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration);
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Statistics.Duration);
private static string GetSourceName(MediaSource source) =>
source switch
@@ -1,8 +0,0 @@
using ErsatzTV.Core.Domain;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public record GetAggregateMediaItems
(MediaType MediaType, int PageNumber, int PageSize) : IRequest<AggregateMediaItemResults>;
}
@@ -1,43 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.AggregateModels;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
namespace ErsatzTV.Application.MediaItems.Queries
{
public class
GetAggregateMediaItemsHandler : IRequestHandler<GetAggregateMediaItems, AggregateMediaItemResults>
{
private readonly IMediaItemRepository _mediaItemRepository;
public GetAggregateMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
_mediaItemRepository = mediaItemRepository;
public async Task<AggregateMediaItemResults> Handle(
GetAggregateMediaItems request,
CancellationToken cancellationToken)
{
int count = await _mediaItemRepository.GetCountByType(request.MediaType);
IEnumerable<MediaItemSummary> allItems = await _mediaItemRepository.GetPageByType(
request.MediaType,
request.PageNumber,
request.PageSize);
var results = allItems
.Map(
s => new AggregateMediaItemViewModel(
s.MediaItemId,
s.Title,
s.Subtitle,
s.SortTitle,
s.Poster))
.ToList();
return new AggregateMediaItemResults(count, results);
}
}
}
@@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands
public class
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public DeleteLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository,
IMediaCollectionRepository mediaCollectionRepository)
{
public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
_mediaCollectionRepository = mediaCollectionRepository;
}
public async Task<Either<BaseError, Task>> Handle(
DeleteLocalMediaSource request,
@@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
.Map(DoDeletion)
.ToEither<Task>();
private async Task DoDeletion(LocalMediaSource mediaSource)
{
private async Task DoDeletion(LocalMediaSource mediaSource) =>
await _mediaSourceRepository.Delete(mediaSource.Id);
if (mediaSource.MediaType == MediaType.TvShow)
{
await _mediaCollectionRepository.DeleteEmptyTelevisionCollections();
}
}
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist(
DeleteLocalMediaSource deleteMediaSource) =>
@@ -1,11 +1,9 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Metadata;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record ScanLocalMediaSource(int MediaSourceId, ScanningMode ScanningMode) :
IRequest<Either<BaseError, string>>,
public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>,
IBackgroundServiceRequest;
}
@@ -16,33 +16,41 @@ namespace ErsatzTV.Application.MediaSources.Commands
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILocalMediaScanner _localMediaScanner;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository,
ILocalMediaScanner localMediaScanner,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker)
{
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_localMediaScanner = localMediaScanner;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
}
public Task<Either<BaseError, string>>
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder))
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(ScanLocalMediaSource request, RequestParameters parameters)
private async Task<Unit> PerformScan(RequestParameters parameters)
{
await _localMediaScanner.ScanLocalMediaSource(
parameters.LocalMediaSource,
parameters.FFprobePath,
request.ScanningMode);
switch (parameters.LocalMediaSource.MediaType)
{
case MediaType.Movie:
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
+10
View File
@@ -0,0 +1,10 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Movies
{
internal static class Mapper
{
internal static MovieViewModel ProjectToViewModel(MovieMediaItem movie) =>
new(movie.Metadata.Title, movie.Metadata.Year?.ToString(), movie.Metadata.Plot, movie.Poster);
}
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Movies
{
public record MovieViewModel(string Title, string Year, string Plot, string Poster);
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Movies.Queries
{
public record GetMovieById(int Id) : IRequest<Option<MovieViewModel>>;
}
@@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Movies.Mapper;
namespace ErsatzTV.Application.Movies.Queries
{
public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieViewModel>>
{
private readonly IMovieRepository _movieRepository;
public GetMovieByIdHandler(IMovieRepository movieRepository) =>
_movieRepository = movieRepository;
public Task<Option<MovieViewModel>> Handle(
GetMovieById request,
CancellationToken cancellationToken) =>
_movieRepository.GetMovie(request.Id).MapT(ProjectToViewModel);
}
}
+12 -9
View File
@@ -1,5 +1,5 @@
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
using System.IO;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts
{
@@ -22,15 +22,18 @@ namespace ErsatzTV.Application.Playouts
new(programSchedule.Id, programSchedule.Name);
private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow &&
Optional(mediaItem.Metadata.SeasonNumber).IsSome &&
Optional(mediaItem.Metadata.EpisodeNumber).IsSome
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}"
: mediaItem.Metadata.Title;
mediaItem switch
{
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration);
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Statistics.Duration);
}
}
@@ -11,7 +11,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
int MediaCollectionId,
ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
@@ -6,7 +6,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
public interface IProgramScheduleItemRequest
{
TimeSpan? StartTime { get; }
int MediaCollectionId { get; }
ProgramScheduleItemCollectionType CollectionType { get; }
int? MediaCollectionId { get; }
int? TelevisionShowId { get; }
int? TelevisionSeasonId { get; }
PlayoutMode PlayoutMode { get; }
int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; }
@@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return programSchedule;
}
protected Validation<BaseError, ProgramSchedule> CollectionTypeMustBeValid(
IProgramScheduleItemRequest item,
ProgramSchedule programSchedule)
{
switch (item.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
if (item.MediaCollectionId is null)
{
return BaseError.New("[MediaCollection] is required for collection type 'Collection'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionShow:
if (item.TelevisionShowId is null)
{
return BaseError.New("[TelevisionShow] is required for collection type 'TelevisionShow'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionSeason:
if (item.TelevisionSeasonId is null)
{
return BaseError.New("[TelevisionSeason] is required for collection type 'TelevisionSeason'");
}
break;
default:
return BaseError.New("[CollectionType] is invalid");
}
return programSchedule;
}
protected ProgramScheduleItem BuildItem(
ProgramSchedule programSchedule,
int index,
@@ -64,21 +98,30 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
},
PlayoutMode.One => new ProgramScheduleItemOne
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
},
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
Count = item.MultipleCount.GetValueOrDefault()
},
PlayoutMode.Duration => new ProgramScheduleItemDuration
@@ -86,7 +129,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id,
Index = index,
StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
OfflineTail = item.OfflineTail.GetValueOrDefault()
},
@@ -12,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
int MediaCollectionId,
ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount,
TimeSpan? PlayoutDuration,
bool? OfflineTail) : IProgramScheduleItemRequest;
@@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) =>
ProgramScheduleMustExist(request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule));
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) =>
request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule);
private Validation<BaseError, ProgramSchedule> CollectionTypesMustBeValid(
ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) =>
request.Items.Map(item => CollectionTypeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule);
}
}
@@ -17,7 +17,16 @@ namespace ErsatzTV.Application.ProgramSchedules
duration.Index,
duration.StartType,
duration.StartTime,
MediaCollections.Mapper.ProjectToViewModel(duration.MediaCollection),
duration.CollectionType,
duration.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(duration.MediaCollection)
: null,
duration.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(duration.TelevisionShow)
: null,
duration.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(duration.TelevisionSeason)
: null,
duration.PlayoutDuration,
duration.OfflineTail),
ProgramScheduleItemFlood flood =>
@@ -26,14 +35,32 @@ namespace ErsatzTV.Application.ProgramSchedules
flood.Index,
flood.StartType,
flood.StartTime,
MediaCollections.Mapper.ProjectToViewModel(flood.MediaCollection)),
flood.CollectionType,
flood.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(flood.MediaCollection)
: null,
flood.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(flood.TelevisionShow)
: null,
flood.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(flood.TelevisionSeason)
: null),
ProgramScheduleItemMultiple multiple =>
new ProgramScheduleItemMultipleViewModel(
multiple.Id,
multiple.Index,
multiple.StartType,
multiple.StartTime,
MediaCollections.Mapper.ProjectToViewModel(multiple.MediaCollection),
multiple.CollectionType,
multiple.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(multiple.MediaCollection)
: null,
multiple.TelevisionShow != null
? Television.Mapper.ProjectToViewModel(multiple.TelevisionShow)
: null,
multiple.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(multiple.TelevisionSeason)
: null,
multiple.Count),
ProgramScheduleItemOne one =>
new ProgramScheduleItemOneViewModel(
@@ -41,7 +68,14 @@ namespace ErsatzTV.Application.ProgramSchedules
one.Index,
one.StartType,
one.StartTime,
MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection)),
one.CollectionType,
one.MediaCollection != null
? MediaCollections.Mapper.ProjectToViewModel(one.MediaCollection)
: null,
one.TelevisionShow != null ? Television.Mapper.ProjectToViewModel(one.TelevisionShow) : null,
one.TelevisionSeason != null
? Television.Mapper.ProjectToViewModel(one.TelevisionSeason)
: null),
_ => throw new NotSupportedException(
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
};
@@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@@ -11,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
TimeSpan playoutDuration,
bool offlineTail) : base(
id,
@@ -19,7 +23,10 @@ namespace ErsatzTV.Application.ProgramSchedules
startType,
startTime,
PlayoutMode.Duration,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
PlayoutDuration = playoutDuration;
OfflineTail = offlineTail;
@@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base(
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id,
index,
startType,
startTime,
PlayoutMode.Flood,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
}
}
@@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@@ -11,14 +12,20 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
int count) : base(
id,
index,
startType,
startTime,
PlayoutMode.Multiple,
mediaCollection) =>
collectionType,
mediaCollection,
televisionShow,
televisionSeason) =>
Count = count;
public int Count { get; }
@@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index,
StartType startType,
TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base(
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id,
index,
startType,
startTime,
PlayoutMode.One,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
}
}
@@ -1,5 +1,6 @@
using System;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules
@@ -10,5 +11,18 @@ namespace ErsatzTV.Application.ProgramSchedules
StartType StartType,
TimeSpan? StartTime,
PlayoutMode PlayoutMode,
MediaCollectionViewModel MediaCollection);
ProgramScheduleItemCollectionType CollectionType,
MediaCollectionViewModel MediaCollection,
TelevisionShowViewModel TelevisionShow,
TelevisionSeasonViewModel TelevisionSeason)
{
public string Name => CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => MediaCollection?.Name,
ProgramScheduleItemCollectionType.TelevisionShow => $"{TelevisionShow?.Title} ({TelevisionShow?.Year})",
ProgramScheduleItemCollectionType.TelevisionSeason =>
$"{TelevisionSeason?.Title} ({TelevisionSeason?.Plot})",
_ => string.Empty
};
}
}
+28
View File
@@ -0,0 +1,28 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Television
{
internal static class Mapper
{
internal static TelevisionShowViewModel ProjectToViewModel(TelevisionShow show) =>
new(show.Id, show.Metadata.Title, show.Metadata.Year?.ToString(), show.Metadata.Plot, show.Poster);
internal static TelevisionSeasonViewModel ProjectToViewModel(TelevisionSeason season) =>
new(
season.Id,
season.TelevisionShowId,
season.TelevisionShow.Metadata.Title,
season.TelevisionShow.Metadata.Year?.ToString(),
season.Number == 0 ? "Specials" : $"Season {season.Number}",
season.Poster);
internal static TelevisionEpisodeViewModel ProjectToViewModel(TelevisionEpisodeMediaItem episode) =>
new(
episode.Season.TelevisionShowId,
episode.SeasonId,
episode.Metadata.Episode,
episode.Metadata.Title,
episode.Metadata.Plot,
episode.Poster);
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionSeasons : IRequest<List<TelevisionSeasonViewModel>>;
}
@@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class
GetAllTelevisionSeasonsHandler : IRequestHandler<GetAllTelevisionSeasons, List<TelevisionSeasonViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetAllTelevisionSeasonsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<List<TelevisionSeasonViewModel>> Handle(
GetAllTelevisionSeasons request,
CancellationToken cancellationToken) =>
_televisionRepository.GetAllSeasons().Map(list => list.Map(ProjectToViewModel).ToList());
}
}
@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionShows : IRequest<List<TelevisionShowViewModel>>;
}
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class GetAllTelevisionShowsHandler : IRequestHandler<GetAllTelevisionShows, List<TelevisionShowViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetAllTelevisionShowsHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<List<TelevisionShowViewModel>> Handle(
GetAllTelevisionShows request,
CancellationToken cancellationToken) =>
_televisionRepository.GetAllShows().Map(list => list.Map(ProjectToViewModel).ToList());
}
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionEpisodeById(int EpisodeId) : IRequest<Option<TelevisionEpisodeViewModel>>;
}
@@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class
GetTelevisionEpisodeByIdHandler : IRequestHandler<GetTelevisionEpisodeById, Option<TelevisionEpisodeViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionEpisodeByIdHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<Option<TelevisionEpisodeViewModel>> Handle(
GetTelevisionEpisodeById request,
CancellationToken cancellationToken) =>
_televisionRepository.GetEpisode(request.EpisodeId)
.MapT(ProjectToViewModel);
}
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionSeasonById(int SeasonId) : IRequest<Option<TelevisionSeasonViewModel>>;
}
@@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class
GetTelevisionSeasonByIdHandler : IRequestHandler<GetTelevisionSeasonById, Option<TelevisionSeasonViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionSeasonByIdHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<Option<TelevisionSeasonViewModel>> Handle(
GetTelevisionSeasonById request,
CancellationToken cancellationToken) =>
_televisionRepository.GetSeason(request.SeasonId)
.MapT(ProjectToViewModel);
}
}
@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionShowById(int Id) : IRequest<Option<TelevisionShowViewModel>>;
}
@@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Television.Mapper;
namespace ErsatzTV.Application.Television.Queries
{
public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>>
{
private readonly ITelevisionRepository _televisionRepository;
public GetTelevisionShowByIdHandler(ITelevisionRepository televisionRepository) =>
_televisionRepository = televisionRepository;
public Task<Option<TelevisionShowViewModel>> Handle(
GetTelevisionShowById request,
CancellationToken cancellationToken) =>
_televisionRepository.GetShow(request.Id)
.MapT(ProjectToViewModel);
}
}
@@ -0,0 +1,10 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionEpisodeViewModel(
int ShowId,
int SeasonId,
int Episode,
string Title,
string Plot,
string Poster);
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionSeasonViewModel(int Id, int ShowId, string Title, string Year, string Plot, string Poster);
}
@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster);
}
@@ -1,143 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using ErsatzTV.Api.Sdk.Api;
using ErsatzTV.Api.Sdk.Model;
using LanguageExt;
using LanguageExt.Common;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.CommandLine.Commands.MediaCollections
{
[Command("collection add-items", Description = "Ensure media collection exists and contains requested media items")]
public class MediaCollectionMediaItemsCommand : MediaItemCommandBase
{
private readonly ILogger<MediaCollectionMediaItemsCommand> _logger;
private readonly string _serverUrl;
public MediaCollectionMediaItemsCommand(
IConfiguration configuration,
ILogger<MediaCollectionMediaItemsCommand> logger)
{
_logger = logger;
_serverUrl = configuration["ServerUrl"];
}
[CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")]
public string Name { get; set; }
public override async ValueTask ExecuteAsync(IConsole console)
{
try
{
CancellationToken cancellationToken = console.GetCancellationToken();
Either<Error, List<string>> maybeFileNames = await GetFileNames();
await maybeFileNames.Match(
allFiles => SynchronizeMediaItemsToCollection(cancellationToken, allFiles),
error =>
{
_logger.LogError("{Error}", error.Message);
return Task.CompletedTask;
});
}
catch (Exception ex)
{
_logger.LogError("Unable to synchronize media items to media collection: {Error}", ex.Message);
}
}
private async Task SynchronizeMediaItemsToCollection(CancellationToken cancellationToken, List<string> allFiles)
{
Either<Error, Unit> result = await GetMediaSourceIdAsync(cancellationToken)
.BindAsync(mediaSourceId => SynchronizeMediaItemsAsync(mediaSourceId, allFiles, cancellationToken))
.BindAsync(mediaItemIds => SynchronizeMediaItemsToCollectionAsync(mediaItemIds, cancellationToken));
result.Match(
_ => _logger.LogInformation(
"Successfully synchronized {Count} media items to media collection {MediaCollection}",
allFiles.Count,
Name),
error => _logger.LogError(
"Unable to synchronize media items to media collection: {Error}",
error.Message));
}
private async Task<Either<Error, int>> GetMediaSourceIdAsync(CancellationToken cancellationToken)
{
var mediaSourcesApi = new MediaSourcesApi(_serverUrl);
List<MediaSourceViewModel> allMediaSources =
await mediaSourcesApi.ApiMediaSourcesGetAsync(cancellationToken);
Option<MediaSourceViewModel> maybeLocalMediaSource =
allMediaSources.SingleOrDefault(cs => cs.SourceType == MediaSourceType.Local);
return maybeLocalMediaSource.Match<Either<Error, int>>(
mediaSource => mediaSource.Id,
() => Error.New("Unable to find local media source"));
}
private async Task<Either<Error, List<int>>> SynchronizeMediaItemsAsync(
int mediaSourceId,
ICollection<string> fileNames,
CancellationToken cancellationToken)
{
var mediaItemsApi = new MediaItemsApi(_serverUrl);
List<MediaItemViewModel> allMediaItems = await mediaItemsApi.ApiMediaItemsGetAsync(cancellationToken);
var missingMediaItems = fileNames.Where(f => allMediaItems.All(c => c.Path != f))
.Map(f => new CreateMediaItem(mediaSourceId, f))
.ToList();
var addedIds = new List<int>();
foreach (CreateMediaItem mediaItem in missingMediaItems)
{
_logger.LogInformation("Adding media item {Path}", mediaItem.Path);
addedIds.Add(await mediaItemsApi.ApiMediaItemsPostAsync(mediaItem, cancellationToken).Map(vm => vm.Id));
}
IEnumerable<int> knownIds = allMediaItems.Where(c => fileNames.Contains(c.Path)).Map(c => c.Id);
return knownIds.Concat(addedIds).ToList();
}
private async Task<Either<Error, Unit>> SynchronizeMediaItemsToCollectionAsync(
List<int> mediaItemIds,
CancellationToken cancellationToken) =>
await EnsureMediaCollectionExistsAsync(cancellationToken)
.BindAsync(
mediaSourceId => SynchronizeMediaCollectionAsync(mediaSourceId, mediaItemIds, cancellationToken));
private async Task<Either<Error, int>> EnsureMediaCollectionExistsAsync(CancellationToken cancellationToken)
{
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
Option<MediaCollectionViewModel> maybeExisting = await mediaCollectionsApi
.ApiMediaCollectionsGetAsync(cancellationToken)
.Map(list => list.SingleOrDefault(mc => mc.Name == Name));
return await maybeExisting.Match(
existing => Task.FromResult(existing.Id),
async () =>
{
var data = new CreateSimpleMediaCollection(Name);
return await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken)
.Map(vm => vm.Id);
});
}
private async Task<Either<Error, Unit>> SynchronizeMediaCollectionAsync(
int mediaCollectionId,
List<int> mediaItemIds,
CancellationToken cancellationToken)
{
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
await mediaCollectionsApi.ApiMediaCollectionsIdItemsPutAsync(
mediaCollectionId,
mediaItemIds,
cancellationToken);
return unit;
}
}
}
@@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using LanguageExt;
using LanguageExt.Common;
namespace ErsatzTV.CommandLine.Commands
{
public abstract class MediaItemCommandBase : ICommand
{
[CommandOption("folder", 'f', Description = "Folder to search for media items")]
public string Folder { get; set; }
[CommandOption("pattern", 'p', Description = "File search pattern")]
public string SearchPattern { get; set; }
public abstract ValueTask ExecuteAsync(IConsole console);
protected async Task<Either<Error, List<string>>> GetFileNames()
{
if (Console.IsInputRedirected)
{
await using Stream standardInput = Console.OpenStandardInput();
using var streamReader = new StreamReader(standardInput);
string input = await streamReader.ReadToEndAsync();
return input.Trim().Split("\n").Map(s => s.Trim()).ToList();
}
if (string.IsNullOrWhiteSpace(Folder) || string.IsNullOrWhiteSpace(SearchPattern))
{
return Error.New(
"--folder and --pattern are required when file names are not passed on standard input");
}
return Directory.GetFiles(Folder, SearchPattern, SearchOption.AllDirectories).ToList();
}
}
}
@@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using ErsatzTV.Api.Sdk.Api;
using ErsatzTV.Api.Sdk.Model;
using LanguageExt;
using LanguageExt.Common;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.CommandLine.Commands
{
[Command("items", Description = "Ensure media items exist")]
public class MediaItemsCommand : MediaItemCommandBase
{
private readonly ILogger<MediaItemsCommand> _logger;
private readonly string _serverUrl;
public MediaItemsCommand(IConfiguration configuration, ILogger<MediaItemsCommand> logger)
{
_logger = logger;
_serverUrl = configuration["ServerUrl"];
}
public override async ValueTask ExecuteAsync(IConsole console)
{
try
{
CancellationToken cancellationToken = console.GetCancellationToken();
Either<Error, List<string>> maybeFileNames = await GetFileNames();
await maybeFileNames.Match(
allFiles => SynchronizeMediaItems(cancellationToken, allFiles),
error =>
{
_logger.LogError("{Error}", error.Message);
return Task.CompletedTask;
});
}
catch (Exception ex)
{
_logger.LogError("Unable to synchronize media items: {Error}", ex.Message);
}
}
private async Task SynchronizeMediaItems(CancellationToken cancellationToken, List<string> allFiles)
{
Either<Error, Unit> result = await GetMediaSourceId(cancellationToken)
.BindAsync(
contentSourceId => PostMediaItems(
contentSourceId,
allFiles,
cancellationToken));
result.Match(
_ => _logger.LogInformation(
"Successfully synchronized {Count} media items",
allFiles.Count),
error => _logger.LogError("Unable to synchronize media items: {Error}", error.Message));
}
private async Task<Either<Error, int>> GetMediaSourceId(CancellationToken cancellationToken)
{
var mediaSourcesApi = new MediaSourcesApi(_serverUrl);
List<MediaSourceViewModel> allMediaSources =
await mediaSourcesApi.ApiMediaSourcesGetAsync(cancellationToken);
Option<MediaSourceViewModel> maybeLocalMediaSource =
allMediaSources.SingleOrDefault(cs => cs.SourceType == MediaSourceType.Local);
return maybeLocalMediaSource.Match<Either<Error, int>>(
mediaSource => mediaSource.Id,
() => Error.New("Unable to find local media source"));
}
private async Task<Either<Error, Unit>> PostMediaItems(
int mediaSourceId,
ICollection<string> fileNames,
CancellationToken cancellationToken)
{
var mediaItemsApi = new MediaItemsApi(_serverUrl);
List<MediaItemViewModel> allContent = await mediaItemsApi.ApiMediaItemsGetAsync(cancellationToken);
var missingMediaItems = fileNames.Where(f => allContent.All(c => c.Path != f))
.Map(f => new CreateMediaItem(mediaSourceId, f))
.ToList();
foreach (CreateMediaItem mediaItem in missingMediaItems)
{
_logger.LogInformation("Adding media item {Path}", mediaItem.Path);
await mediaItemsApi.ApiMediaItemsPostAsync(mediaItem, cancellationToken);
}
return unit;
}
}
}
@@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
{
MediaItem = new MediaItem
{
Metadata = new MediaMetadata()
Statistics = new MediaItemStatistics()
}
};
@@ -175,9 +175,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -198,9 +198,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -221,9 +221,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -245,9 +245,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -269,9 +269,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
@@ -295,9 +295,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -323,10 +323,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -352,10 +352,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
@@ -380,10 +380,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "libx264";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "libx264";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -409,10 +409,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -437,9 +437,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -464,10 +464,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -492,9 +492,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -520,10 +520,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video";
playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -546,7 +546,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "aac";
playoutItem.MediaItem.Statistics.AudioCodec = "aac";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -567,7 +567,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -588,7 +588,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -609,7 +609,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming,
@@ -630,7 +630,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -651,7 +651,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -674,7 +674,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -697,7 +697,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -719,7 +719,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -741,7 +741,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
};
PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3";
playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream,
@@ -0,0 +1,9 @@
using System;
namespace ErsatzTV.Core.Tests.Fakes
{
public record FakeFileEntry(string Path)
{
public DateTime LastWriteTime { get; set; } = DateTime.MinValue;
}
}

Some files were not shown because too many files have changed in this diff Show More