Browse Source

Database redesign (#31)

* starting database redesign

* set season and episode numbers

* use datetimes in db (utc); update movie metadata

* get movie cards from new table

* copy show/episode metadata

* remove old movie metadata type

* rename new movie metadata type

* code cleanup

* start to remove old television classes

* remove old television tables from database

* fix playout building

* fix collection views

* fix show/season views

* clean up movie metadata table

* fix scanner tests

* add libraries ui

* code cleanup

* fix movie scanning/metadata

* add library scan button to ui

* delete library path from ui

* temp disable movie scanning

* remove orphan media items and prevent duplicate paths

* attach artwork to metadata

* fix split show/season display

* fix television artwork

* store year distinct from release date

* fix collections ui

* code cleanup

* add library paths from ui

* fix adding to collections from ui

* fix schedule items loading

* schedule editing works again

* remove some todos

* more cleanup

* fix unit tests

* fix episode sorting

* fix deleting show library paths

* remove unused class

* fix playout list in ui

* fix log viewer

* start to use version/file instead of statistics

* clean up old columns

* fix playout display (time zone)

* fix playback

* fix channel guide time zone

* cascade more deletes

* fix compiler warnings

* fix adding new seasons

* use artwork for channel logo

* clean cache folder on startup (move channel logos, delete everything else)

* log database migration

* update homepage docs for libraries

* fix adding new channel with logo

* fix episode numbers in epg
pull/33/head
Jason Dove 5 years ago committed by GitHub
parent
commit
f392bab118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  2. 34
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  3. 16
      ErsatzTV.Application/Channels/Mapper.cs
  4. 3
      ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs
  5. 19
      ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs
  6. 20
      ErsatzTV.Application/Images/Commands/SaveImageToDiskHandler.cs
  7. 4
      ErsatzTV.Application/Images/Queries/GetImageContents.cs
  8. 28
      ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs
  9. 9
      ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs
  10. 62
      ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs
  11. 7
      ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryPath.cs
  12. 34
      ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryPathHandler.cs
  13. 4
      ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs
  14. 6
      ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs
  15. 13
      ErsatzTV.Application/Libraries/Mapper.cs
  16. 6
      ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs
  17. 18
      ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs
  18. 7
      ErsatzTV.Application/Libraries/Queries/GetLocalLibraries.cs
  19. 22
      ErsatzTV.Application/Libraries/Queries/GetLocalLibrariesHandler.cs
  20. 7
      ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs
  21. 22
      ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs
  22. 7
      ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs
  23. 25
      ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs
  24. 2
      ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs
  25. 84
      ErsatzTV.Application/MediaCards/Mapper.cs
  26. 8
      ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs
  27. 26
      ErsatzTV.Application/MediaCards/Queries/GetCollectionCardsHandler.cs
  28. 9
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCards.cs
  29. 26
      ErsatzTV.Application/MediaCards/Queries/GetSimpleMediaCollectionCardsHandler.cs
  30. 7
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollection.cs
  31. 50
      ErsatzTV.Application/MediaCollections/Commands/AddEpisodeToCollectionHandler.cs
  32. 7
      ErsatzTV.Application/MediaCollections/Commands/AddMovieCollection.cs
  33. 50
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToCollectionHandler.cs
  34. 8
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollection.cs
  35. 62
      ErsatzTV.Application/MediaCollections/Commands/AddMovieToSimpleMediaCollectionHandler.cs
  36. 7
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollection.cs
  37. 51
      ErsatzTV.Application/MediaCollections/Commands/AddSeasonToCollectionHandler.cs
  38. 7
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollection.cs
  39. 50
      ErsatzTV.Application/MediaCollections/Commands/AddShowToCollectionHandler.cs
  40. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollection.cs
  41. 68
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionEpisodeToSimpleMediaCollectionHandler.cs
  42. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollection.cs
  43. 68
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionSeasonToSimpleMediaCollectionHandler.cs
  44. 8
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollection.cs
  45. 67
      ErsatzTV.Application/MediaCollections/Commands/AddTelevisionShowToSimpleMediaCollectionHandler.cs
  46. 8
      ErsatzTV.Application/MediaCollections/Commands/CreateCollection.cs
  47. 25
      ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs
  48. 9
      ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollection.cs
  49. 2
      ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs
  50. 17
      ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs
  51. 11
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollection.cs
  52. 51
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromCollectionHandler.cs
  53. 15
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollection.cs
  54. 74
      ErsatzTV.Application/MediaCollections/Commands/RemoveItemsFromSimpleMediaCollectionHandler.cs
  55. 8
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollection.cs
  56. 45
      ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs
  57. 8
      ErsatzTV.Application/MediaCollections/Commands/UpdateSimpleMediaCollection.cs
  58. 47
      ErsatzTV.Application/MediaCollections/Commands/UpdateSimpleMediaCollectionHandler.cs
  59. 4
      ErsatzTV.Application/MediaCollections/Mapper.cs
  60. 2
      ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs
  61. 6
      ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs
  62. 7
      ErsatzTV.Application/MediaCollections/Queries/GetAllSimpleMediaCollections.cs
  63. 25
      ErsatzTV.Application/MediaCollections/Queries/GetAllSimpleMediaCollectionsHandler.cs
  64. 7
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionById.cs
  65. 8
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs
  66. 2
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionItems.cs
  67. 8
      ErsatzTV.Application/MediaCollections/Queries/GetCollectionItemsHandler.cs
  68. 7
      ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionById.cs
  69. 62
      ErsatzTV.Application/MediaItems/Mapper.cs
  70. 2
      ErsatzTV.Application/MediaItems/MediaItemViewModel.cs
  71. 4
      ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs
  72. 10
      ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSource.cs
  73. 78
      ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSourceHandler.cs
  74. 9
      ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSource.cs
  75. 38
      ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSourceHandler.cs
  76. 114
      ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs
  77. 8
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSource.cs
  78. 111
      ErsatzTV.Application/MediaSources/Commands/ScanLocalMediaSourceHandler.cs
  79. 7
      ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs
  80. 12
      ErsatzTV.Application/MediaSources/Mapper.cs
  81. 6
      ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs
  82. 9
      ErsatzTV.Application/MediaSources/PlexMediaSourceViewModel.cs
  83. 16
      ErsatzTV.Application/Movies/Mapper.cs
  84. 32
      ErsatzTV.Application/Playouts/Mapper.cs
  85. 2
      ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs
  86. 2
      ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs
  87. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs
  88. 12
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs
  89. 25
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs
  90. 144
      ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  91. 2
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs
  92. 8
      ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs
  93. 2
      ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs
  94. 2
      ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs
  95. 11
      ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs
  96. 32
      ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs
  97. 18
      ErsatzTV.Application/Plex/Mapper.cs
  98. 6
      ErsatzTV.Application/Plex/PlexLibraryViewModel.cs
  99. 6
      ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs
  100. 2
      ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs
  101. Some files were not shown because too many files have changed in this diff Show More

26
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
@ -36,10 +37,29 @@ namespace ErsatzTV.Application.Channels.Commands @@ -36,10 +37,29 @@ namespace ErsatzTV.Application.Channels.Commands
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
(ValidateName(request), ValidateNumber(request), await FFmpegProfileMustExist(request))
.Apply(
(name, number, ffmpegProfileId) => new Channel(Guid.NewGuid())
(name, number, ffmpegProfileId) =>
{
Name = name, Number = number, FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
return new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork
};
});
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>

34
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
using System.Threading;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
@ -6,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -6,6 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands
{
@ -27,7 +31,33 @@ namespace ErsatzTV.Application.Channels.Commands @@ -27,7 +31,33 @@ namespace ErsatzTV.Application.Channels.Commands
c.Name = update.Name;
c.Number = update.Number;
c.FFmpegProfileId = update.FFmpegProfileId;
c.Logo = update.Logo;
if (!string.IsNullOrWhiteSpace(update.Logo))
{
c.Artwork ??= new List<Artwork>();
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
await _channelRepository.Update(c);
return ProjectToViewModel(c);

16
ErsatzTV.Application/Channels/Mapper.cs

@ -1,10 +1,22 @@ @@ -1,10 +1,22 @@
using ErsatzTV.Core.Domain;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels
{
internal static class Mapper
{
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
new(channel.Id, channel.Number, channel.Name, channel.FFmpegProfileId, channel.Logo, channel.StreamingMode);
new(
channel.Id,
channel.Number,
channel.Name,
channel.FFmpegProfileId,
GetLogo(channel),
channel.StreamingMode);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
}
}

3
ErsatzTV.Application/Images/Commands/SaveImageToDisk.cs → ErsatzTV.Application/Images/Commands/SaveArtworkToDisk.cs

@ -1,9 +1,10 @@ @@ -1,9 +1,10 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Images.Commands
{
// ReSharper disable once SuggestBaseTypeForParameter
public record SaveImageToDisk(byte[] Buffer) : IRequest<Either<BaseError, string>>;
public record SaveArtworkToDisk(byte[] Buffer, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
}

19
ErsatzTV.Application/Images/Commands/SaveArtworkToDiskHandler.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Images.Commands
{
public class SaveArtworkToDiskHandler : IRequestHandler<SaveArtworkToDisk, Either<BaseError, string>>
{
private readonly IImageCache _imageCache;
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) =>
_imageCache.SaveArtworkToCache(request.Buffer, request.ArtworkKind);
}
}

20
ErsatzTV.Application/Images/Commands/SaveImageToDiskHandler.cs

@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Images.Commands
{
public class SaveImageToDiskHandler : IRequestHandler<SaveImageToDisk, Either<BaseError, string>>
{
private readonly IImageCache _imageCache;
public SaveImageToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
public Task<Either<BaseError, string>> Handle(
SaveImageToDisk request,
CancellationToken cancellationToken) => _imageCache.SaveImage(request.Buffer);
}
}

4
ErsatzTV.Application/Images/Queries/GetImageContents.cs

@ -1,8 +1,10 @@ @@ -1,8 +1,10 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Images.Queries
{
public record GetImageContents(string FileName) : IRequest<Either<BaseError, ImageViewModel>>;
public record GetImageContents
(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<Either<BaseError, ImageViewModel>>;
}

28
ErsatzTV.Application/Images/Queries/GetImageContentsHandler.cs

@ -3,6 +3,8 @@ using System.IO; @@ -3,6 +3,8 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
@ -13,9 +15,14 @@ namespace ErsatzTV.Application.Images.Queries @@ -13,9 +15,14 @@ namespace ErsatzTV.Application.Images.Queries
public class GetImageContentsHandler : IRequestHandler<GetImageContents, Either<BaseError, ImageViewModel>>
{
private static readonly MimeTypes MimeTypes = new();
private readonly IImageCache _imageCache;
private readonly IMemoryCache _memoryCache;
public GetImageContentsHandler(IMemoryCache memoryCache) => _memoryCache = memoryCache;
public GetImageContentsHandler(IImageCache imageCache, IMemoryCache memoryCache)
{
_imageCache = imageCache;
_memoryCache = memoryCache;
}
public async Task<Either<BaseError, ImageViewModel>> Handle(
GetImageContents request,
@ -29,8 +36,25 @@ namespace ErsatzTV.Application.Images.Queries @@ -29,8 +36,25 @@ namespace ErsatzTV.Application.Images.Queries
{
entry.SlidingExpiration = TimeSpan.FromHours(1);
string fileName = Path.Combine(FileSystemLayout.ImageCacheFolder, request.FileName);
string subfolder = request.FileName.Substring(0, 2);
string baseFolder = request.ArtworkKind switch
{
ArtworkKind.Poster => Path.Combine(FileSystemLayout.PosterCacheFolder, subfolder),
ArtworkKind.Thumbnail => Path.Combine(FileSystemLayout.ThumbnailCacheFolder, subfolder),
ArtworkKind.Logo => Path.Combine(FileSystemLayout.LogoCacheFolder, subfolder),
_ => FileSystemLayout.LegacyImageCacheFolder
};
string fileName = Path.Combine(baseFolder, request.FileName);
byte[] contents = await File.ReadAllBytesAsync(fileName, cancellationToken);
if (request.MaxHeight.HasValue)
{
Either<BaseError, byte[]> resizeResult = await _imageCache
.ResizeImage(contents, request.MaxHeight.Value);
resizeResult.IfRight(result => contents = result);
}
MimeType mimeType = MimeTypes.GetMimeType(contents);
return new ImageViewModel(contents, mimeType.Name);
});

9
ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPath.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Libraries.Commands
{
public record CreateLocalLibraryPath
(int LibraryId, string Path) : IRequest<Either<BaseError, LocalLibraryPathViewModel>>;
}

62
ErsatzTV.Application/Libraries/Commands/CreateLocalLibraryPathHandler.cs

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
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 MediatR;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Commands
{
public class CreateLocalLibraryPathHandler : IRequestHandler<CreateLocalLibraryPath,
Either<BaseError, LocalLibraryPathViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public CreateLocalLibraryPathHandler(ILibraryRepository mediaSourceRepository) =>
_libraryRepository = mediaSourceRepository;
public Task<Either<BaseError, LocalLibraryPathViewModel>> Handle(
CreateLocalLibraryPath request,
CancellationToken cancellationToken) =>
Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync());
private Task<LocalLibraryPathViewModel> PersistLocalLibraryPath(LibraryPath p) =>
_libraryRepository.Add(p).Map(ProjectToViewModel);
private Task<Validation<BaseError, LibraryPath>> Validate(CreateLocalLibraryPath request) =>
ValidateFolder(request)
.MapT(
folder =>
new LibraryPath
{
LibraryId = request.LibraryId,
Path = folder
});
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalLibraryPath request)
{
List<string> allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId)
.Map(list => list.Map(c => c.Path).ToList());
return Optional(request.Path)
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
.ToValidation<BaseError>("Path must not belong to another library path");
}
private static bool AreSubPaths(string path1, string path2)
{
string one = path1 + Path.DirectorySeparatorChar;
string two = path2 + Path.DirectorySeparatorChar;
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
}
}
}

7
ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryPath.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Libraries.Commands
{
public record DeleteLocalLibraryPath(int LocalLibraryPathId) : MediatR.IRequest<Either<BaseError, Unit>>;
}

34
ErsatzTV.Application/Libraries/Commands/DeleteLocalLibraryPathHandler.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Libraries.Commands
{
public class
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
{
private readonly ILibraryRepository _libraryRepository;
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<Either<BaseError, Unit>> Handle(
DeleteLocalLibraryPath request,
CancellationToken cancellationToken) =>
MediaSourceMustExist(request)
.MapT(DoDeletion)
.Bind(t => t.ToEitherAsync());
private Task<Unit> DoDeletion(LibraryPath libraryPath) =>
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit();
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) =>
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
.HeadOrNone()
.ToValidation<BaseError>(
$"Local library path {request.LocalLibraryPathId} does not exist.");
}
}

4
ErsatzTV.Application/Libraries/LocalLibraryPathViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Libraries
{
public record LocalLibraryPathViewModel(int Id, int LibraryId, string Path);
}

6
ErsatzTV.Application/Libraries/LocalLibraryViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Libraries
{
public record LocalLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind);
}

13
ErsatzTV.Application/Libraries/Mapper.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Libraries
{
internal static class Mapper
{
public static LocalLibraryViewModel ProjectToViewModel(LocalLibrary library) =>
new(library.Id, library.Name, library.MediaKind);
public static LocalLibraryPathViewModel ProjectToViewModel(LibraryPath libraryPath) =>
new(libraryPath.Id, libraryPath.LibraryId, libraryPath.Path);
}
}

6
ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPath.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record CountMediaItemsByLibraryPath(int LibraryPathId) : IRequest<int>;
}

18
ErsatzTV.Application/Libraries/Queries/CountMediaItemsByLibraryPathHandler.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public class CountMediaItemsByLibraryPathHandler : IRequestHandler<CountMediaItemsByLibraryPath, int>
{
private readonly ILibraryRepository _libraryRepository;
public CountMediaItemsByLibraryPathHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<int> Handle(CountMediaItemsByLibraryPath request, CancellationToken cancellationToken) =>
_libraryRepository.CountMediaItemsByPath(request.LibraryPathId);
}
}

7
ErsatzTV.Application/Libraries/Queries/GetLocalLibraries.cs

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

22
ErsatzTV.Application/Libraries/Queries/GetLocalLibrariesHandler.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
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.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibrariesHandler : IRequestHandler<GetLocalLibraries, List<LocalLibraryViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibrariesHandler(ILibraryRepository libraryRepository) => _libraryRepository = libraryRepository;
public Task<List<LocalLibraryViewModel>>
Handle(GetLocalLibraries request, CancellationToken cancellationToken) =>
_libraryRepository.GetAllLocal().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

7
ErsatzTV.Application/Libraries/Queries/GetLocalLibraryById.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record GetLocalLibraryById(int LibraryId) : IRequest<Option<LocalLibraryViewModel>>;
}

22
ErsatzTV.Application/Libraries/Queries/GetLocalLibraryByIdHandler.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibraryByIdHandler : IRequestHandler<GetLocalLibraryById, Option<LocalLibraryViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibraryByIdHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<Option<LocalLibraryViewModel>> Handle(
GetLocalLibraryById request,
CancellationToken cancellationToken) =>
_libraryRepository.GetLocal(request.LibraryId).MapT(ProjectToViewModel);
}
}

7
ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPaths.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Libraries.Queries
{
public record GetLocalLibraryPaths(int LocalLibraryId) : IRequest<List<LocalLibraryPathViewModel>>;
}

25
ErsatzTV.Application/Libraries/Queries/GetLocalLibraryPathsHandler.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Libraries.Mapper;
namespace ErsatzTV.Application.Libraries.Queries
{
public class GetLocalLibraryPathsHandler : IRequestHandler<GetLocalLibraryPaths, List<LocalLibraryPathViewModel>>
{
private readonly ILibraryRepository _libraryRepository;
public GetLocalLibraryPathsHandler(ILibraryRepository libraryRepository) =>
_libraryRepository = libraryRepository;
public Task<List<LocalLibraryPathViewModel>> Handle(
GetLocalLibraryPaths request,
CancellationToken cancellationToken) =>
_libraryRepository.GetLocalPaths(request.LocalLibraryId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

2
ErsatzTV.Application/MediaCards/SimpleMediaCollectionCardResultsViewModel.cs → ErsatzTV.Application/MediaCards/CollectionCardResultsViewModel.cs

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
namespace ErsatzTV.Application.MediaCards
{
public record SimpleMediaCollectionCardResultsViewModel(
public record CollectionCardResultsViewModel(
string Name,
List<MovieCardViewModel> MovieCards,
List<TelevisionShowCardViewModel> ShowCards,

84
ErsatzTV.Application/MediaCards/Mapper.cs

@ -1,60 +1,70 @@ @@ -1,60 +1,70 @@
using System;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCards
{
internal static class Mapper
{
internal static TelevisionShowCardViewModel ProjectToViewModel(TelevisionShow televisionShow) =>
internal static TelevisionShowCardViewModel ProjectToViewModel(ShowMetadata showMetadata) =>
new(
televisionShow.Id,
televisionShow.Metadata?.Title,
televisionShow.Metadata?.Year.ToString(),
televisionShow.Metadata?.SortTitle,
televisionShow.Poster);
showMetadata.ShowId,
showMetadata.Title,
showMetadata.Year?.ToString(),
showMetadata.SortTitle,
GetPoster(showMetadata));
internal static TelevisionSeasonCardViewModel ProjectToViewModel(TelevisionSeason televisionSeason) =>
internal static TelevisionSeasonCardViewModel ProjectToViewModel(Season season) =>
new(
televisionSeason.TelevisionShow.Metadata?.Title,
televisionSeason.Id,
televisionSeason.Number,
GetSeasonName(televisionSeason.Number),
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty),
season.Id,
season.SeasonNumber,
GetSeasonName(season.SeasonNumber),
string.Empty,
GetSeasonName(televisionSeason.Number),
televisionSeason.Poster,
televisionSeason.Number == 0 ? "S" : televisionSeason.Number.ToString());
GetSeasonName(season.SeasonNumber),
season.SeasonMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
TelevisionEpisodeMediaItem televisionEpisode) =>
EpisodeMetadata episodeMetadata) =>
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) =>
episodeMetadata.EpisodeId,
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty),
episodeMetadata.Title,
$"Episode {episodeMetadata.Episode.EpisodeNumber}",
episodeMetadata.Episode.EpisodeNumber.ToString(),
GetThumbnail(episodeMetadata),
episodeMetadata.Episode.EpisodeNumber.ToString());
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) =>
new(
movie.Id,
movie.Metadata?.Title,
movie.Metadata?.Year?.ToString(),
movie.Metadata?.SortTitle,
movie.Poster);
internal static SimpleMediaCollectionCardResultsViewModel
ProjectToViewModel(SimpleMediaCollection collection) =>
movieMetadata.MovieId,
movieMetadata.Title,
movieMetadata.Year?.ToString(),
movieMetadata.SortTitle,
GetPoster(movieMetadata));
internal static CollectionCardResultsViewModel
ProjectToViewModel(Collection collection) =>
new(
collection.Name,
collection.Movies.Map(ProjectToViewModel).ToList(),
collection.TelevisionShows.Map(ProjectToViewModel).ToList(),
collection.TelevisionSeasons.Map(ProjectToViewModel).ToList(),
collection.TelevisionEpisodes.Map(ProjectToViewModel).ToList());
collection.MediaItems.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(),
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
.ToList());
private static string GetSeasonName(int number) =>
number == 0 ? "Specials" : $"Season {number}";
private static string GetPoster(Metadata metadata) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty);
private static string GetThumbnail(Metadata metadata) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Thumbnail))
.Match(a => a.Path, string.Empty);
}
}

8
ErsatzTV.Application/MediaCards/Queries/GetCollectionCards.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCards.Queries
{
public record GetCollectionCards(int Id) : IRequest<Either<BaseError, CollectionCardResultsViewModel>>;
}

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

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaCards.Mapper;
namespace ErsatzTV.Application.MediaCards.Queries
{
public class GetCollectionCardsHandler : IRequestHandler<GetCollectionCards,
Either<BaseError, CollectionCardResultsViewModel>>
{
private readonly IMediaCollectionRepository _collectionRepository;
public GetCollectionCardsHandler(IMediaCollectionRepository collectionRepository) =>
_collectionRepository = collectionRepository;
public async Task<Either<BaseError, CollectionCardResultsViewModel>> Handle(
GetCollectionCards request,
CancellationToken cancellationToken) =>
(await _collectionRepository.GetCollectionWithItemsUntracked(request.Id))
.ToEither(BaseError.New("Unable to load collection"))
.Map(ProjectToViewModel);
}
}

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

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

@ -1,26 +0,0 @@ @@ -1,26 +0,0 @@
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/MediaCollections/Commands/AddEpisodeToCollection.cs

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

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

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddEpisodeToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddEpisodeToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request))
.Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request) =>
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId);
private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) =>
(await CollectionMustExist(request), await ValidateEpisode(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddEpisodeToCollection request) =>
_mediaCollectionRepository.Get(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateEpisode(AddEpisodeToCollection request) =>
LoadTelevisionEpisode(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Episode does not exist"));
private Task<Option<int>> LoadTelevisionEpisode(AddEpisodeToCollection request) =>
_televisionRepository.GetEpisode(request.EpisodeId).MapT(e => e.Id);
}
}

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

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

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

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
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 AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository;
public AddMovieToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
IMovieRepository movieRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_movieRepository = movieRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddMovieToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddMoviesRequest(request))
.Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request) =>
_mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId);
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) =>
(await CollectionMustExist(request), await ValidateMovies(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMovieToCollection request) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateMovies(AddMovieToCollection request) =>
LoadMovie(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Movie does not exist"));
private Task<Option<Movie>> LoadMovie(AddMovieToCollection request) =>
_movieRepository.GetMovie(request.MovieId);
}
}

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

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

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

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

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

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

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
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 AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddSeasonToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddSeasonToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddTelevisionSeasonRequest(request))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyAddTelevisionSeasonRequest(AddSeasonToCollection request) =>
await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId);
private async Task<Validation<BaseError, Unit>> Validate(AddSeasonToCollection request) =>
(await CollectionMustExist(request), await ValidateSeason(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddSeasonToCollection request) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateSeason(AddSeasonToCollection request) =>
LoadTelevisionSeason(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Season does not exist"));
private Task<Option<Season>> LoadTelevisionSeason(
AddSeasonToCollection request) =>
_televisionRepository.GetSeason(request.SeasonId);
}
}

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

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

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

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
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 AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
public AddShowToCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository)
{
_mediaCollectionRepository = mediaCollectionRepository;
_televisionRepository = televisionRepository;
}
public Task<Either<BaseError, Unit>> Handle(
AddShowToCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyAddTelevisionShowRequest(request))
.Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request)
=> _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId);
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) =>
(await CollectionMustExist(request), await ValidateShow(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddShowToCollection request) =>
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Task<Validation<BaseError, Unit>> ValidateShow(AddShowToCollection request) =>
LoadTelevisionShow(request)
.MapT(_ => Unit.Default)
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
private Task<Option<Show>> LoadTelevisionShow(AddShowToCollection request) =>
_televisionRepository.GetShow(request.ShowId);
}
}

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

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

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

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

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

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

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

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

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record CreateCollection(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>;
}

25
ErsatzTV.Application/MediaCollections/Commands/CreateSimpleMediaCollectionHandler.cs → ErsatzTV.Application/MediaCollections/Commands/CreateCollectionHandler.cs

@ -12,36 +12,33 @@ using static LanguageExt.Prelude; @@ -12,36 +12,33 @@ using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class CreateSimpleMediaCollectionHandler : IRequestHandler<CreateSimpleMediaCollection,
Either<BaseError, MediaCollectionViewModel>>
public class
CreateCollectionHandler : IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public CreateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
public CreateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Either<BaseError, MediaCollectionViewModel>> Handle(
CreateSimpleMediaCollection request,
CreateCollection request,
CancellationToken cancellationToken) =>
Validate(request).MapT(PersistCollection).Bind(v => v.ToEitherAsync());
private Task<MediaCollectionViewModel> PersistCollection(SimpleMediaCollection c) =>
private Task<MediaCollectionViewModel> PersistCollection(Collection c) =>
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
private Task<Validation<BaseError, SimpleMediaCollection>> Validate(CreateSimpleMediaCollection request) =>
private Task<Validation<BaseError, Collection>> Validate(CreateCollection request) =>
ValidateName(request).MapT(
name => new SimpleMediaCollection
name => new Collection
{
Name = name,
Movies = new List<MovieMediaItem>(),
TelevisionShows = new List<TelevisionShow>(),
TelevisionEpisodes = new List<TelevisionEpisodeMediaItem>(),
TelevisionSeasons = new List<TelevisionSeason>()
MediaItems = new List<MediaItem>()
});
private async Task<Validation<BaseError, string>> ValidateName(CreateSimpleMediaCollection createCollection)
private async Task<Validation<BaseError, string>> ValidateName(CreateCollection createCollection)
{
List<string> allNames = await _mediaCollectionRepository.GetSimpleMediaCollections()
List<string> allNames = await _mediaCollectionRepository.GetAll()
.Map(list => list.Map(c => c.Name).ToList());
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
@ -49,7 +46,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands @@ -49,7 +46,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
var result2 = Optional(createCollection.Name)
.Filter(name => !allNames.Contains(name))
.ToValidation<BaseError>("Media collection name must be unique");
.ToValidation<BaseError>("Collection name must be unique");
return (result1, result2).Apply((_, _) => createCollection.Name);
}

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

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record CreateSimpleMediaCollection
(string Name) : IRequest<Either<BaseError, MediaCollectionViewModel>>;
}

2
ErsatzTV.Application/MediaCollections/Commands/DeleteSimpleMediaCollection.cs → ErsatzTV.Application/MediaCollections/Commands/DeleteCollection.cs

@ -5,5 +5,5 @@ using MediatR; @@ -5,5 +5,5 @@ using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record DeleteSimpleMediaCollection(int SimpleMediaCollectionId) : IRequest<Either<BaseError, Task>>;
public record DeleteCollection(int CollectionId) : IRequest<Either<BaseError, Task>>;
}

17
ErsatzTV.Application/MediaCollections/Commands/DeleteSimpleMediaCollectionHandler.cs → ErsatzTV.Application/MediaCollections/Commands/DeleteCollectionHandler.cs

@ -7,28 +7,27 @@ using MediatR; @@ -7,28 +7,27 @@ using MediatR;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public class
DeleteSimpleMediaCollectionHandler : IRequestHandler<DeleteSimpleMediaCollection, Either<BaseError, Task>>
public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<BaseError, Task>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public DeleteSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
public DeleteCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<Either<BaseError, Task>> Handle(
DeleteSimpleMediaCollection request,
DeleteCollection request,
CancellationToken cancellationToken) =>
(await SimpleMediaCollectionMustExist(request))
(await CollectionMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
private Task DoDeletion(int mediaCollectionId) => _mediaCollectionRepository.Delete(mediaCollectionId);
private async Task<Validation<BaseError, int>> SimpleMediaCollectionMustExist(
DeleteSimpleMediaCollection deleteMediaCollection) =>
(await _mediaCollectionRepository.GetSimpleMediaCollection(deleteMediaCollection.SimpleMediaCollectionId))
private async Task<Validation<BaseError, int>> CollectionMustExist(
DeleteCollection deleteMediaCollection) =>
(await _mediaCollectionRepository.Get(deleteMediaCollection.CollectionId))
.ToValidation<BaseError>(
$"SimpleMediaCollection {deleteMediaCollection.SimpleMediaCollectionId} does not exist.")
$"Collection {deleteMediaCollection.CollectionId} does not exist.")
.Map(c => c.Id);
}
}

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

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

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

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
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
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public RemoveItemsFromCollectionHandler(
IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Either<BaseError, Unit>> Handle(
RemoveItemsFromCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(collection => ApplyAddTelevisionEpisodeRequest(request, collection))
.Bind(v => v.ToEitherAsync());
private Task<Unit> ApplyAddTelevisionEpisodeRequest(
RemoveItemsFromCollection request,
Collection collection)
{
var itemsToRemove = collection.MediaItems
.Filter(m => request.MediaItemIds.Contains(m.Id))
.ToList();
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m));
return itemsToRemove.Any()
? _mediaCollectionRepository.Update(collection).ToUnit()
: Task.FromResult(Unit.Default);
}
private Task<Validation<BaseError, Collection>> Validate(
RemoveItemsFromCollection request) =>
CollectionMustExist(request);
private Task<Validation<BaseError, Collection>> CollectionMustExist(
RemoveItemsFromCollection updateCollection) =>
_mediaCollectionRepository.GetCollectionWithItems(updateCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
}
}

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

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

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

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

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record UpdateCollection
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>;
}

45
ErsatzTV.Application/MediaCollections/Commands/UpdateCollectionHandler.cs

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
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 UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection update)
{
c.Name = update.Name;
await _mediaCollectionRepository.Update(c);
return Unit.Default;
}
private async Task<Validation<BaseError, Collection>>
Validate(UpdateCollection request) =>
(await CollectionMustExist(request), ValidateName(request))
.Apply((collectionToUpdate, _) => collectionToUpdate);
private Task<Validation<BaseError, Collection>> CollectionMustExist(
UpdateCollection updateCollection) =>
_mediaCollectionRepository.Get(updateCollection.CollectionId)
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
private Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) =>
updateSimpleMediaCollection.NotEmpty(c => c.Name)
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
}
}

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

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaCollections.Commands
{
public record UpdateSimpleMediaCollection
(int MediaCollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>;
}

47
ErsatzTV.Application/MediaCollections/Commands/UpdateSimpleMediaCollectionHandler.cs

@ -1,47 +0,0 @@ @@ -1,47 +0,0 @@
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
UpdateSimpleMediaCollectionHandler : MediatR.IRequestHandler<UpdateSimpleMediaCollection,
Either<BaseError, Unit>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public UpdateSimpleMediaCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateSimpleMediaCollection request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> ApplyUpdateRequest(SimpleMediaCollection c, UpdateSimpleMediaCollection update)
{
c.Name = update.Name;
await _mediaCollectionRepository.Update(c);
return Unit.Default;
}
private async Task<Validation<BaseError, SimpleMediaCollection>>
Validate(UpdateSimpleMediaCollection request) =>
(await SimpleMediaCollectionMustExist(request), ValidateName(request))
.Apply((simpleMediaCollectionToUpdate, _) => simpleMediaCollectionToUpdate);
private Task<Validation<BaseError, SimpleMediaCollection>> SimpleMediaCollectionMustExist(
UpdateSimpleMediaCollection updateSimpleMediaCollection) =>
_mediaCollectionRepository.GetSimpleMediaCollection(updateSimpleMediaCollection.MediaCollectionId)
.Map(v => v.ToValidation<BaseError>("SimpleMediaCollection does not exist."));
private Validation<BaseError, string> ValidateName(UpdateSimpleMediaCollection updateSimpleMediaCollection) =>
updateSimpleMediaCollection.NotEmpty(c => c.Name)
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
}
}

4
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.MediaCollections @@ -4,7 +4,7 @@ namespace ErsatzTV.Application.MediaCollections
{
internal static class Mapper
{
internal static MediaCollectionViewModel ProjectToViewModel(MediaCollection mediaCollection) =>
new(mediaCollection.Id, mediaCollection.Name);
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
new(collection.Id, collection.Name);
}
}

2
ErsatzTV.Application/MediaCollections/Queries/GetAllMediaCollections.cs → ErsatzTV.Application/MediaCollections/Queries/GetAllCollections.cs

@ -3,5 +3,5 @@ using MediatR; @@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetAllMediaCollections : IRequest<List<MediaCollectionViewModel>>;
public record GetAllCollections : IRequest<List<MediaCollectionViewModel>>;
}

6
ErsatzTV.Application/MediaCollections/Queries/GetAllMediaCollectionsHandler.cs → ErsatzTV.Application/MediaCollections/Queries/GetAllCollectionsHandler.cs

@ -9,15 +9,15 @@ using static ErsatzTV.Application.MediaCollections.Mapper; @@ -9,15 +9,15 @@ using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class GetAllMediaCollectionsHandler : IRequestHandler<GetAllMediaCollections, List<MediaCollectionViewModel>>
public class GetAllCollectionsHandler : IRequestHandler<GetAllCollections, List<MediaCollectionViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetAllMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
public GetAllCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<List<MediaCollectionViewModel>> Handle(
GetAllMediaCollections request,
GetAllCollections request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
}

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

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

25
ErsatzTV.Application/MediaCollections/Queries/GetAllSimpleMediaCollectionsHandler.cs

@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class
GetAllSimpleMediaCollectionsHandler : IRequestHandler<GetAllSimpleMediaCollections,
List<MediaCollectionViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetAllSimpleMediaCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public async Task<List<MediaCollectionViewModel>> Handle(
GetAllSimpleMediaCollections request,
CancellationToken cancellationToken) =>
(await _mediaCollectionRepository.GetSimpleMediaCollections()).Map(ProjectToViewModel).ToList();
}
}

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

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

8
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionByIdHandler.cs → ErsatzTV.Application/MediaCollections/Queries/GetCollectionByIdHandler.cs

@ -8,18 +8,18 @@ using static ErsatzTV.Application.MediaCollections.Mapper; @@ -8,18 +8,18 @@ using static ErsatzTV.Application.MediaCollections.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class
GetSimpleMediaCollectionByIdHandler : IRequestHandler<GetSimpleMediaCollectionById,
GetCollectionByIdHandler : IRequestHandler<GetCollectionById,
Option<MediaCollectionViewModel>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
public GetCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Option<MediaCollectionViewModel>> Handle(
GetSimpleMediaCollectionById request,
GetCollectionById request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetSimpleMediaCollection(request.Id)
_mediaCollectionRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}

2
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionItems.cs → ErsatzTV.Application/MediaCollections/Queries/GetCollectionItems.cs

@ -5,5 +5,5 @@ using MediatR; @@ -5,5 +5,5 @@ using MediatR;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public record GetSimpleMediaCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>;
public record GetCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>;
}

8
ErsatzTV.Application/MediaCollections/Queries/GetSimpleMediaCollectionItemsHandler.cs → ErsatzTV.Application/MediaCollections/Queries/GetCollectionItemsHandler.cs

@ -9,18 +9,18 @@ using static ErsatzTV.Application.MediaItems.Mapper; @@ -9,18 +9,18 @@ using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.MediaCollections.Queries
{
public class GetSimpleMediaCollectionItemsHandler : IRequestHandler<GetSimpleMediaCollectionItems,
public class GetCollectionItemsHandler : IRequestHandler<GetCollectionItems,
Option<IEnumerable<MediaItemViewModel>>>
{
private readonly IMediaCollectionRepository _mediaCollectionRepository;
public GetSimpleMediaCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
public GetCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
_mediaCollectionRepository = mediaCollectionRepository;
public Task<Option<IEnumerable<MediaItemViewModel>>> Handle(
GetSimpleMediaCollectionItems request,
GetCollectionItems request,
CancellationToken cancellationToken) =>
_mediaCollectionRepository.GetSimpleMediaCollectionItems(request.Id)
_mediaCollectionRepository.GetItems(request.Id)
.MapT(mediaItems => mediaItems.Map(ProjectToViewModel));
}
}

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

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

62
ErsatzTV.Application/MediaItems/Mapper.cs

@ -1,5 +1,4 @@ @@ -1,5 +1,4 @@
using System;
using System.IO;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaItems
@ -7,31 +6,28 @@ namespace ErsatzTV.Application.MediaItems @@ -7,31 +6,28 @@ namespace ErsatzTV.Application.MediaItems
internal static class Mapper
{
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
new(
mediaItem.Id,
mediaItem.MediaSourceId,
mediaItem.Path);
new(mediaItem.Id, mediaItem.LibraryPathId);
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => ProjectToSearchViewModel(e),
MovieMediaItem m => ProjectToSearchViewModel(m),
Episode e => ProjectToSearchViewModel(e),
Movie m => ProjectToSearchViewModel(m),
_ => throw new ArgumentOutOfRangeException()
};
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(TelevisionEpisodeMediaItem mediaItem) =>
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Episode mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
GetLibraryName(mediaItem),
"TV Show",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(MovieMediaItem mediaItem) =>
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Movie mediaItem) =>
new(
mediaItem.Id,
GetSourceName(mediaItem.Source),
GetLibraryName(mediaItem),
"Movie",
GetDisplayTitle(mediaItem),
GetDisplayDuration(mediaItem));
@ -40,23 +36,41 @@ namespace ErsatzTV.Application.MediaItems @@ -40,23 +36,41 @@ namespace ErsatzTV.Application.MediaItems
private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
Episode e => e.EpisodeMetadata.HeadOrNone()
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
.IfNone("[unknown episode]"),
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Statistics.Duration);
private static string GetSourceName(MediaSource source) =>
source switch
private static string GetDisplayDuration(MediaItem mediaItem)
{
MediaVersion version = mediaItem switch
{
LocalMediaSource lms => lms.Folder,
_ => source.Name
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
return string.Format(
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
version.Duration);
}
// TODO: fix this when search is reimplemented
private static string GetLibraryName(MediaItem item) =>
"Library Name";
public static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???"));
public static NamedMediaItemViewModel ProjectToViewModel(Season season) =>
new(season.Id, $"{ShowTitle(season)} ({SeasonDescription(season)})");
private static string ShowTitle(Season season) =>
season.Show.ShowMetadata.HeadOrNone().Map(sm => sm.Title).IfNone("???");
private static string SeasonDescription(Season season) =>
season.SeasonNumber == 0 ? "Specials" : $"Season {season.SeasonNumber}";
}
}

2
ErsatzTV.Application/MediaItems/MediaItemViewModel.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
namespace ErsatzTV.Application.MediaItems
{
public record MediaItemViewModel(int Id, int MediaSourceId, string Path);
public record MediaItemViewModel(int Id, int LibraryPathId);
}

4
ErsatzTV.Application/MediaItems/NamedMediaItemViewModel.cs

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
namespace ErsatzTV.Application.MediaItems
{
public record NamedMediaItemViewModel(int MediaItemId, string Name);
}

10
ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSource.cs

@ -1,10 +0,0 @@ @@ -1,10 +0,0 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record CreateLocalMediaSource
(string Name, MediaType MediaType, string Folder) : IRequest<Either<BaseError, MediaSourceViewModel>>;
}

78
ErsatzTV.Application/MediaSources/Commands/CreateLocalMediaSourceHandler.cs

@ -1,78 +0,0 @@ @@ -1,78 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
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 MediatR;
using static ErsatzTV.Application.MediaSources.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class CreateLocalMediaSourceHandler : IRequestHandler<CreateLocalMediaSource,
Either<BaseError, MediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public CreateLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, MediaSourceViewModel>> Handle(
CreateLocalMediaSource request,
CancellationToken cancellationToken) =>
Validate(request).MapT(PersistLocalMediaSource).Bind(v => v.ToEitherAsync());
private Task<MediaSourceViewModel> PersistLocalMediaSource(LocalMediaSource c) =>
_mediaSourceRepository.Add(c).Map(ProjectToViewModel);
private async Task<Validation<BaseError, LocalMediaSource>> Validate(CreateLocalMediaSource request) =>
(await ValidateName(request), await ValidateFolder(request))
.Apply(
(name, folder) =>
new LocalMediaSource
{
Name = name,
MediaType = request.MediaType,
Folder = folder
});
private async Task<Validation<BaseError, string>> ValidateName(CreateLocalMediaSource request)
{
List<string> allNames = await _mediaSourceRepository.GetAll()
.Map(list => list.Map(c => c.Name).ToList());
Validation<BaseError, string> result1 = request.NotEmpty(c => c.Name)
.Bind(_ => request.NotLongerThan(50)(c => c.Name));
var result2 = Optional(request.Name)
.Filter(name => !allNames.Contains(name))
.ToValidation<BaseError>("Media source name must be unique");
return (result1, result2).Apply((_, _) => request.Name);
}
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalMediaSource request)
{
List<string> allFolders = await _mediaSourceRepository.GetAll()
.Map(list => list.OfType<LocalMediaSource>().Map(c => c.Folder).ToList());
return Optional(request.Folder)
.Filter(folder => allFolders.ForAll(f => !AreSubPaths(f, folder)))
.ToValidation<BaseError>("Folder must not belong to another media source");
}
private static bool AreSubPaths(string path1, string path2)
{
string one = path1 + Path.DirectorySeparatorChar;
string two = path2 + Path.DirectorySeparatorChar;
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
}
}
}

9
ErsatzTV.Application/MediaSources/Commands/DeleteLocalMediaSource.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public record DeleteLocalMediaSource(int LocalMediaSourceId) : IRequest<Either<BaseError, Task>>;
}

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

@ -1,38 +0,0 @@ @@ -1,38 +0,0 @@
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 MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class
DeleteLocalMediaSourceHandler : IRequestHandler<DeleteLocalMediaSource, Either<BaseError, Task>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public DeleteLocalMediaSourceHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public async Task<Either<BaseError, Task>> Handle(
DeleteLocalMediaSource request,
CancellationToken cancellationToken) =>
(await MediaSourceMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
private async Task DoDeletion(LocalMediaSource mediaSource) =>
await _mediaSourceRepository.Delete(mediaSource.Id);
private async Task<Validation<BaseError, LocalMediaSource>> MediaSourceMustExist(
DeleteLocalMediaSource deleteMediaSource) =>
(await _mediaSourceRepository.Get(deleteMediaSource.LocalMediaSourceId))
.OfType<LocalMediaSource>()
.HeadOrNone()
.ToValidation<BaseError>(
$"Local media source {deleteMediaSource.LocalMediaSourceId} does not exist.");
}
}

114
ErsatzTV.Application/MediaSources/Commands/ScanLocalLibraryHandler.cs

@ -0,0 +1,114 @@ @@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<ScanLocalLibraryHandler> _logger;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalLibraryHandler(
ILibraryRepository libraryRepository,
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker,
ILogger<ScanLocalLibraryHandler> logger)
{
_libraryRepository = libraryRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceScanLocalLibrary request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
ScanLocalLibraryIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(IScanLocalLibrary request) =>
Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalLibrary.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(RequestParameters parameters)
{
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
foreach (LibraryPath libraryPath in localLibrary.Paths)
{
switch (localLibrary.MediaKind)
{
case LibraryMediaKind.Movies:
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath);
break;
case LibraryMediaKind.Shows:
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath);
break;
}
}
localLibrary.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(localLibrary);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of library {Name}",
localLibrary.Name);
}
_entityLocker.UnlockLibrary(localLibrary.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
(await LocalLibraryMustExist(request), await ValidateFFprobePath())
.Apply(
(library, ffprobePath) => new RequestParameters(
library,
ffprobePath,
request.ForceScan));
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
IScanLocalLibrary request) =>
_libraryRepository.Get(request.LibraryId)
.Map(maybeLibrary => maybeLibrary.Map(ms => ms as LocalLibrary))
.Map(v => v.ToValidation<BaseError>($"Local library {request.LibraryId} does not exist."));
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(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
}
}

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

@ -4,18 +4,18 @@ using MediatR; @@ -4,18 +4,18 @@ using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
{
public interface IScanLocalMediaSource : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
public interface IScanLocalLibrary : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
{
int MediaSourceId { get; }
int LibraryId { get; }
bool ForceScan { get; }
}
public record ScanLocalMediaSourceIfNeeded(int MediaSourceId) : IScanLocalMediaSource
public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary
{
public bool ForceScan => false;
}
public record ForceScanLocalMediaSource(int MediaSourceId) : IScanLocalMediaSource
public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary
{
public bool ForceScan => true;
}

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

@ -1,111 +0,0 @@ @@ -1,111 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.MediaSources.Commands
{
public class ScanLocalMediaSourceHandler : IRequestHandler<ForceScanLocalMediaSource, Either<BaseError, string>>,
IRequestHandler<ScanLocalMediaSourceIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEntityLocker _entityLocker;
private readonly ILogger<ScanLocalMediaSourceHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMovieFolderScanner _movieFolderScanner;
private readonly ITelevisionFolderScanner _televisionFolderScanner;
public ScanLocalMediaSourceHandler(
IMediaSourceRepository mediaSourceRepository,
IConfigElementRepository configElementRepository,
IMovieFolderScanner movieFolderScanner,
ITelevisionFolderScanner televisionFolderScanner,
IEntityLocker entityLocker,
ILogger<ScanLocalMediaSourceHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_configElementRepository = configElementRepository;
_movieFolderScanner = movieFolderScanner;
_televisionFolderScanner = televisionFolderScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceScanLocalMediaSource request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
public Task<Either<BaseError, string>> Handle(
ScanLocalMediaSourceIfNeeded request,
CancellationToken cancellationToken) =>
Handle((IScanLocalMediaSource) request, cancellationToken);
private Task<Either<BaseError, string>>
Handle(IScanLocalMediaSource request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(parameters => PerformScan(parameters).Map(_ => parameters.LocalMediaSource.Folder))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> PerformScan(RequestParameters parameters)
{
DateTimeOffset lastScan = parameters.LocalMediaSource.LastScan ?? DateTime.MinValue;
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
switch (parameters.LocalMediaSource.MediaType)
{
case MediaType.Movie:
await _movieFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
case MediaType.TvShow:
await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
parameters.LocalMediaSource.LastScan = DateTimeOffset.Now;
await _mediaSourceRepository.Update(parameters.LocalMediaSource);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of media source {Folder}",
parameters.LocalMediaSource.Folder);
}
_entityLocker.UnlockMediaSource(parameters.LocalMediaSource.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalMediaSource request) =>
(await LocalMediaSourceMustExist(request), await ValidateFFprobePath())
.Apply(
(localMediaSource, ffprobePath) => new RequestParameters(
localMediaSource,
ffprobePath,
request.ForceScan));
private Task<Validation<BaseError, LocalMediaSource>> LocalMediaSourceMustExist(
IScanLocalMediaSource request) =>
_mediaSourceRepository.Get(request.MediaSourceId)
.Map(maybeMediaSource => maybeMediaSource.Map(ms => ms as LocalMediaSource))
.Map(v => v.ToValidation<BaseError>($"Local media source {request.MediaSourceId} does not exist."));
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(LocalMediaSource LocalMediaSource, string FFprobePath, bool ForceScan);
}
}

7
ErsatzTV.Application/MediaSources/LocalMediaSourceViewModel.cs

@ -1,7 +1,4 @@ @@ -1,7 +1,4 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
namespace ErsatzTV.Application.MediaSources
{
public record LocalMediaSourceViewModel(int Id, string Name, string Folder)
: MediaSourceViewModel(Id, Name, MediaSourceType.Local);
public record LocalMediaSourceViewModel(int Id) : MediaSourceViewModel(Id, "Local");
}

12
ErsatzTV.Application/MediaSources/Mapper.cs

@ -1,7 +1,5 @@ @@ -1,7 +1,5 @@
using System;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaSources
{
@ -10,15 +8,9 @@ namespace ErsatzTV.Application.MediaSources @@ -10,15 +8,9 @@ namespace ErsatzTV.Application.MediaSources
internal static MediaSourceViewModel ProjectToViewModel(MediaSource mediaSource) =>
mediaSource switch
{
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id, lms.Name, lms.Folder),
PlexMediaSource pms => ProjectToViewModel(pms),
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id),
PlexMediaSource pms => Plex.Mapper.ProjectToViewModel(pms),
_ => throw new NotSupportedException($"Unsupported media source {mediaSource.GetType().Name}")
};
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) =>
new(
plexMediaSource.Id,
plexMediaSource.Name,
Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty));
}
}

6
ErsatzTV.Application/MediaSources/MediaSourceViewModel.cs

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
namespace ErsatzTV.Application.MediaSources
{
public record MediaSourceViewModel(int Id, string Name, MediaSourceType SourceType);
public record MediaSourceViewModel(int Id, string Name);
}

9
ErsatzTV.Application/MediaSources/PlexMediaSourceViewModel.cs

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.MediaSources
{
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(
Id,
Name,
MediaSourceType.Plex);
}

16
ErsatzTV.Application/Movies/Mapper.cs

@ -1,10 +1,20 @@ @@ -1,10 +1,20 @@
using ErsatzTV.Core.Domain;
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
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);
internal static MovieViewModel ProjectToViewModel(Movie movie)
{
MovieMetadata metadata = Optional(movie.MovieMetadata).Flatten().Head();
return new MovieViewModel(
metadata.Title,
metadata.Year?.ToString(),
metadata.Plot,
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Poster))
.Match(a => a.Path, string.Empty));
}
}
}

32
ErsatzTV.Application/Playouts/Mapper.cs

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
using System.IO;
using System;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts
@ -13,7 +13,10 @@ namespace ErsatzTV.Application.Playouts @@ -13,7 +13,10 @@ namespace ErsatzTV.Application.Playouts
playout.ProgramSchedulePlayoutType);
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
new(GetDisplayTitle(playoutItem.MediaItem), playoutItem.Start, GetDisplayDuration(playoutItem.MediaItem));
new(
GetDisplayTitle(playoutItem.MediaItem),
playoutItem.StartOffset,
GetDisplayDuration(playoutItem.MediaItem));
private static PlayoutChannelViewModel Project(Channel channel) =>
new(channel.Id, channel.Number, channel.Name);
@ -24,16 +27,25 @@ namespace ErsatzTV.Application.Playouts @@ -24,16 +27,25 @@ namespace ErsatzTV.Application.Playouts
private static string GetDisplayTitle(MediaItem mediaItem) =>
mediaItem switch
{
TelevisionEpisodeMediaItem e => e.Metadata != null
? $"{e.Metadata.Title} - s{e.Metadata.Season:00}e{e.Metadata.Episode:00}"
: Path.GetFileName(e.Path),
MovieMediaItem m => m.Metadata?.Title ?? Path.GetFileName(m.Path),
Episode e => e.EpisodeMetadata.HeadOrNone()
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
.IfNone("[unknown episode]"),
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
_ => string.Empty
};
private static string GetDisplayDuration(MediaItem mediaItem) =>
string.Format(
mediaItem.Statistics.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
mediaItem.Statistics.Duration);
private static string GetDisplayDuration(MediaItem mediaItem)
{
MediaVersion version = mediaItem switch
{
Movie m => m.MediaVersions.Head(),
Episode e => e.MediaVersions.Head(),
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
};
return string.Format(
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
version.Duration);
}
}
}

2
ErsatzTV.Application/MediaSources/Commands/StartPlexPinFlow.cs → ErsatzTV.Application/Plex/Commands/StartPlexPinFlow.cs

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record StartPlexPinFlow : IRequest<Either<BaseError, string>>;
}

2
ErsatzTV.Application/MediaSources/Commands/StartPlexPinFlowHandler.cs → ErsatzTV.Application/Plex/Commands/StartPlexPinFlowHandler.cs

@ -7,7 +7,7 @@ using LanguageExt; @@ -7,7 +7,7 @@ using LanguageExt;
using MediatR;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class StartPlexPinFlowHandler : IRequestHandler<StartPlexPinFlow, Either<BaseError, string>>
{

2
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexLibraries.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraries.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>;
}

12
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexLibrariesHandler.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexLibrariesHandler.cs

@ -10,7 +10,7 @@ using ErsatzTV.Core.Plex; @@ -10,7 +10,7 @@ using ErsatzTV.Core.Plex;
using LanguageExt;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexLibrariesHandler : MediatR.IRequestHandler<SynchronizePlexLibraries, Either<BaseError, Unit>>
@ -51,7 +51,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -51,7 +51,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
PlexMediaSource plexMediaSource)
{
Option<PlexMediaSourceConnection> maybeConnection =
Option<PlexConnection> maybeConnection =
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive);
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection))
.ToValidation<BaseError>("Plex media source requires an active connection");
@ -68,14 +68,14 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -68,14 +68,14 @@ namespace ErsatzTV.Application.MediaSources.Commands
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<PlexMediaSourceLibrary>> maybeLibraries = await _plexServerApiClient.GetLibraries(
Either<BaseError, List<PlexLibrary>> maybeLibraries = await _plexServerApiClient.GetLibraries(
connectionParameters.ActiveConnection,
connectionParameters.PlexServerAuthToken);
await maybeLibraries.Match(
libraries =>
{
List<PlexMediaSourceLibrary> existing = connectionParameters.PlexMediaSource.Libraries;
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
existing.AddRange(toAdd);
@ -87,7 +87,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -87,7 +87,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
{
_logger.LogWarning(
"Unable to synchronize libraries from plex server {PlexServer}: {Error}",
connectionParameters.PlexMediaSource.Name,
connectionParameters.PlexMediaSource.ServerName,
error.Value);
return Task.CompletedTask;
@ -98,7 +98,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -98,7 +98,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
private record ConnectionParameters(
PlexMediaSource PlexMediaSource,
PlexMediaSourceConnection ActiveConnection)
PlexConnection ActiveConnection)
{
public PlexServerAuthToken PlexServerAuthToken { get; set; }
}

25
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryById.cs

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Plex.Commands
{
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
{
int PlexMediaSourceId { get; }
int PlexLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizePlexLibraryByIdIfNeeded
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizePlexLibraryById
(int PlexMediaSourceId, int PlexLibraryId) : ISynchronizePlexLibraryById
{
public bool ForceScan => true;
}
}

144
ErsatzTV.Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -0,0 +1,144 @@ @@ -0,0 +1,144 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Plex;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexLibraryByIdHandler : IRequestHandler<ForceSynchronizePlexLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IEntityLocker _entityLocker;
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
private readonly IPlexSecretStore _plexSecretStore;
public SynchronizePlexLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IPlexSecretStore plexSecretStore,
IPlexMovieLibraryScanner plexMovieLibraryScanner,
IEntityLocker entityLocker,
ILogger<SynchronizePlexLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_plexSecretStore = plexSecretStore;
_plexMovieLibraryScanner = plexMovieLibraryScanner;
_entityLocker = entityLocker;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizePlexLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizePlexLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(ISynchronizePlexLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
if (parameters.ForceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
{
switch (parameters.Library.MediaKind)
{
case LibraryMediaKind.Movies:
await _plexMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library);
break;
case LibraryMediaKind.Shows:
// TODO: plex tv scanner
// await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
break;
}
parameters.Library.LastScan = DateTime.UtcNow;
await _mediaSourceRepository.Update(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of plex media library {Name}",
parameters.Library.Name);
}
// _entityLocker.UnlockMediaSource(parameters.MediaSource.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(ISynchronizePlexLibraryById request) =>
(await ValidateConnection(request), await PlexLibraryMustExist(request))
.Apply(
(connectionParameters, plexLibrary) => new RequestParameters(
connectionParameters,
plexLibrary,
request.ForceScan
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizePlexLibraryById request) =>
PlexMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveToken);
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
ISynchronizePlexLibraryById request) =>
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
.Map(v => v.ToValidation<BaseError>($"Plex media source {request.PlexMediaSourceId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
PlexMediaSource plexMediaSource)
{
Option<PlexConnection> maybeConnection =
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive);
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection))
.ToValidation<BaseError>("Plex media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveToken(
ConnectionParameters connectionParameters)
{
Option<PlexServerAuthToken> maybeToken = await
_plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier);
return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token })
.ToValidation<BaseError>("Plex media source requires a token");
}
private Task<Validation<BaseError, PlexLibrary>> PlexLibraryMustExist(
ISynchronizePlexLibraryById request) =>
_mediaSourceRepository.GetPlexLibrary(request.PlexLibraryId)
.Map(v => v.ToValidation<BaseError>($"Plex library {request.PlexLibraryId} does not exist."));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
PlexLibrary Library,
bool ForceScan);
private record ConnectionParameters(
PlexMediaSource PlexMediaSource,
PlexConnection ActiveConnection)
{
public PlexServerAuthToken PlexServerAuthToken { get; set; }
}
}
}

2
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexMediaSources.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSources.cs

@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain; @@ -4,7 +4,7 @@ using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record
SynchronizePlexMediaSources : IRequest<Either<BaseError, List<PlexMediaSource>>>, IPlexBackgroundServiceRequest;

8
ErsatzTV.Application/MediaSources/Commands/SynchronizePlexMediaSourcesHandler.cs → ErsatzTV.Application/Plex/Commands/SynchronizePlexMediaSourcesHandler.cs

@ -9,7 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -9,7 +9,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class
SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePlexMediaSources,
@ -50,7 +50,7 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -50,7 +50,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
existing =>
{
existing.ProductVersion = server.ProductVersion;
existing.Name = server.Name;
existing.ServerName = server.ServerName;
MergeConnections(existing.Connections, server.Connections);
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
{
@ -72,8 +72,8 @@ namespace ErsatzTV.Application.MediaSources.Commands @@ -72,8 +72,8 @@ namespace ErsatzTV.Application.MediaSources.Commands
}
private void MergeConnections(
List<PlexMediaSourceConnection> existing,
List<PlexMediaSourceConnection> incoming)
List<PlexConnection> existing,
List<PlexConnection> incoming)
{
var toAdd = incoming.Filter(connection => existing.All(c => c.Uri != connection.Uri)).ToList();
var toRemove = existing.Filter(connection => incoming.All(c => c.Uri != connection.Uri)).ToList();

2
ErsatzTV.Application/MediaSources/Commands/TryCompletePlexPinFlow.cs → ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlow.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Plex; @@ -3,7 +3,7 @@ using ErsatzTV.Core.Plex;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public record TryCompletePlexPinFlow(PlexAuthPin AuthPin) : IRequest<Either<BaseError, bool>>,
IPlexBackgroundServiceRequest;

2
ErsatzTV.Application/MediaSources/Commands/TryCompletePlexPinFlowHandler.cs → ErsatzTV.Application/Plex/Commands/TryCompletePlexPinFlowHandler.cs

@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Plex;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Commands
namespace ErsatzTV.Application.Plex.Commands
{
public class TryCompletePlexPinFlowHandler : IRequestHandler<TryCompletePlexPinFlow, Either<BaseError, bool>>
{

11
ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferences.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Plex.Commands
{
public record UpdatePlexLibraryPreferences
(List<PlexLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record PlexLibraryPreference(int Id, bool ShouldSyncItems);
}

32
ErsatzTV.Application/Plex/Commands/UpdatePlexLibraryPreferencesHandler.cs

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Plex.Commands
{
public class
UpdatePlexLibraryPreferencesHandler : MediatR.IRequestHandler<UpdatePlexLibraryPreferences,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlexLibraryPreferences request,
CancellationToken cancellationToken)
{
IEnumerable<int> toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id);
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
return Unit.Default;
}
}
}

18
ErsatzTV.Application/Plex/Mapper.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Plex
{
internal static class Mapper
{
internal static PlexMediaSourceViewModel ProjectToViewModel(PlexMediaSource plexMediaSource) =>
new(
plexMediaSource.Id,
plexMediaSource.ServerName,
Optional(plexMediaSource.Connections.SingleOrDefault(c => c.IsActive)).Match(c => c.Uri, string.Empty));
internal static PlexLibraryViewModel ProjectToViewModel(PlexLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
}
}

6
ErsatzTV.Application/Plex/PlexLibraryViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Plex
{
public record PlexLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems);
}

6
ErsatzTV.Application/Plex/PlexMediaSourceViewModel.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Application.MediaSources;
namespace ErsatzTV.Application.Plex
{
public record PlexMediaSourceViewModel(int Id, string Name, string Address) : MediaSourceViewModel(Id, Name);
}

2
ErsatzTV.Application/MediaSources/Queries/GetAllPlexMediaSources.cs → ErsatzTV.Application/Plex/Queries/GetAllPlexMediaSources.cs

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.MediaSources.Queries
namespace ErsatzTV.Application.Plex.Queries
{
public record GetAllPlexMediaSources : IRequest<List<PlexMediaSourceViewModel>>;
}

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

Loading…
Cancel
Save