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:
# Define some variables for things we need # Define some variables for things we need
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}" release_name="ErsatzTV-$tag-${{ matrix.target }}"
release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}" #release_name_cli="ErsatzTV.CommandLine-$tag-${{ matrix.target }}"
# Build everything # 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/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 # Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*" 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 else
tar czvf "${release_name}.tar.gz" "$release_name" 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 fi
# Delete output directory # Delete output directory
rm -r "$release_name" rm -r "$release_name"
rm -r "$release_name_cli" #rm -r "$release_name_cli"
- name: Publish - name: Publish
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1

9
ErsatzTV.Application/IMediaCard.cs

@ -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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel); _mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) => 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) private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection)
{ {

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

@ -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 @@
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 @@
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 @@
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 @@
using ErsatzTV.Core.AggregateModels; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaCollections namespace ErsatzTV.Application.MediaCollections
{ {
@ -7,13 +6,5 @@ namespace ErsatzTV.Application.MediaCollections
{ {
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) => internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) =>
new(mediaCollection.Id, mediaCollection.Name); 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
using ErsatzTV.Core.Domain; using System;
using static LanguageExt.Prelude; using System.IO;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaItems namespace ErsatzTV.Application.MediaItems
{ {
@ -12,25 +13,44 @@ namespace ErsatzTV.Application.MediaItems
mediaItem.Path); mediaItem.Path);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) => 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( new(
mediaItem.Id, mediaItem.Id,
GetSourceName(mediaItem.Source), 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), GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem)); GetDisplayDuration(mediaItem));
private static string GetDisplayTitle(this MediaItem mediaItem) => private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow && mediaItem switch
Optional(mediaItem.Metadata.SeasonNumber).IsSome && {
Optional(mediaItem.Metadata.EpisodeNumber).IsSome TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}" ? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: mediaItem.Metadata.Title; : Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) => private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format( string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration); mediaItem.Statistics.Duration);
private static string GetSourceName(MediaSource source) => private static string GetSourceName(MediaSource source) =>
source switch source switch

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

@ -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 @@
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
public class public class
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>> DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>>
{ {
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
public DeleteLocalMediaSourceHandler( public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
IMediaSourceRepository mediaSourceRepository,
IMediaCollectionRepository mediaCollectionRepository)
{
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_mediaCollectionRepository = mediaCollectionRepository;
}
public async Task<Either<BaseError, Task>> Handle( public async Task<Either<BaseError, Task>> Handle(
DeleteLocalMediaSource request, DeleteLocalMediaSource request,
@ -30,14 +24,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
.Map(DoDeletion) .Map(DoDeletion)
.ToEither<Task>(); .ToEither<Task>();
private async Task DoDeletion(LocalMediaSource mediaSource) private async Task DoDeletion(LocalMediaSource mediaSource) =>
{
await _mediaSourceRepository.Delete(mediaSource.Id); await _mediaSourceRepository.Delete(mediaSource.Id);
if (mediaSource.MediaType == MediaType.TvShow)
{
await _mediaCollectionRepository.DeleteEmptyTelevisionCollections();
}
}
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist( private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist(
DeleteLocalMediaSource deleteMediaSource) => DeleteLocalMediaSource deleteMediaSource) =>

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

@ -1,11 +1,9 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Metadata;
using LanguageExt; using LanguageExt;
using MediatR; using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands namespace ErsatzTV.Application.MediaSources.Commands
{ {
public record ScanLocalMediaSource(int MediaSourceId, ScanningMode ScanningMode) : public record ScanLocalMediaSource(int MediaSourceId) : IRequest<Either<BaseError, string>>,
IRequest<Either<BaseError, string>>,
IBackgroundServiceRequest; IBackgroundServiceRequest;
} }

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

@ -16,33 +16,41 @@ namespace ErsatzTV.Application.MediaSources.Commands
{ {
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
private readonly ILocalMediaScanner _localMediaScanner;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalMediaSourceHandler( public ScanLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository, IConfigElementRepository configElementRepository,
ILocalMediaScanner localMediaScanner, IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker) IEntityLocker entityLocker)
{ {
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
_localMediaScanner = localMediaScanner; _movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker; _entityLocker = entityLocker;
} }
public Task<Either<BaseError, string>> public Task<Either<BaseError, string>>
Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) => Handle(ScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(parameters => PerformScan(request, parameters).Map(_ => parameters.LocalMediaSource.Folder)) .MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(ScanLocalMediaSource request, RequestParameters parameters) private async Task<Unit> PerformScan(RequestParameters parameters)
{ {
await _localMediaScanner.ScanLocalMediaSource( switch (parameters.LocalMediaSource.MediaType)
parameters.LocalMediaSource, {
parameters.FFprobePath, case MediaType.Movie:
request.ScanningMode); await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id); _entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);

10
ErsatzTV.Application/Movies/Mapper.cs

@ -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 @@
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 @@
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 @@
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 @@
using ErsatzTV.Core.Domain; using System.IO;
using static LanguageExt.Prelude; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts namespace ErsatzTV.Application.Playouts
{ {
@ -22,15 +22,18 @@ namespace ErsatzTV.Application.Playouts
new(programSchedule.Id, programSchedule.Name); new(programSchedule.Id, programSchedule.Name);
private static string GetDisplayTitle(MediaItem mediaItem) => private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem.Metadata.MediaType == MediaType.TvShow && mediaItem switch
Optional(mediaItem.Metadata.SeasonNumber).IsSome && {
Optional(mediaItem.Metadata.EpisodeNumber).IsSome TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{mediaItem.Metadata.Title} s{mediaItem.Metadata.SeasonNumber:00}e{mediaItem.Metadata.EpisodeNumber:00}" ? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: mediaItem.Metadata.Title; : Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) => private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format( string.Format(
mediaItem.Metadata.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}", mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Metadata.Duration); mediaItem.Statistics.Duration);
} }
} }

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

@ -11,7 +11,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType, StartType StartType,
TimeSpan? StartTime, TimeSpan? StartTime,
PlayoutMode PlayoutMode, PlayoutMode PlayoutMode,
int MediaCollectionId, ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount, int? MultipleCount,
TimeSpan? PlayoutDuration, TimeSpan? PlayoutDuration,
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest; bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;

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

@ -6,7 +6,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
public interface IProgramScheduleItemRequest public interface IProgramScheduleItemRequest
{ {
TimeSpan? StartTime { get; } TimeSpan? StartTime { get; }
int MediaCollectionId { get; } ProgramScheduleItemCollectionType CollectionType { get; }
int? MediaCollectionId { get; }
int? TelevisionShowId { get; }
int? TelevisionSeasonId { get; }
PlayoutMode PlayoutMode { get; } PlayoutMode PlayoutMode { get; }
int? MultipleCount { get; } int? MultipleCount { get; }
TimeSpan? PlayoutDuration { get; } TimeSpan? PlayoutDuration { get; }

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

@ -53,6 +53,40 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
return programSchedule; 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( protected ProgramScheduleItem BuildItem(
ProgramSchedule programSchedule, ProgramSchedule programSchedule,
int index, int index,
@ -64,21 +98,30 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
Index = index, Index = index,
StartTime = item.StartTime, StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
}, },
PlayoutMode.One => new ProgramScheduleItemOne PlayoutMode.One => new ProgramScheduleItemOne
{ {
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
Index = index, Index = index,
StartTime = item.StartTime, StartTime = item.StartTime,
MediaCollectionId = item.MediaCollectionId CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId
}, },
PlayoutMode.Multiple => new ProgramScheduleItemMultiple PlayoutMode.Multiple => new ProgramScheduleItemMultiple
{ {
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
Index = index, Index = index,
StartTime = item.StartTime, StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId, MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
Count = item.MultipleCount.GetValueOrDefault() Count = item.MultipleCount.GetValueOrDefault()
}, },
PlayoutMode.Duration => new ProgramScheduleItemDuration PlayoutMode.Duration => new ProgramScheduleItemDuration
@ -86,7 +129,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
Index = index, Index = index,
StartTime = item.StartTime, StartTime = item.StartTime,
CollectionType = item.CollectionType,
MediaCollectionId = item.MediaCollectionId, MediaCollectionId = item.MediaCollectionId,
TelevisionShowId = item.TelevisionShowId,
TelevisionSeasonId = item.TelevisionSeasonId,
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(), PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
OfflineTail = item.OfflineTail.GetValueOrDefault() OfflineTail = item.OfflineTail.GetValueOrDefault()
}, },

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

@ -12,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
StartType StartType, StartType StartType,
TimeSpan? StartTime, TimeSpan? StartTime,
PlayoutMode PlayoutMode, PlayoutMode PlayoutMode,
int MediaCollectionId, ProgramScheduleItemCollectionType CollectionType,
int? MediaCollectionId,
int? TelevisionShowId,
int? TelevisionSeasonId,
int? MultipleCount, int? MultipleCount,
TimeSpan? PlayoutDuration, TimeSpan? PlayoutDuration,
bool? OfflineTail) : IProgramScheduleItemRequest; bool? OfflineTail) : IProgramScheduleItemRequest;

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

@ -55,12 +55,19 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) => private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) =>
ProgramScheduleMustExist(request.ProgramScheduleId) ProgramScheduleMustExist(request.ProgramScheduleId)
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule)); .BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid( private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
ReplaceProgramScheduleItems request, ReplaceProgramScheduleItems request,
ProgramSchedule programSchedule) => ProgramSchedule programSchedule) =>
request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence() request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence()
.Map(_ => programSchedule); .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
duration.Index, duration.Index,
duration.StartType, duration.StartType,
duration.StartTime, 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.PlayoutDuration,
duration.OfflineTail), duration.OfflineTail),
ProgramScheduleItemFlood flood => ProgramScheduleItemFlood flood =>
@ -26,14 +35,32 @@ namespace ErsatzTV.Application.ProgramSchedules
flood.Index, flood.Index,
flood.StartType, flood.StartType,
flood.StartTime, 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 => ProgramScheduleItemMultiple multiple =>
new ProgramScheduleItemMultipleViewModel( new ProgramScheduleItemMultipleViewModel(
multiple.Id, multiple.Id,
multiple.Index, multiple.Index,
multiple.StartType, multiple.StartType,
multiple.StartTime, 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), multiple.Count),
ProgramScheduleItemOne one => ProgramScheduleItemOne one =>
new ProgramScheduleItemOneViewModel( new ProgramScheduleItemOneViewModel(
@ -41,7 +68,14 @@ namespace ErsatzTV.Application.ProgramSchedules
one.Index, one.Index,
one.StartType, one.StartType,
one.StartTime, 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( _ => throw new NotSupportedException(
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}") $"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
}; };

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemDurationViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
@ -11,7 +12,10 @@ namespace ErsatzTV.Application.ProgramSchedules
int index, int index,
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection, MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
TimeSpan playoutDuration, TimeSpan playoutDuration,
bool offlineTail) : base( bool offlineTail) : base(
id, id,
@ -19,7 +23,10 @@ namespace ErsatzTV.Application.ProgramSchedules
startType, startType,
startTime, startTime,
PlayoutMode.Duration, PlayoutMode.Duration,
mediaCollection) collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{ {
PlayoutDuration = playoutDuration; PlayoutDuration = playoutDuration;
OfflineTail = offlineTail; OfflineTail = offlineTail;

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemFloodViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index, int index,
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base( ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id, id,
index, index,
startType, startType,
startTime, startTime,
PlayoutMode.Flood, PlayoutMode.Flood,
mediaCollection) collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{ {
} }
} }

9
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemMultipleViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
@ -11,14 +12,20 @@ namespace ErsatzTV.Application.ProgramSchedules
int index, int index,
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection, MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason,
int count) : base( int count) : base(
id, id,
index, index,
startType, startType,
startTime, startTime,
PlayoutMode.Multiple, PlayoutMode.Multiple,
mediaCollection) => collectionType,
mediaCollection,
televisionShow,
televisionSeason) =>
Count = count; Count = count;
public int Count { get; } public int Count { get; }

11
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemOneViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
@ -11,13 +12,19 @@ namespace ErsatzTV.Application.ProgramSchedules
int index, int index,
StartType startType, StartType startType,
TimeSpan? startTime, TimeSpan? startTime,
MediaCollectionViewModel mediaCollection) : base( ProgramScheduleItemCollectionType collectionType,
MediaCollectionViewModel mediaCollection,
TelevisionShowViewModel televisionShow,
TelevisionSeasonViewModel televisionSeason) : base(
id, id,
index, index,
startType, startType,
startTime, startTime,
PlayoutMode.One, PlayoutMode.One,
mediaCollection) collectionType,
mediaCollection,
televisionShow,
televisionSeason)
{ {
} }
} }

16
ErsatzTV.Application/ProgramSchedules/ProgramScheduleItemViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using ErsatzTV.Application.MediaCollections; using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Television;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.ProgramSchedules namespace ErsatzTV.Application.ProgramSchedules
@ -10,5 +11,18 @@ namespace ErsatzTV.Application.ProgramSchedules
StartType StartType, StartType StartType,
TimeSpan? StartTime, TimeSpan? StartTime,
PlayoutMode PlayoutMode, 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
{ {
MediaItem = new MediaItem MediaItem = new MediaItem
{ {
Metadata = new MediaMetadata() Statistics = new MediaItemStatistics()
} }
}; };
@ -175,9 +175,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -198,9 +198,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -221,9 +221,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -245,9 +245,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -269,9 +269,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
@ -295,9 +295,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -323,10 +323,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -352,10 +352,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
@ -380,10 +380,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "libx264"; playoutItem.MediaItem.Statistics.VideoCodec = "libx264";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -409,10 +409,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -437,9 +437,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -464,10 +464,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -492,9 +492,9 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1918; playoutItem.MediaItem.Statistics.Width = 1918;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -520,10 +520,10 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.Width = 1920; playoutItem.MediaItem.Statistics.Width = 1920;
playoutItem.MediaItem.Metadata.Height = 1080; playoutItem.MediaItem.Statistics.Height = 1080;
playoutItem.MediaItem.Metadata.SampleAspectRatio = "1:1"; // not anamorphic playoutItem.MediaItem.Statistics.SampleAspectRatio = "1:1"; // not anamorphic
playoutItem.MediaItem.Metadata.VideoCodec = "mpeg2video"; playoutItem.MediaItem.Statistics.VideoCodec = "mpeg2video";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -546,7 +546,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "aac"; playoutItem.MediaItem.Statistics.AudioCodec = "aac";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -567,7 +567,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -588,7 +588,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -609,7 +609,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreaming, StreamingMode.HttpLiveStreaming,
@ -630,7 +630,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -651,7 +651,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -674,7 +674,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -697,7 +697,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -719,7 +719,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,
@ -741,7 +741,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}; };
PlayoutItem playoutItem = EmptyPlayoutItem(); PlayoutItem playoutItem = EmptyPlayoutItem();
playoutItem.MediaItem.Metadata.AudioCodec = "ac3"; playoutItem.MediaItem.Statistics.AudioCodec = "ac3";
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.TransportStream, StreamingMode.TransportStream,

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

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