Browse Source

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
pull/28/head v0.0.10-prealpha
Jason Dove 4 years ago committed by GitHub
parent
commit
871a031467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .github/workflows/release.yml
  2. 9
      ErsatzTV.Application/IMediaCard.cs
  3. 60
      ErsatzTV.Application/MediaCards/Mapper.cs
  4. 4
      ErsatzTV.Application/MediaCards/MediaCardViewModel.cs
  5. 6
      ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs
  6. 11
      ErsatzTV.Application/MediaCards/MovieCardViewModel.cs
  7. 6
      ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs
  8. 32
      ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs
  9. 9
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs
  10. 26
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs
  11. 7
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs
  12. 34
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs
  13. 7
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs
  14. 34
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs
  15. 6
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs
  16. 33
      ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs
  17. 11
      ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs
  18. 6
      ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs
  19. 21
      ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs
  20. 6
      ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs
  21. 19
      ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs
  22. 6
      ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs
  23. 11
      ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs
  24. 9
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs
  25. 79
      ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs
  26. 8
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs
  27. 62
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs
  28. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs
  29. 68
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs
  30. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs
  31. 68
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs
  32. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs
  33. 67
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs
  34. 10
      ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs
  35. 15
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs
  36. 74
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs
  37. 11
      ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs
  38. 65
      ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs
  39. 11
      ErsatzTV.Application/MediaCollections/Mapper.cs
  40. 10
      ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs
  41. 7
      ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs
  42. 27
      ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs
  43. 11
      ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs
  44. 37
      ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs
  45. 6
      ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs
  46. 9
      ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs
  47. 8
      ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs
  48. 101
      ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs
  49. 9
      ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs
  50. 31
      ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs
  51. 8
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs
  52. 9
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs
  53. 46
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs
  54. 9
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs
  55. 53
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs
  56. 9
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs
  57. 45
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs
  58. 9
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs
  59. 65
      ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs
  60. 42
      ErsatzTV.Application/MediaItems/Mapper.cs
  61. 8
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs
  62. 43
      ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs
  63. 16
      ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs
  64. 4
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  65. 26
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  66. 10
      ErsatzTV.Application/Movies/Mapper.cs
  67. 4
      ErsatzTV.Application/Movies/MovieViewModel.cs
  68. 7
      ErsatzTV.Application/Movies/Queries/GetMovieById.cs
  69. 22
      ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs
  70. 21
      ErsatzTV.Application/Playouts/Mapper.cs
  71. 5
      ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs
  72. 5
      ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs
  73. 50
      ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs
  74. 5
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs
  75. 9
      ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs
  76. 42
      ErsatzTV.Application/ProgramSchedules/Mapper.cs
  77. 9
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs
  78. 11
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs
  79. 9
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs
  80. 11
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs
  81. 16
      ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs
  82. 28
      ErsatzTV.Application/Television/Mapper.cs
  83. 7
      ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs
  84. 25
      ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs
  85. 7
      ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs
  86. 24
      ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs
  87. 7
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs
  88. 24
      ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs
  89. 7
      ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs
  90. 24
      ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs
  91. 7
      ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs
  92. 23
      ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs
  93. 10
      ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs
  94. 4
      ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs
  95. 4
      ErsatzTV.Application/Television/TelevisionShowViewModel.cs
  96. 143
      ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs
  97. 42
      ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs
  98. 99
      ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs
  99. 118
      ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs
  100. 9
      ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs
  101. Some files were not shown because too many files have changed in this diff Show More

10
.github/workflows/release.yml

@ -39,24 +39,24 @@ jobs: @@ -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
ErsatzTV.Application/IMediaCard.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application
{
public interface IMediaCard
{
string Title { get; }
string SortTitle { get; }
string Subtitle { get; }
}
}

60
ErsatzTV.Application/MediaCards/Mapper.cs

@ -0,0 +1,60 @@ @@ -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}";
}
}

4
ErsatzTV.Application/MediaCards/MediaCardViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.MediaCards
{
public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster);
}

6
ErsatzTV.Application/MediaCards/MovieCardResultsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards);
}

11
ErsatzTV.Application/MediaCards/MovieCardViewModel.cs

@ -0,0 +1,11 @@ @@ -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)
{
}
}

6
ErsatzTV.Application/MediaCards/Queries/GetMovieCards.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetMovieCards(int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
}

32
ErsatzTV.Application/MediaCards/Queries/GetMovieCardsHandler.cs

@ -0,0 +1,32 @@ @@ -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);
}
}
}

9
ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs

@ -0,0 +1,9 @@ @@ -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>>;
}

26
ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs

@ -0,0 +1,26 @@ @@ -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);
}
}

7
ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCards.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionEpisodeCards
(int TelevisionSeasonId, int PageNumber, int PageSize) : IRequest<TelevisionEpisodeCardResultsViewModel>;
}

34
ErsatzTV.Application/MediaCards/Queries/GetTelevisionEpisodeCardsHandler.cs

@ -0,0 +1,34 @@ @@ -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);
}
}
}

7
ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCards.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionSeasonCards
(int TelevisionShowId, int PageNumber, int PageSize) : IRequest<TelevisionSeasonCardResultsViewModel>;
}

34
ErsatzTV.Application/MediaCards/Queries/GetTelevisionSeasonCardsHandler.cs

@ -0,0 +1,34 @@ @@ -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);
}
}
}

6
ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCards.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetTelevisionShowCards(int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
}

33
ErsatzTV.Application/MediaCards/Queries/GetTelevisionShowCardsHandler.cs

@ -0,0 +1,33 @@ @@ -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);
}
}
}

11
ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs

@ -0,0 +1,11 @@ @@ -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);
}

6
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardResultsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionEpisodeCardResultsViewModel(int Count, List<TelevisionEpisodeCardViewModel> Cards);
}

21
ErsatzTV.Application/MediaCards/TelevisionEpisodeCardViewModel.cs

@ -0,0 +1,21 @@ @@ -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)
{
}
}

6
ErsatzTV.Application/MediaCards/TelevisionSeasonCardResultsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionSeasonCardResultsViewModel(int Count, List<TelevisionSeasonCardViewModel> Cards);
}

19
ErsatzTV.Application/MediaCards/TelevisionSeasonCardViewModel.cs

@ -0,0 +1,19 @@ @@ -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)
{
}
}

6
ErsatzTV.Application/MediaCards/TelevisionShowCardResultsViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaCards
{
public record TelevisionShowCardResultsViewModel(int Count, List<TelevisionShowCardViewModel> Cards);
}

11
ErsatzTV.Application/MediaCards/TelevisionShowCardViewModel.cs

@ -0,0 +1,11 @@ @@ -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)
{
}
}

9
ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollection.cs

@ -1,9 +0,0 @@ @@ -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>>;
}

79
ErsatzTV.Application/MediaCollections/Commands/AddItemsToSimpleMediaCollectionHandler.cs

@ -1,79 +0,0 @@ @@ -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);
}
}

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

@ -0,0 +1,8 @@ @@ -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>>;
}

62
ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs

@ -0,0 +1,62 @@ @@ -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);
}
}

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

@ -0,0 +1,8 @@ @@ -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>>;
}

68
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs

@ -0,0 +1,68 @@ @@ -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);
}
}

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

@ -0,0 +1,8 @@ @@ -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>>;
}

68
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs

@ -0,0 +1,68 @@ @@ -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);
}
}

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

@ -0,0 +1,8 @@ @@ -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>>;
}

67
ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs

@ -0,0 +1,67 @@ @@ -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);
}
}

10
ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs

@ -29,7 +29,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -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)
{

15
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs

@ -0,0 +1,15 @@ @@ -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();
}
}

74
ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs

@ -0,0 +1,74 @@ @@ -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."));
}
}

11
ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItems.cs

@ -1,11 +0,0 @@ @@ -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>>>;
}

65
ErsatzTV.Application/MediaCollections/Commands/ReplaceSimpleMediaCollectionItemsHandler.cs

@ -1,65 +0,0 @@ @@ -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();
}
}
}

11
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -1,5 +1,4 @@ @@ -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 @@ -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);
}
}

10
ErsatzTV.Application/MediaCollections/MediaCollectionViewModel.cs

@ -1,4 +1,10 @@ @@ -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);
}

7
ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummaries.cs

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetMediaCollectionSummaries(string SearchString) : IRequest<List<MediaCollectionSummaryViewModel>>;
}

27
ErsatzTV.Application/MediaCollections/Queries/GetMediaCollectionSummariesHandler.cs

@ -1,27 +0,0 @@ @@ -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());
}
}

11
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsById.cs

@ -1,11 +0,0 @@ @@ -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>>>>;
}

37
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionWithItemsByIdHandler.cs

@ -1,37 +0,0 @@ @@ -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);
}
}
}

6
ErsatzTV.Application/MediaItems/AggregateMediaItemResults.cs

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
using System.Collections.Generic;
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemResults(int Count, List<AggregateMediaItemViewModel> DataPage);
}

9
ErsatzTV.Application/MediaItems/AggregateMediaItemViewModel.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems
{
public record AggregateMediaItemViewModel(
int MediaItemId,
string Title,
string Subtitle,
string SortTitle,
string Poster);
}

8
ErsatzTV.Application/MediaItems/Commands/CreateMediaItem.cs

@ -1,8 +0,0 @@ @@ -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>>;
}

101
ErsatzTV.Application/MediaItems/Commands/CreateMediaItemHandler.cs

@ -1,101 +0,0 @@ @@ -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);
}
}

9
ErsatzTV.Application/MediaItems/Commands/DeleteMediaItem.cs

@ -1,9 +0,0 @@ @@ -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>>;
}

31
ErsatzTV.Application/MediaItems/Commands/DeleteMediaItemHandler.cs

@ -1,31 +0,0 @@ @@ -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);
}
}

8
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItem.cs

@ -1,8 +0,0 @@ @@ -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;
}

9
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollections.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemCollections : RefreshMediaItem
{
public RefreshMediaItemCollections(int mediaItemId) : base(mediaItemId)
{
}
}
}

46
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemCollectionsHandler.cs

@ -1,46 +0,0 @@ @@ -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();
}
}

9
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadata.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemMetadata : RefreshMediaItem
{
public RefreshMediaItemMetadata(int mediaItemId) : base(mediaItemId)
{
}
}
}

53
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemMetadataHandler.cs

@ -1,53 +0,0 @@ @@ -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();
}
}

9
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPoster.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemPoster : RefreshMediaItem
{
public RefreshMediaItemPoster(int mediaItemId) : base(mediaItemId)
{
}
}
}

45
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemPosterHandler.cs

@ -1,45 +0,0 @@ @@ -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();
}
}

9
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatistics.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
namespace ErsatzTV.Application.MediaItems.Commands
{
public record RefreshMediaItemStatistics : RefreshMediaItem
{
public RefreshMediaItemStatistics(int mediaItemId) : base(mediaItemId)
{
}
}
}

65
ErsatzTV.Application/MediaItems/Commands/RefreshMediaItemStatisticsHandler.cs

@ -1,65 +0,0 @@ @@ -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);
}
}

42
ErsatzTV.Application/MediaItems/Mapper.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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

8
ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItems.cs

@ -1,8 +0,0 @@ @@ -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>;
}

43
ErsatzTV.Application/MediaItems/Queries/GetAggregateMediaItemsHandler.cs

@ -1,43 +0,0 @@ @@ -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);
}
}
}

16
ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs

@ -12,16 +12,10 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -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 @@ -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) =>

4
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs

@ -1,11 +1,9 @@ @@ -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;
}

26
ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs

@ -16,33 +16,41 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -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
ErsatzTV.Application/Movies/Mapper.cs

@ -0,0 +1,10 @@ @@ -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);
}
}

4
ErsatzTV.Application/Movies/MovieViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Movies
{
public record MovieViewModel(string Title, string Year, string Plot, string Poster);
}

7
ErsatzTV.Application/Movies/Queries/GetMovieById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Movies.Queries
{
public record GetMovieById(int Id) : IRequest<Option<MovieViewModel>>;
}

22
ErsatzTV.Application/Movies/Queries/GetMovieByIdHandler.cs

@ -0,0 +1,22 @@ @@ -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);
}
}

21
ErsatzTV.Application/Playouts/Mapper.cs

@ -1,5 +1,5 @@ @@ -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 @@ -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);
}
}

5
ErsatzTV.Application/ProgramSchedules/Commands/AddProgramScheduleItem.cs

@ -11,7 +11,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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;

5
ErsatzTV.Application/ProgramSchedules/Commands/IProgramScheduleItemRequest.cs

@ -6,7 +6,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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; }

50
ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs

@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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 @@ -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 @@ -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()
},

5
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItems.cs

@ -12,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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;

9
ErsatzTV.Application/ProgramSchedules/Commands/ReplaceProgramScheduleItemsHandler.cs

@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands @@ -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);
}
}

42
ErsatzTV.Application/ProgramSchedules/Mapper.cs

@ -17,7 +17,16 @@ namespace ErsatzTV.Application.ProgramSchedules @@ -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 @@ -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 @@ -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}")
};

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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 @@ -19,7 +23,10 @@ namespace ErsatzTV.Application.ProgramSchedules
startType,
startTime,
PlayoutMode.Duration,
mediaCollection)
collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{
PlayoutDuration = playoutDuration;
OfflineTail = offlineTail;

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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)
{
}
}

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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; }

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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)
{
}
}

16
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -1,5 +1,6 @@ @@ -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 @@ -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
ErsatzTV.Application/Television/Mapper.cs

@ -0,0 +1,28 @@ @@ -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);
}
}

7
ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasons.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionSeasons : IRequest<List<TelevisionSeasonViewModel>>;
}

25
ErsatzTV.Application/Television/Queries/GetAllTelevisionSeasonsHandler.cs

@ -0,0 +1,25 @@ @@ -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());
}
}

7
ErsatzTV.Application/Television/Queries/GetAllTelevisionShows.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetAllTelevisionShows : IRequest<List<TelevisionShowViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetAllTelevisionShowsHandler.cs

@ -0,0 +1,24 @@ @@ -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());
}
}

7
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionEpisodeById(int EpisodeId) : IRequest<Option<TelevisionEpisodeViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetTelevisionEpisodeByIdHandler.cs

@ -0,0 +1,24 @@ @@ -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);
}
}

7
ErsatzTV.Application/Television/Queries/GetTelevisionSeasonById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionSeasonById(int SeasonId) : IRequest<Option<TelevisionSeasonViewModel>>;
}

24
ErsatzTV.Application/Television/Queries/GetTelevisionSeasonByIdHandler.cs

@ -0,0 +1,24 @@ @@ -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);
}
}

7
ErsatzTV.Application/Television/Queries/GetTelevisionShowById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Television.Queries
{
public record GetTelevisionShowById(int Id) : IRequest<Option<TelevisionShowViewModel>>;
}

23
ErsatzTV.Application/Television/Queries/GetTelevisionShowByIdHandler.cs

@ -0,0 +1,23 @@ @@ -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);
}
}

10
ErsatzTV.Application/Television/TelevisionEpisodeViewModel.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionEpisodeViewModel(
int ShowId,
int SeasonId,
int Episode,
string Title,
string Plot,
string Poster);
}

4
ErsatzTV.Application/Television/TelevisionSeasonViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionSeasonViewModel(int Id, int ShowId, string Title, string Year, string Plot, string Poster);
}

4
ErsatzTV.Application/Television/TelevisionShowViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Television
{
public record TelevisionShowViewModel(int Id, string Title, string Year, string Plot, string Poster);
}

143
ErsatzTV.CommandLine/Commands/MediaCollections/MediaCollectionMediaItemsCommand.cs

@ -1,143 +0,0 @@ @@ -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;
}
}
}

42
ErsatzTV.CommandLine/Commands/MediaItemCommandBase.cs

@ -1,42 +0,0 @@ @@ -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();
}
}
}

99
ErsatzTV.CommandLine/Commands/MediaItemsCommand.cs

@ -1,99 +0,0 @@ @@ -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;
}
}
}

118
ErsatzTV.Core.Tests/FFmpeg/FFmpegPlaybackSettingsServiceTests.cs

@ -20,7 +20,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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,

9
ErsatzTV.Core.Tests/Fakes/FakeFileEntry.cs

@ -0,0 +1,9 @@ @@ -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

Loading…
Cancel
Save