Compare commits
39 Commits
v0.0.42-pr
...
v0.0.47-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc6cdd0b7 | ||
|
|
cebab33d79 | ||
|
|
b580125e86 | ||
|
|
b38ba14c40 | ||
|
|
c10bc6b184 | ||
|
|
a75737a032 | ||
|
|
57aa14b764 | ||
|
|
6e6d5a133f | ||
|
|
b4ba37f778 | ||
|
|
275f82fcc9 | ||
|
|
72d967946d | ||
|
|
a0740de972 | ||
|
|
e69569ea46 | ||
|
|
679feb6d21 | ||
|
|
0fb5bfde58 | ||
|
|
4172074ac4 | ||
|
|
e9889cefd6 | ||
|
|
fc59c9c284 | ||
|
|
0750a0712f | ||
|
|
0365d4c8f8 | ||
|
|
5b36252dd0 | ||
|
|
7d852bc960 | ||
|
|
cdf10b0535 | ||
|
|
f0b429efb5 | ||
|
|
da5148affd | ||
|
|
cec5a09839 | ||
|
|
e20f9be702 | ||
|
|
3bc3faa7c4 | ||
|
|
db24ba84f7 | ||
|
|
8346a02747 | ||
|
|
c3b33c184f | ||
|
|
6bec9c5f07 | ||
|
|
0ef03d66f3 | ||
|
|
10c422a3eb | ||
|
|
6c867d0d51 | ||
|
|
ed0796ad58 | ||
|
|
49109ac121 | ||
|
|
3e3bbcf38e | ||
|
|
ce9ef72799 |
83
CHANGELOG.md
83
CHANGELOG.md
@@ -5,6 +5,82 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.0.47-prealpha] - 2021-06-15
|
||||
### Added
|
||||
- Add warning during playout rebuild when schedule has been emptied
|
||||
- Save Logs, Playout Detail, Schedule Detail table page sizes
|
||||
|
||||
### Changed
|
||||
- Show all log entries in log viewer, not just most recent 100 entries
|
||||
- Use server-side paging and sorting for Logs table
|
||||
- Use server-side paging for Playout Detail table
|
||||
- Remove pager from Schedule Items editor (all schedule items will always be displayed)
|
||||
|
||||
### Fixed
|
||||
- Fix ui crash adding a channel without a watermark
|
||||
- Clear playout detail table when playout is deleted
|
||||
- Fix blazor error font color
|
||||
- Fix some audio stream languages missing from UI and search index
|
||||
- Fix audio stream selection for languages with multiple codes
|
||||
- Fix searching when queries contain non-ascii characters
|
||||
|
||||
## [0.0.46-prealpha] - 2021-06-14
|
||||
### Added
|
||||
- Add watermark opacity setting to allow blending with content
|
||||
- Add global watermark setting; channel-specific watermarks have precedence over global watermarks
|
||||
- Save Schedules, Playouts table page sizes
|
||||
|
||||
### Changed
|
||||
- Remove unused API and SDK project; may reintroduce in the future but for now they have fallen out of date
|
||||
- Rework watermarks to be separate from channels (similar to ffmpeg profiles)
|
||||
- **All existing watermarks have been removed and will need to be recreated using the new page**
|
||||
- This allows easy watermark reuse across channels
|
||||
|
||||
### Fixed
|
||||
- Fix ui crash adding or editing schedule items due to Artist with no name
|
||||
- Fix many potential sources of inconsistent data in UI
|
||||
|
||||
## [0.0.45-prealpha] - 2021-06-12
|
||||
### Added
|
||||
- Add experimental `HLS Hybrid` channel mode
|
||||
- Media items are transcoded using the channel's ffmpeg profile and served using HLS
|
||||
- Add optional channel watermark
|
||||
|
||||
### Changed
|
||||
- Remove framerate normalization; it caused more problems than it solved
|
||||
- Include non-US (and unknown) content ratings in XMLTV
|
||||
|
||||
### Fixed
|
||||
- Fix serving channels.m3u with missing content ratings
|
||||
- Fix percent progress indicator for Jellyfin and Emby show library scans
|
||||
|
||||
## [0.0.44-prealpha] - 2021-06-09
|
||||
### Added
|
||||
- Add artists directly to schedules
|
||||
- Include MPAA and VCHIP content ratings in XMLTV guide data
|
||||
- Quickly skip missing files during Plex library scan
|
||||
|
||||
### Fixed
|
||||
- Ignore unsupported plex guids (this prevented some libraries from scanning correctly)
|
||||
- Ignore unsupported STRM files from Jellyfin
|
||||
|
||||
## [0.0.43-prealpha] - 2021-06-05
|
||||
### Added
|
||||
- Support `(Part #)` name suffixes for multi-part episode grouping
|
||||
- Support multi-episode files in local and Plex libraries
|
||||
- Save Channels table page size
|
||||
- Add optional query string parameter to M3U channel playlist to allow some customization per client
|
||||
- `?mode=ts` will force `MPEG-TS` mode for all channels
|
||||
- `?mode=hls-direct` will force `HLS Direct` mode for all channels
|
||||
- `?mode=mixed` or no parameter will maintain existing behavior
|
||||
|
||||
### Changed
|
||||
- Rename channel mode `TransportStream` to `MPEG-TS` and `HttpLiveStreaming` to `HLS Direct`
|
||||
- Improve `HLS Direct` mode compatibility with Channels DVR Server
|
||||
|
||||
### Fixed
|
||||
- Fix search result crashes due to missing season metadata
|
||||
|
||||
## [0.0.42-prealpha] - 2021-05-31
|
||||
### Added
|
||||
- Support roman numerals and english integer names for multi-part episode grouping
|
||||
@@ -392,7 +468,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.42-prealpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.47-prealpha...HEAD
|
||||
[0.0.47-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.46-prealpha...v0.0.47-prealpha
|
||||
[0.0.46-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.45-prealpha...v0.0.46-prealpha
|
||||
[0.0.45-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.44-prealpha...v0.0.45-prealpha
|
||||
[0.0.44-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.43-prealpha...v0.0.44-prealpha
|
||||
[0.0.43-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.42-prealpha...v0.0.43-prealpha
|
||||
[0.0.42-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.41-prealpha...v0.0.42-prealpha
|
||||
[0.0.41-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.40-prealpha...v0.0.41-prealpha
|
||||
[0.0.40-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.39-prealpha...v0.0.40-prealpha
|
||||
|
||||
8
ErsatzTV.Application/Artists/Queries/GetAllArtists.cs
Normal file
8
ErsatzTV.Application/Artists/Queries/GetAllArtists.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
|
||||
}
|
||||
29
ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs
Normal file
29
ErsatzTV.Application/Artists/Queries/GetAllArtistsHandler.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
|
||||
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
|
||||
|
||||
public Task<List<NamedMediaItemViewModel>> Handle(
|
||||
GetAllArtists request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_artistRepository.GetAllArtists()
|
||||
.Map(
|
||||
list => list.Filter(
|
||||
a => !string.IsNullOrWhiteSpace(
|
||||
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,9 @@ namespace ErsatzTV.Application.Artists.Queries
|
||||
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
|
||||
async artist =>
|
||||
{
|
||||
List<string> languages = await _searchRepository.GetLanguagesForArtist(artist);
|
||||
return ProjectToViewModel(artist, languages);
|
||||
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
|
||||
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
|
||||
return ProjectToViewModel(artist, languageCodes);
|
||||
},
|
||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Channels
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode);
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId);
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
}
|
||||
|
||||
@@ -7,42 +7,44 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, ChannelViewModel>>
|
||||
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
|
||||
{
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreateChannelHandler(
|
||||
IChannelRepository channelRepository,
|
||||
IFFmpegProfileRepository ffmpegProfileRepository)
|
||||
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, CreateChannelResult>> Handle(
|
||||
CreateChannel request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_channelRepository = channelRepository;
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistChannel(dbContext, c));
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
CreateChannel request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PersistChannel)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
|
||||
{
|
||||
await dbContext.Channels.AddAsync(channel);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return new CreateChannelResult(channel.Id);
|
||||
}
|
||||
|
||||
private Task<ChannelViewModel> PersistChannel(Channel c) =>
|
||||
_channelRepository.Add(c).Map(ProjectToViewModel);
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
|
||||
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request),
|
||||
ValidatePreferredLanguage(request))
|
||||
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
|
||||
(ValidateName(request), await ValidateNumber(dbContext, request),
|
||||
await FFmpegProfileMustExist(dbContext, request),
|
||||
ValidatePreferredLanguage(request),
|
||||
await WatermarkMustExist(dbContext, request))
|
||||
.Apply(
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode) =>
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
@@ -57,7 +59,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
});
|
||||
}
|
||||
|
||||
return new Channel(Guid.NewGuid())
|
||||
var channel = new Channel(Guid.NewGuid())
|
||||
{
|
||||
Name = name,
|
||||
Number = number,
|
||||
@@ -66,22 +68,30 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
Artwork = artwork,
|
||||
PreferredLanguageCode = preferredLanguageCode
|
||||
};
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
{
|
||||
channel.WatermarkId = id;
|
||||
}
|
||||
|
||||
return channel;
|
||||
});
|
||||
|
||||
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
|
||||
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
|
||||
createChannel.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||
private static Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
||||
Option<Channel> maybeExistingChannel = await dbContext.Channels
|
||||
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
|
||||
return maybeExistingChannel.Match<Validation<BaseError, string>>(
|
||||
_ => BaseError.New("Channel number must be unique"),
|
||||
() =>
|
||||
@@ -95,9 +105,31 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) =>
|
||||
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId))
|
||||
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
|
||||
TvContext dbContext,
|
||||
CreateChannel createChannel) =>
|
||||
dbContext.FFmpegProfiles
|
||||
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
|
||||
.Map(Optional)
|
||||
.Filter(c => c > 0)
|
||||
.MapT(_ => createChannel.FFmpegProfileId)
|
||||
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
|
||||
|
||||
private static async Task<Validation<BaseError, Option<int>>> WatermarkMustExist(
|
||||
TvContext dbContext,
|
||||
CreateChannel createChannel)
|
||||
{
|
||||
if (createChannel.WatermarkId is null)
|
||||
{
|
||||
return Option<int>.None;
|
||||
}
|
||||
|
||||
return await dbContext.ChannelWatermarks
|
||||
.CountAsync(w => w.Id == createChannel.WatermarkId)
|
||||
.Map(Optional)
|
||||
.Filter(c => c > 0)
|
||||
.MapT(_ => Optional(createChannel.WatermarkId))
|
||||
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
|
||||
}
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
@@ -17,28 +19,30 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||
{
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public UpdateChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
UpdateChannel request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(c => ApplyUpdateRequest(c, request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<ChannelViewModel> ApplyUpdateRequest(Channel c, UpdateChannel update)
|
||||
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
||||
{
|
||||
c.Name = update.Name;
|
||||
c.Number = update.Number;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.PreferredLanguageCode = update.PreferredLanguageCode;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
{
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
|
||||
Option<Artwork> maybeLogo =
|
||||
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
|
||||
|
||||
@@ -60,29 +64,40 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
c.Artwork.Add(artwork);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
await _channelRepository.Update(c);
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ProjectToViewModel(c);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
|
||||
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
|
||||
(await ChannelMustExist(dbContext, request), ValidateName(request),
|
||||
await ValidateNumber(dbContext, request),
|
||||
ValidatePreferredLanguage(request))
|
||||
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
||||
_channelRepository.Get(updateChannel.ChannelId)
|
||||
.Map(v => v.ToValidation<BaseError>("Channel does not exist."));
|
||||
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateChannel updateChannel) =>
|
||||
dbContext.Channels
|
||||
.Include(c => c.Artwork)
|
||||
.Include(c => c.Watermark)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
|
||||
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
|
||||
|
||||
private Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
|
||||
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
|
||||
updateChannel.NotEmpty(c => c.Name)
|
||||
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(
|
||||
TvContext dbContext,
|
||||
UpdateChannel updateChannel)
|
||||
{
|
||||
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
|
||||
int matchId = await match.Map(c => c.Id).IfNoneAsync(updateChannel.ChannelId);
|
||||
int matchId = await dbContext.Channels
|
||||
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
|
||||
.Match(c => c.Id, () => updateChannel.ChannelId);
|
||||
|
||||
if (matchId == updateChannel.ChannelId)
|
||||
{
|
||||
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
|
||||
@@ -96,7 +111,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
return BaseError.New("Channel number must be unique");
|
||||
}
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.Filter(
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
|
||||
@@ -14,10 +14,15 @@ namespace ErsatzTV.Application.Channels
|
||||
channel.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.PreferredLanguageCode,
|
||||
channel.StreamingMode);
|
||||
channel.StreamingMode,
|
||||
channel.WatermarkId);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
private static string GetWatermark(Channel channel) =>
|
||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Channels.Queries
|
||||
{
|
||||
public record GetChannelPlaylist(string Scheme, string Host) : IRequest<ChannelPlaylist>;
|
||||
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using LanguageExt;
|
||||
@@ -16,6 +18,31 @@ namespace ErsatzTV.Application.Channels.Queries
|
||||
|
||||
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => EnsureMode(channels, request.Mode))
|
||||
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
|
||||
|
||||
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
|
||||
{
|
||||
var result = new List<Channel>();
|
||||
foreach (Channel channel in channels)
|
||||
{
|
||||
switch (mode.ToLowerInvariant())
|
||||
{
|
||||
case "hls-direct":
|
||||
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
||||
result.Add(channel);
|
||||
break;
|
||||
case "ts":
|
||||
channel.StreamingMode = StreamingMode.TransportStream;
|
||||
result.Add(channel);
|
||||
break;
|
||||
default:
|
||||
result.Add(channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
@@ -15,19 +14,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
||||
|
||||
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(request.Key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = request.Key.Key, Value = request.Value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
|
||||
await _configElementRepository.Upsert(request.Key, request.Value);
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@ using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Commands
|
||||
{
|
||||
public class
|
||||
UpdateLibraryRefreshIntervalHandler : MediatR.IRequestHandler<UpdateLibraryRefreshInterval,
|
||||
Either<BaseError, Unit>>
|
||||
public class UpdateLibraryRefreshIntervalHandler :
|
||||
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
@@ -21,27 +20,14 @@ namespace ErsatzTV.Application.Configuration.Commands
|
||||
UpdateLibraryRefreshInterval request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval.ToString()))
|
||||
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||
Optional(request.LibraryRefreshInterval)
|
||||
.Filter(lri => lri > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
}).ToUnit();
|
||||
}
|
||||
}
|
||||
|
||||
4
ErsatzTV.Application/EntityIdResult.cs
Normal file
4
ErsatzTV.Application/EntityIdResult.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application
|
||||
{
|
||||
public record EntityIdResult(int Id);
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,6 +21,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
|
||||
}
|
||||
|
||||
@@ -2,39 +2,42 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public class
|
||||
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||
public class CreateFFmpegProfileHandler :
|
||||
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IResolutionRepository _resolutionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreateFFmpegProfileHandler(
|
||||
IFFmpegProfileRepository ffmpegProfileRepository,
|
||||
IResolutionRepository resolutionRepository)
|
||||
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
|
||||
CreateFFmpegProfile request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
_resolutionRepository = resolutionRepository;
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||
CreateFFmpegProfile request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PersistFFmpegProfile)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
||||
TvContext dbContext,
|
||||
FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
|
||||
}
|
||||
|
||||
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) =>
|
||||
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel);
|
||||
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(CreateFFmpegProfile request) =>
|
||||
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request))
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
|
||||
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
|
||||
.Apply(
|
||||
(name, threadCount, resolutionId) => new FFmpegProfile
|
||||
{
|
||||
@@ -53,20 +56,22 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
NormalizeLoudness = request.NormalizeLoudness,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeAudio = request.NormalizeAudio,
|
||||
FrameRate = request.FrameRate
|
||||
NormalizeAudio = request.NormalizeAudio
|
||||
});
|
||||
|
||||
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
createFFmpegProfile.NotEmpty(x => x.Name)
|
||||
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
|
||||
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private static Task<Validation<BaseError, int>> ResolutionMustExist(
|
||||
TvContext dbContext,
|
||||
CreateFFmpegProfile createFFmpegProfile) =>
|
||||
dbContext.Resolutions
|
||||
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId)
|
||||
.MapT(r => r.Id)
|
||||
.Map(o => o.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Task>>
|
||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public DeleteFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, Task>> Handle(
|
||||
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
|
||||
DeleteFFmpegProfile request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await FFmpegProfileMustExist(request))
|
||||
.Map(DoDeletion)
|
||||
.ToEither<Task>();
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
|
||||
return await validation.Apply(p => DoDeletion(dbContext, p));
|
||||
}
|
||||
|
||||
private Task DoDeletion(int channelId) => _ffmpegProfileRepository.Delete(channelId);
|
||||
private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return LanguageExt.Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(
|
||||
DeleteFFmpegProfile deleteFFmpegProfile) =>
|
||||
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId))
|
||||
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
|
||||
TvContext dbContext,
|
||||
DeleteFFmpegProfile request) =>
|
||||
dbContext.FFmpegProfiles
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId)
|
||||
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {request.FFmpegProfileId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
|
||||
@@ -12,24 +14,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IResolutionRepository _resolutionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public NewFFmpegProfileHandler(
|
||||
IResolutionRepository resolutionRepository,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_resolutionRepository = resolutionRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
|
||||
{
|
||||
int defaultResolutionId = await _configElementRepository
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
int defaultResolutionId = await dbContext.ConfigElements
|
||||
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
|
||||
.IfNoneAsync(0);
|
||||
|
||||
List<Resolution> allResolutions = await _resolutionRepository.GetAll();
|
||||
List<Resolution> allResolutions = await dbContext.Resolutions
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
|
||||
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());
|
||||
|
||||
@@ -22,6 +22,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
|
||||
}
|
||||
|
||||
@@ -2,35 +2,35 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public class
|
||||
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
|
||||
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IResolutionRepository _resolutionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public UpdateFFmpegProfileHandler(
|
||||
IFFmpegProfileRepository ffmpegProfileRepository,
|
||||
IResolutionRepository resolutionRepository)
|
||||
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
|
||||
UpdateFFmpegProfile request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
_resolutionRepository = resolutionRepository;
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
|
||||
UpdateFFmpegProfile request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(c => ApplyUpdateRequest(c, request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<FFmpegProfileViewModel> ApplyUpdateRequest(FFmpegProfile p, UpdateFFmpegProfile update)
|
||||
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||
TvContext dbContext,
|
||||
FFmpegProfile p,
|
||||
UpdateFFmpegProfile update)
|
||||
{
|
||||
p.Name = update.Name;
|
||||
p.ThreadCount = update.ThreadCount;
|
||||
@@ -48,31 +48,37 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
p.AudioChannels = update.AudioChannels;
|
||||
p.AudioSampleRate = update.AudioSampleRate;
|
||||
p.NormalizeAudio = update.NormalizeAudio;
|
||||
p.FrameRate = update.FrameRate;
|
||||
await _ffmpegProfileRepository.Update(p);
|
||||
return ProjectToViewModel(p);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return new UpdateFFmpegProfileResult(p.Id);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) =>
|
||||
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request),
|
||||
await ResolutionMustExist(request))
|
||||
private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
|
||||
TvContext dbContext,
|
||||
UpdateFFmpegProfile request) =>
|
||||
(await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request),
|
||||
await ResolutionMustExist(dbContext, request))
|
||||
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
|
||||
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
|
||||
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId))
|
||||
.ToValidation<BaseError>("FFmpegProfile does not exist.");
|
||||
dbContext.FFmpegProfiles
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId)
|
||||
.Map(o => o.ToValidation<BaseError>("FFmpegProfile does not exist."));
|
||||
|
||||
private Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
private static Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
updateFFmpegProfile.NotEmpty(x => x.Name)
|
||||
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
private static Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
|
||||
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private static Task<Validation<BaseError, int>> ResolutionMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
dbContext.Resolutions
|
||||
.SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId)
|
||||
.MapT(r => r.Id)
|
||||
.Map(o => o.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
{
|
||||
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
}
|
||||
@@ -86,35 +86,36 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
|
||||
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
|
||||
{
|
||||
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
|
||||
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
|
||||
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString());
|
||||
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString());
|
||||
await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
|
||||
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegDefaultProfileId,
|
||||
request.Settings.DefaultFFmpegProfileId.ToString());
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegSaveReports,
|
||||
request.Settings.SaveReports.ToString());
|
||||
|
||||
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
|
||||
}
|
||||
|
||||
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode);
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegPreferredLanguageCode,
|
||||
request.Settings.PreferredLanguageCode);
|
||||
|
||||
if (request.Settings.GlobalWatermarkId is not null)
|
||||
{
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.FFmpegGlobalWatermarkId,
|
||||
request.Settings.GlobalWatermarkId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task Upsert(ConfigElementKey key, string value)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,5 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio,
|
||||
string FrameRate);
|
||||
bool NormalizeAudio);
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
public int DefaultFFmpegProfileId { get; set; }
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
public bool SaveReports { get; set; }
|
||||
public int? GlobalWatermarkId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
profile.NormalizeLoudness,
|
||||
profile.AudioChannels,
|
||||
profile.AudioSampleRate,
|
||||
profile.NormalizeAudio,
|
||||
profile.FrameRate);
|
||||
profile.NormalizeAudio);
|
||||
|
||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
{
|
||||
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<List<FFmpegProfileViewModel>> Handle(
|
||||
GetAllFFmpegProfiles request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.FFmpegProfiles
|
||||
.Include(p => p.Resolution)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
{
|
||||
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
|
||||
{
|
||||
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
|
||||
_ffmpegProfileRepository = ffmpegProfileRepository;
|
||||
public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<Option<FFmpegProfileViewModel>> Handle(
|
||||
public async Task<Option<FFmpegProfileViewModel>> Handle(
|
||||
GetFFmpegProfileById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_ffmpegProfileRepository.Get(request.Id)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.FFmpegProfiles
|
||||
.Include(p => p.Resolution)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,24 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
||||
Option<string> preferredLanguageCode =
|
||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||
Option<int> watermark =
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||
|
||||
return new FFmpegSettingsViewModel
|
||||
var result = new FFmpegSettingsViewModel
|
||||
{
|
||||
FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty),
|
||||
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
|
||||
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
||||
SaveReports = await saveReports.IfNoneAsync(false),
|
||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng")
|
||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
|
||||
};
|
||||
|
||||
foreach (int watermarkId in watermark)
|
||||
{
|
||||
result.GlobalWatermarkId = watermarkId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,27 +19,14 @@ namespace ErsatzTV.Application.HDHR.Commands
|
||||
UpdateHDHRTunerCount request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
|
||||
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
Optional(request.TunerCount)
|
||||
.Filter(tc => tc > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
}).ToUnit();
|
||||
}
|
||||
}
|
||||
|
||||
6
ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs
Normal file
6
ErsatzTV.Application/Logs/PagedLogEntriesViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
public record PagedLogEntriesViewModel(int TotalCount, List<LogEntryViewModel> Page);
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq.Expressions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Logs.Queries
|
||||
{
|
||||
public record GetRecentLogEntries : IRequest<List<LogEntryViewModel>>;
|
||||
public record GetRecentLogEntries(int PageNum, int PageSize) : IRequest<PagedLogEntriesViewModel>
|
||||
{
|
||||
public Expression<Func<LogEntry, object>> SortExpression { get; set; }
|
||||
public Option<bool> SortDescending { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,46 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Logs.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Logs.Queries
|
||||
{
|
||||
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, List<LogEntryViewModel>>
|
||||
public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, PagedLogEntriesViewModel>
|
||||
{
|
||||
private readonly ILogRepository _logRepository;
|
||||
private readonly IDbContextFactory<LogContext> _dbContextFactory;
|
||||
|
||||
public GetRecentLogEntriesHandler(ILogRepository logRepository) => _logRepository = logRepository;
|
||||
public GetRecentLogEntriesHandler(IDbContextFactory<LogContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<List<LogEntryViewModel>> Handle(GetRecentLogEntries request, CancellationToken cancellationToken) =>
|
||||
_logRepository.GetRecentLogEntries().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
public async Task<PagedLogEntriesViewModel> Handle(
|
||||
GetRecentLogEntries request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using LogContext logContext = _dbContextFactory.CreateDbContext();
|
||||
int count = await logContext.LogEntries.CountAsync(cancellationToken);
|
||||
|
||||
IOrderedQueryable<LogEntry> ordered = logContext.LogEntries
|
||||
.OrderByDescending(le => le.Id);
|
||||
|
||||
foreach (bool descending in request.SortDescending)
|
||||
{
|
||||
ordered = descending
|
||||
? logContext.LogEntries.OrderByDescending(request.SortExpression).ThenByDescending(le => le.Id)
|
||||
: logContext.LogEntries.OrderBy(request.SortExpression).ThenByDescending(le => le.Id);
|
||||
}
|
||||
|
||||
List<LogEntryViewModel> page = await ordered
|
||||
.Skip(request.PageNum * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new PagedLogEntriesViewModel(count, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,17 +50,14 @@ namespace ErsatzTV.Application.MediaCards
|
||||
episodeMetadata.Episode.Season.ShowId,
|
||||
episodeMetadata.Episode.SeasonId,
|
||||
episodeMetadata.Episode.Season.SeasonNumber,
|
||||
episodeMetadata.Episode.EpisodeNumber,
|
||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, () => 0),
|
||||
episodeMetadata.Title,
|
||||
episodeMetadata.SortTitle,
|
||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
||||
em => em.Plot ?? string.Empty,
|
||||
() => string.Empty),
|
||||
isSearchResult
|
||||
? GetPoster(
|
||||
episodeMetadata.Episode.Season.SeasonMetadata.Head(),
|
||||
maybeJellyfin,
|
||||
maybeEmby)
|
||||
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
||||
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
||||
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
||||
episodeMetadata.Writers.Map(w => w.Name).ToList());
|
||||
@@ -146,6 +143,24 @@ namespace ErsatzTV.Application.MediaCards
|
||||
private static string GetSeasonName(int number) =>
|
||||
number == 0 ? "Specials" : $"Season {number}";
|
||||
|
||||
private static string GetEpisodePoster(
|
||||
EpisodeMetadata episodeMetadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
Option<SeasonMetadata> maybeSeasonMetadata = episodeMetadata.Episode.Season.SeasonMetadata.HeadOrNone();
|
||||
return maybeSeasonMetadata.Match(
|
||||
seasonMetadata => GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
||||
() =>
|
||||
{
|
||||
Option<ShowMetadata> maybeShowMetadata =
|
||||
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone();
|
||||
return maybeShowMetadata.Match(
|
||||
showMetadata => GetPoster(showMetadata, maybeJellyfin, maybeEmby),
|
||||
() => string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetPoster(
|
||||
Metadata metadata,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
|
||||
@@ -3,23 +3,26 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class GetCollectionCardsHandler : IRequestHandler<GetCollectionCards,
|
||||
Either<BaseError, CollectionCardResultsViewModel>>
|
||||
public class GetCollectionCardsHandler :
|
||||
IRequestHandler<GetCollectionCards, Either<BaseError, CollectionCardResultsViewModel>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _collectionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetCollectionCardsHandler(
|
||||
IMediaCollectionRepository collectionRepository,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaSourceRepository mediaSourceRepository)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
}
|
||||
|
||||
@@ -27,14 +30,57 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
||||
GetCollectionCards request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<EmbyMediaSource> maybeEmby = await _mediaSourceRepository.GetAllEmby()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
return await _collectionRepository
|
||||
.GetCollectionWithItemsUntracked(request.Id)
|
||||
return await dbContext.Collections
|
||||
.AsNoTracking()
|
||||
.Include(c => c.CollectionItems)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => i.LibraryPath)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||
.ThenInclude(mm => mm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
|
||||
.ThenInclude(mvm => mvm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as MusicVideo).Artist)
|
||||
.ThenInclude(a => a.ArtistMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Show).ShowMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Season).SeasonMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Season).Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Directors)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).EpisodeMetadata)
|
||||
.ThenInclude(em => em.Writers)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).Season)
|
||||
.ThenInclude(s => s.SeasonMetadata)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||
}
|
||||
|
||||
@@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddArtistToCollectionHandler : MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
|
||||
public class AddArtistToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public AddArtistToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_artistRepository = artistRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddArtistToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddArtistRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddArtistRequest(AddArtistToCollection request)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ArtistId))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddArtistRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddArtistRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.Artist);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddArtistToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateArtist(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddArtistToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateArtist(dbContext, request))
|
||||
.Apply((collection, artist) => new Parameters(collection, artist));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddArtistToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddArtistToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateArtist(AddArtistToCollection request) =>
|
||||
LoadArtist(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
private static Task<Validation<BaseError, Artist>> ValidateArtist(
|
||||
TvContext dbContext,
|
||||
AddArtistToCollection request) =>
|
||||
dbContext.Artists
|
||||
.SelectOneAsync(a => a.Id, a => a.Id == request.ArtistId)
|
||||
.Map(o => o.ToValidation<BaseError>("Music video does not exist"));
|
||||
|
||||
private Task<Option<Artist>> LoadArtist(AddArtistToCollection request) =>
|
||||
_artistRepository.GetArtist(request.ArtistId);
|
||||
private record Parameters(Collection Collection, Artist Artist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,42 +3,49 @@ using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddEpisodeToCollectionHandler : MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
|
||||
public class AddEpisodeToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public AddEpisodeToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddEpisodeToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddTelevisionEpisodeRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(AddEpisodeToCollection request)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.EpisodeId))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddTelevisionEpisodeRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddTelevisionEpisodeRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.Episode);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -47,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddEpisodeToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateEpisode(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddEpisodeToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateEpisode(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
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 static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddEpisodeToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.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 static Task<Validation<BaseError, Episode>> ValidateEpisode(
|
||||
TvContext dbContext,
|
||||
AddEpisodeToCollection request) =>
|
||||
dbContext.Episodes
|
||||
.SelectOneAsync(e => e.Id, e => e.Id == request.EpisodeId)
|
||||
.Map(o => o.ToValidation<BaseError>("Episode does not exist"));
|
||||
|
||||
private Task<Option<int>> LoadTelevisionEpisode(AddEpisodeToCollection request) =>
|
||||
_televisionRepository.GetEpisode(request.EpisodeId).MapT(e => e.Id);
|
||||
private record Parameters(Collection Collection, Episode Episode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,54 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddItemsToCollectionHandler : MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
|
||||
public class AddItemsToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public AddItemsToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMovieRepository movieRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_movieRepository = movieRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddItemsToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddItemsRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
private async Task<Unit> ApplyAddItemsRequest(AddItemsToCollection request)
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyAddItemsRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddItemsRequest(TvContext dbContext, Collection collection, AddItemsToCollection request)
|
||||
{
|
||||
var allItems = request.MovieIds
|
||||
.Append(request.ShowIds)
|
||||
@@ -46,7 +57,14 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Append(request.MusicVideoIds)
|
||||
.ToList();
|
||||
|
||||
if (await _mediaCollectionRepository.AddMediaItems(request.CollectionId, allItems))
|
||||
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
||||
List<MediaItem> toAdd = await dbContext.MediaItems
|
||||
.Filter(mi => toAddIds.Contains(mi.Id))
|
||||
.ToListAsync();
|
||||
|
||||
collection.MediaItems.AddRange(toAdd);
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
@@ -59,15 +77,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddItemsToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateMovies(request), await ValidateShows(request),
|
||||
private async Task<Validation<BaseError, Collection>> Validate(TvContext dbContext, AddItemsToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request),
|
||||
await ValidateMovies(request),
|
||||
await ValidateShows(request),
|
||||
await ValidateEpisodes(request))
|
||||
.Apply((_, _, _, _) => Unit.Default);
|
||||
.Apply((collection, _, _, _) => collection);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddItemsToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddItemsToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateMovies(AddItemsToCollection request) =>
|
||||
_movieRepository.AllMoviesExist(request.MovieIds)
|
||||
@@ -88,6 +111,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
.Map(Optional)
|
||||
.Filter(v => v == true)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
||||
.Map(v => v.ToValidation<BaseError>("Episode does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddMovieToCollectionHandler : MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
|
||||
public class AddMovieToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
|
||||
public AddMovieToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMovieRepository movieRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_movieRepository = movieRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddMovieToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddMoviesRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddMoviesRequest(AddMovieToCollection request)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MovieId))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddMovieRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddMovieRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.Movie);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddMovieToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateMovies(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddMovieToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateMovie(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
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 static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddMovieToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.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 static Task<Validation<BaseError, Movie>> ValidateMovie(
|
||||
TvContext dbContext,
|
||||
AddMovieToCollection request) =>
|
||||
dbContext.Movies
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.MovieId)
|
||||
.Map(o => o.ToValidation<BaseError>("Movie does not exist"));
|
||||
|
||||
private Task<Option<Movie>> LoadMovie(AddMovieToCollection request) =>
|
||||
_movieRepository.GetMovie(request.MovieId);
|
||||
private record Parameters(Collection Collection, Movie Movie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
|
||||
public class AddMusicVideoToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
|
||||
public AddMusicVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddMusicVideoToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddMusicVideoRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddMusicVideoRequest(AddMusicVideoToCollection request)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddMusicVideoRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddMusicVideoRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.MusicVideo);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -48,21 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateMusicVideo(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddMusicVideoToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateMusicVideo(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMusicVideoToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddMusicVideoToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateMusicVideo(AddMusicVideoToCollection request) =>
|
||||
LoadMusicVideo(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
private static Task<Validation<BaseError, MusicVideo>> ValidateMusicVideo(
|
||||
TvContext dbContext,
|
||||
AddMusicVideoToCollection request) =>
|
||||
dbContext.MusicVideos
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.MusicVideoId)
|
||||
.Map(o => o.ToValidation<BaseError>("MusicVideo does not exist"));
|
||||
|
||||
private Task<Option<MusicVideo>> LoadMusicVideo(AddMusicVideoToCollection request) =>
|
||||
_musicVideoRepository.GetMusicVideo(request.MusicVideoId);
|
||||
private record Parameters(Collection Collection, MusicVideo MusicVideo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,41 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddSeasonToCollectionHandler : MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
|
||||
public class AddSeasonToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public AddSeasonToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async 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)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.SeasonId))
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddSeasonRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddSeasonRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.Season);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -48,22 +54,27 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
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(
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddSeasonToCollection request) =>
|
||||
_televisionRepository.GetSeason(request.SeasonId);
|
||||
(await CollectionMustExist(dbContext, request), await ValidateSeason(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddSeasonToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private static Task<Validation<BaseError, Season>> ValidateSeason(
|
||||
TvContext dbContext,
|
||||
AddSeasonToCollection request) =>
|
||||
dbContext.Seasons
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.SeasonId)
|
||||
.Map(o => o.ToValidation<BaseError>("Season does not exist"));
|
||||
|
||||
private record Parameters(Collection Collection, Season Season);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,65 +5,76 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class AddShowToCollectionHandler : MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
|
||||
public class AddShowToCollectionHandler :
|
||||
MediatR.IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public AddShowToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddShowToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddTelevisionShowRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddTelevisionShowRequest(AddShowToCollection request)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new Unit();
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddShowRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ShowId))
|
||||
private async Task<Unit> ApplyAddShowRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.Show);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddShowToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateShow(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddShowToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateShow(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
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 static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddShowToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.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 static Task<Validation<BaseError, Show>> ValidateShow(
|
||||
TvContext dbContext,
|
||||
AddShowToCollection request) =>
|
||||
dbContext.Shows
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.ShowId)
|
||||
.Map(o => o.ToValidation<BaseError>("Show does not exist"));
|
||||
|
||||
private Task<Option<Show>> LoadTelevisionShow(AddShowToCollection request) =>
|
||||
_televisionRepository.GetShow(request.ShowId);
|
||||
private record Parameters(Collection Collection, Show Show);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,60 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
CreateCollectionHandler : IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>>
|
||||
public class CreateCollectionHandler :
|
||||
IRequestHandler<CreateCollection, Either<BaseError, MediaCollectionViewModel>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
public CreateCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<Either<BaseError, MediaCollectionViewModel>> Handle(
|
||||
public async Task<Either<BaseError, MediaCollectionViewModel>> Handle(
|
||||
CreateCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request).MapT(PersistCollection).Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistCollection(dbContext, c));
|
||||
}
|
||||
|
||||
private Task<MediaCollectionViewModel> PersistCollection(Collection c) =>
|
||||
_mediaCollectionRepository.Add(c).Map(ProjectToViewModel);
|
||||
private static async Task<MediaCollectionViewModel> PersistCollection(
|
||||
TvContext dbContext,
|
||||
Collection collection)
|
||||
{
|
||||
await dbContext.Collections.AddAsync(collection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return ProjectToViewModel(collection);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, Collection>> Validate(CreateCollection request) =>
|
||||
ValidateName(request).MapT(
|
||||
private static Task<Validation<BaseError, Collection>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateCollection request) =>
|
||||
ValidateName(dbContext, request).MapT(
|
||||
name => new Collection
|
||||
{
|
||||
Name = name,
|
||||
MediaItems = new List<MediaItem>()
|
||||
});
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateName(CreateCollection createCollection)
|
||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||
TvContext dbContext,
|
||||
CreateCollection createCollection)
|
||||
{
|
||||
List<string> allNames = await _mediaCollectionRepository.GetAll()
|
||||
.Map(list => list.Map(c => c.Name).ToList());
|
||||
List<string> allNames = await dbContext.Collections
|
||||
.Map(c => c.Name)
|
||||
.ToListAsync();
|
||||
|
||||
Validation<BaseError, string> result1 = createCollection.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record DeleteCollection(int CollectionId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeleteCollection(int CollectionId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,42 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class DeleteCollectionHandler : IRequestHandler<DeleteCollection, Either<BaseError, Task>>
|
||||
public class DeleteCollectionHandler : MediatR.IRequestHandler<DeleteCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public DeleteCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
public DeleteCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, Task>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await CollectionMustExist(request))
|
||||
.Map(DoDeletion)
|
||||
.ToEither<Task>();
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
private Task DoDeletion(int mediaCollectionId) => _mediaCollectionRepository.Delete(mediaCollectionId);
|
||||
Validation<BaseError, Collection> validation = await CollectionMustExist(dbContext, request);
|
||||
return await validation.Apply(c => DoDeletion(dbContext, c));
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, int>> CollectionMustExist(
|
||||
DeleteCollection deleteMediaCollection) =>
|
||||
(await _mediaCollectionRepository.Get(deleteMediaCollection.CollectionId))
|
||||
.ToValidation<BaseError>(
|
||||
$"Collection {deleteMediaCollection.CollectionId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private static Task<Unit> DoDeletion(TvContext dbContext, Collection collection)
|
||||
{
|
||||
dbContext.Collections.Remove(collection);
|
||||
return dbContext.SaveChangesAsync().ToUnit();
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
DeleteCollection request) =>
|
||||
dbContext.Collections
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>($"Collection {request.CollectionId} does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,32 +6,41 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
RemoveItemsFromCollectionHandler : MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
|
||||
public class RemoveItemsFromCollectionHandler :
|
||||
MediatR.IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public RemoveItemsFromCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
RemoveItemsFromCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(collection => ApplyRemoveItemsRequest(request, collection))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyRemoveItemsRequest(dbContext, request, c));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyRemoveItemsRequest(
|
||||
TvContext dbContext,
|
||||
RemoveItemsFromCollection request,
|
||||
Collection collection)
|
||||
{
|
||||
@@ -41,7 +50,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
|
||||
itemsToRemove.ForEach(m => collection.MediaItems.Remove(m));
|
||||
|
||||
if (itemsToRemove.Any() && await _mediaCollectionRepository.Update(collection))
|
||||
if (itemsToRemove.Any() && await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
|
||||
@@ -53,13 +62,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, Collection>> Validate(
|
||||
private static Task<Validation<BaseError, Collection>> Validate(
|
||||
TvContext dbContext,
|
||||
RemoveItemsFromCollection request) =>
|
||||
CollectionMustExist(request);
|
||||
CollectionMustExist(dbContext, request);
|
||||
|
||||
private Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
RemoveItemsFromCollection updateCollection) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(updateCollection.MediaCollectionId)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
RemoveItemsFromCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.MediaCollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,47 +6,60 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder,
|
||||
Either<BaseError, Unit>>
|
||||
public class UpdateCollectionCustomOrderHandler :
|
||||
MediatR.IRequestHandler<UpdateCollectionCustomOrder, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public UpdateCollectionCustomOrderHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateCollectionCustomOrder request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(c => ApplyUpdateRequest(c, request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollectionCustomOrder request)
|
||||
private async Task<Unit> ApplyUpdateRequest(
|
||||
TvContext dbContext,
|
||||
Collection c,
|
||||
UpdateCollectionCustomOrder request)
|
||||
{
|
||||
foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders)
|
||||
{
|
||||
Option<CollectionItem> maybeCollectionItem =
|
||||
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
|
||||
Option<CollectionItem> maybeCollectionItem = c.CollectionItems
|
||||
.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
|
||||
|
||||
await maybeCollectionItem.IfSomeAsync(ci => ci.CustomIndex = updateItem.CustomIndex);
|
||||
foreach (CollectionItem collectionItem in maybeCollectionItem)
|
||||
{
|
||||
collectionItem.CustomIndex = updateItem.CustomIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (await _mediaCollectionRepository.Update(c))
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||
request.CollectionId))
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
@@ -55,12 +68,17 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) =>
|
||||
CollectionMustExist(request);
|
||||
|
||||
private Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
private static Task<Validation<BaseError, Collection>> Validate(
|
||||
TvContext dbContext,
|
||||
UpdateCollectionCustomOrder request) =>
|
||||
_mediaCollectionRepository.Get(request.CollectionId)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
CollectionMustExist(dbContext, request);
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateCollectionCustomOrder request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.CollectionItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,47 @@ using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public UpdateCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(c => ApplyUpdateRequest(c, request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Collection> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request)
|
||||
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, Collection c, UpdateCollection request)
|
||||
{
|
||||
c.Name = request.Name;
|
||||
await request.UseCustomPlaybackOrder.IfSomeAsync(
|
||||
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
|
||||
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
|
||||
foreach (bool useCustomPlaybackOrder in request.UseCustomPlaybackOrder)
|
||||
{
|
||||
c.UseCustomPlaybackOrder = useCustomPlaybackOrder;
|
||||
}
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0 && request.UseCustomPlaybackOrder.IsSome)
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||
@@ -47,17 +58,20 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Collection>>
|
||||
Validate(UpdateCollection request) =>
|
||||
(await CollectionMustExist(request), ValidateName(request))
|
||||
private static async Task<Validation<BaseError, Collection>> Validate(
|
||||
TvContext dbContext,
|
||||
UpdateCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), ValidateName(request))
|
||||
.Apply((collectionToUpdate, _) => collectionToUpdate);
|
||||
|
||||
private Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
UpdateCollection updateCollection) =>
|
||||
_mediaCollectionRepository.Get(updateCollection.CollectionId)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
dbContext.Collections
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == updateCollection.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) =>
|
||||
private static Validation<BaseError, string> ValidateName(UpdateCollection updateSimpleMediaCollection) =>
|
||||
updateSimpleMediaCollection.NotEmpty(c => c.Name)
|
||||
.Bind(_ => updateSimpleMediaCollection.NotLongerThan(50)(c => c.Name));
|
||||
}
|
||||
|
||||
@@ -2,23 +2,29 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetAllCollectionsHandler : IRequestHandler<GetAllCollections, List<MediaCollectionViewModel>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
public GetAllCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<List<MediaCollectionViewModel>> Handle(
|
||||
public async Task<List<MediaCollectionViewModel>> Handle(
|
||||
GetAllCollections request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaCollectionRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.Collections
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class
|
||||
GetCollectionByIdHandler : IRequestHandler<GetCollectionById,
|
||||
Option<MediaCollectionViewModel>>
|
||||
public class GetCollectionByIdHandler :
|
||||
IRequestHandler<GetCollectionById, Option<MediaCollectionViewModel>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetCollectionByIdHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
public GetCollectionByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<Option<MediaCollectionViewModel>> Handle(
|
||||
public async Task<Option<MediaCollectionViewModel>> Handle(
|
||||
GetCollectionById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaCollectionRepository.Get(request.Id)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.Collections
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public record GetCollectionItems(int Id) : IRequest<Option<IEnumerable<MediaItemViewModel>>>;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetCollectionItemsHandler : IRequestHandler<GetCollectionItems,
|
||||
Option<IEnumerable<MediaItemViewModel>>>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public GetCollectionItemsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
|
||||
public Task<Option<IEnumerable<MediaItemViewModel>>> Handle(
|
||||
GetCollectionItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaCollectionRepository.GetItems(request.Id)
|
||||
.MapT(mediaItems => mediaItems.Map(ProjectToViewModel));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using Dapper;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, PagedMediaCollectionsViewModel>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IDbConnection _dbConnection;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetPagedCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
public GetPagedCollectionsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_dbConnection = dbConnection;
|
||||
}
|
||||
|
||||
public async Task<PagedMediaCollectionsViewModel> Handle(
|
||||
GetPagedCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _mediaCollectionRepository.CountAllCollections();
|
||||
int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM Collection");
|
||||
|
||||
List<MediaCollectionViewModel> page = await _mediaCollectionRepository
|
||||
.GetPagedCollections(request.PageNum, request.PageSize)
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
List<MediaCollectionViewModel> page = await dbContext.Collections.FromSqlRaw(
|
||||
@"SELECT * FROM Collection
|
||||
ORDER BY Name
|
||||
LIMIT {0} OFFSET {1}",
|
||||
request.PageSize,
|
||||
request.PageNum * request.PageSize)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new PagedMediaCollectionsViewModel(count, page);
|
||||
|
||||
@@ -4,15 +4,15 @@ namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
|
||||
new(mediaItem.Id, mediaItem.LibraryPathId);
|
||||
|
||||
public static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
|
||||
internal static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
|
||||
new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???"));
|
||||
|
||||
public static NamedMediaItemViewModel ProjectToViewModel(Season season) =>
|
||||
internal static NamedMediaItemViewModel ProjectToViewModel(Season season) =>
|
||||
new(season.Id, $"{ShowTitle(season)} ({SeasonDescription(season)})");
|
||||
|
||||
internal static NamedMediaItemViewModel ProjectToViewModel(Artist artist) =>
|
||||
new(artist.Id, artist.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => "???"));
|
||||
|
||||
private static string ShowTitle(Season season) =>
|
||||
season.Show.ShowMetadata.HeadOrNone().Map(sm => sm.Title).IfNone("???");
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record MediaItemSearchResultViewModel(
|
||||
int Id,
|
||||
string Source,
|
||||
string MediaType,
|
||||
string Title,
|
||||
string Duration);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
public record MediaItemViewModel(int Id, int LibraryPathId);
|
||||
}
|
||||
@@ -1,35 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
public GetAllLanguageCodesHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaItemRepository mediaItemRepository)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
}
|
||||
|
||||
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<CultureInfo>();
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
var result = new System.Collections.Generic.HashSet<CultureInfo>();
|
||||
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
List<string> allLanguageCodes = await _mediaItemRepository.GetAllLanguageCodes();
|
||||
foreach (string code in allLanguageCodes)
|
||||
List<string> mediaCodes = await _mediaItemRepository.GetAllLanguageCodes();
|
||||
foreach (string mediaCode in mediaCodes)
|
||||
{
|
||||
Option<CultureInfo> maybeCulture = allCultures.Find(
|
||||
ci => string.Equals(code, ci.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||
await maybeCulture.IfSomeAsync(cultureInfo => result.Add(cultureInfo));
|
||||
foreach (string code in await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCode))
|
||||
{
|
||||
Option<CultureInfo> maybeCulture = allCultures.Find(
|
||||
c => string.Equals(code, c.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||
foreach (CultureInfo culture in maybeCulture)
|
||||
{
|
||||
result.Add(culture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAllMediaItems : IRequest<List<MediaItemViewModel>>;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllMediaItemsHandler : IRequestHandler<GetAllMediaItems, List<MediaItemViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public Task<List<MediaItemViewModel>> Handle(
|
||||
GetAllMediaItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaItemRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetMediaItemById(int Id) : IRequest<Option<MediaItemViewModel>>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetMediaItemByIdHandler : IRequestHandler<GetMediaItemById, Option<MediaItemViewModel>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetMediaItemByIdHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public Task<Option<MediaItemViewModel>> Handle(
|
||||
GetMediaItemById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaItemRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static MediaSourceViewModel ProjectToViewModel(MediaSource mediaSource) =>
|
||||
mediaSource switch
|
||||
{
|
||||
LocalMediaSource lms => new LocalMediaSourceViewModel(lms.Id),
|
||||
PlexMediaSource pms => Plex.Mapper.ProjectToViewModel(pms),
|
||||
_ => throw new NotSupportedException($"Unsupported media source {mediaSource.GetType().Name}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public record CountMediaItemsById(int MediaSourceId) : IRequest<int>;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public class CountMediaItemsByIdHandler : IRequestHandler<CountMediaItemsById, int>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public CountMediaItemsByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<int> Handle(CountMediaItemsById request, CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.CountMediaItems(request.MediaSourceId);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public record GetAllMediaSources : IRequest<List<MediaSourceViewModel>>;
|
||||
}
|
||||
@@ -1,23 +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.MediaSources.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public class GetAllMediaSourcesHandler : IRequestHandler<GetAllMediaSources, List<MediaSourceViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetAllMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public async Task<List<MediaSourceViewModel>> Handle(
|
||||
GetAllMediaSources request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await _mediaSourceRepository.GetAll()).Map(ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public record GetMediaSourceById(int Id) : IRequest<Option<MediaSourceViewModel>>;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaSources.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaSources.Queries
|
||||
{
|
||||
public class GetMediaSourceByIdHandler : IRequestHandler<GetMediaSourceById, Option<MediaSourceViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
|
||||
public GetMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
|
||||
public Task<Option<MediaSourceViewModel>> Handle(
|
||||
GetMediaSourceById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ namespace ErsatzTV.Application.Movies
|
||||
{
|
||||
internal static MovieViewModel ProjectToViewModel(
|
||||
Movie movie,
|
||||
List<string> languageCodes,
|
||||
Option<JellyfinMediaSource> maybeJellyfin,
|
||||
Option<EmbyMediaSource> maybeEmby)
|
||||
{
|
||||
@@ -28,7 +29,7 @@ namespace ErsatzTV.Application.Movies
|
||||
metadata.Studios.Map(s => s.Name).ToList(),
|
||||
(metadata.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList(),
|
||||
LanguagesForMovie(movie),
|
||||
LanguagesForMovie(languageCodes),
|
||||
metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||
.ToList(),
|
||||
@@ -40,13 +41,11 @@ namespace ErsatzTV.Application.Movies
|
||||
};
|
||||
}
|
||||
|
||||
private static List<CultureInfo> LanguagesForMovie(Movie movie)
|
||||
private static List<CultureInfo> LanguagesForMovie(List<string> languageCodes)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return movie.MediaVersions
|
||||
.Map(mv => mv.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).Map(s => s.Language))
|
||||
.Flatten()
|
||||
return languageCodes
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Movies.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Movies.Queries
|
||||
@@ -11,10 +16,15 @@ namespace ErsatzTV.Application.Movies.Queries
|
||||
public class GetMovieByIdHandler : IRequestHandler<GetMovieById, Option<MovieViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
|
||||
public GetMovieByIdHandler(IMovieRepository movieRepository, IMediaSourceRepository mediaSourceRepository)
|
||||
public GetMovieByIdHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMovieRepository movieRepository,
|
||||
IMediaSourceRepository mediaSourceRepository)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_movieRepository = movieRepository;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
}
|
||||
@@ -23,6 +33,8 @@ namespace ErsatzTV.Application.Movies.Queries
|
||||
GetMovieById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
@@ -30,7 +42,20 @@ namespace ErsatzTV.Application.Movies.Queries
|
||||
.Map(list => list.HeadOrNone());
|
||||
|
||||
Option<Movie> movie = await _movieRepository.GetMovie(request.Id);
|
||||
return movie.Map(m => ProjectToViewModel(m, maybeJellyfin, maybeEmby));
|
||||
|
||||
Option<MediaVersion> maybeVersion = movie.Map(m => m.MediaVersions.HeadOrNone()).Flatten();
|
||||
var languageCodes = new List<string>();
|
||||
foreach (MediaVersion version in maybeVersion)
|
||||
{
|
||||
var mediaCodes = version.Streams
|
||||
.Filter(ms => ms.MediaStreamKind == MediaStreamKind.Audio)
|
||||
.Map(ms => ms.Language)
|
||||
.ToList();
|
||||
|
||||
languageCodes.AddRange(await dbContext.LanguageCodes.GetAllLanguageCodes(mediaCodes));
|
||||
}
|
||||
|
||||
return movie.Map(m => ProjectToViewModel(m, languageCodes, maybeJellyfin, maybeEmby));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,41 +2,57 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public class BuildPlayoutHandler : MediatR.IRequestHandler<BuildPlayout, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IPlayoutBuilder _playoutBuilder;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
|
||||
public BuildPlayoutHandler(IPlayoutRepository playoutRepository, IPlayoutBuilder playoutBuilder)
|
||||
public BuildPlayoutHandler(IDbContextFactory<TvContext> dbContextFactory, IPlayoutBuilder playoutBuilder)
|
||||
{
|
||||
_playoutRepository = playoutRepository;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_playoutBuilder = playoutBuilder;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.Map(v => v.ToEither<Playout>())
|
||||
.BindT(playout => ApplyUpdateRequest(request, playout));
|
||||
|
||||
private async Task<Either<BaseError, Unit>> ApplyUpdateRequest(BuildPlayout request, Playout playout)
|
||||
public async Task<Either<BaseError, Unit>> Handle(BuildPlayout request, CancellationToken cancellationToken)
|
||||
{
|
||||
Playout result = await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild);
|
||||
await _playoutRepository.Update(result);
|
||||
return unit;
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout));
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, Playout>> Validate(BuildPlayout request) =>
|
||||
PlayoutMustExist(request);
|
||||
private async Task<Unit> ApplyUpdateRequest(TvContext dbContext, BuildPlayout request, Playout playout)
|
||||
{
|
||||
await _playoutBuilder.BuildPlayoutItems(playout, request.Rebuild);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> PlayoutMustExist(BuildPlayout buildPlayout) =>
|
||||
(await _playoutRepository.GetFull(buildPlayout.PlayoutId))
|
||||
.ToValidation<BaseError>("Playout does not exist.");
|
||||
private static Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, BuildPlayout request) =>
|
||||
PlayoutMustExist(dbContext, request);
|
||||
|
||||
private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
|
||||
TvContext dbContext,
|
||||
BuildPlayout buildPlayout) =>
|
||||
dbContext.Playouts
|
||||
.Include(p => p.Channel)
|
||||
.Include(p => p.Items)
|
||||
.Include(p => p.ProgramScheduleAnchors)
|
||||
.ThenInclude(a => a.MediaItem)
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.Collection)
|
||||
.Include(p => p.ProgramSchedule)
|
||||
.ThenInclude(ps => ps.Items)
|
||||
.ThenInclude(psi => psi.MediaItem)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == buildPlayout.PlayoutId)
|
||||
.Map(o => o.ToValidation<BaseError>("Playout does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
public record CreatePlayout(
|
||||
int ChannelId,
|
||||
int ProgramScheduleId,
|
||||
ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, PlayoutViewModel>>;
|
||||
ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, CreatePlayoutResponse>>;
|
||||
}
|
||||
|
||||
@@ -4,51 +4,50 @@ using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Playouts.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
using Channel = ErsatzTV.Core.Domain.Channel;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public class
|
||||
CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, PlayoutViewModel>>
|
||||
public class CreatePlayoutHandler : IRequestHandler<CreatePlayout, Either<BaseError, CreatePlayoutResponse>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreatePlayoutHandler(
|
||||
IPlayoutRepository playoutRepository,
|
||||
IChannelRepository channelRepository,
|
||||
IProgramScheduleRepository programScheduleRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_playoutRepository = playoutRepository;
|
||||
_channelRepository = channelRepository;
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
_channel = channel;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, PlayoutViewModel>> Handle(
|
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
|
||||
CreatePlayout request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PersistPlayout)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<PlayoutViewModel> PersistPlayout(Playout c)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PlayoutViewModel result = await _playoutRepository.Add(c).Map(ProjectToViewModel);
|
||||
await _channel.WriteAsync(new BuildPlayout(result.Id));
|
||||
return result;
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(playout => PersistPlayout(dbContext, playout));
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(CreatePlayout request) =>
|
||||
(await ValidateChannel(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
|
||||
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)
|
||||
{
|
||||
await dbContext.Playouts.AddAsync(playout);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await _channel.WriteAsync(new BuildPlayout(playout.Id));
|
||||
return new CreatePlayoutResponse(playout.Id);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(TvContext dbContext, CreatePlayout request) =>
|
||||
(await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request),
|
||||
ValidatePlayoutType(request))
|
||||
.Apply(
|
||||
(channel, programSchedule, playoutType) => new Playout
|
||||
{
|
||||
@@ -57,31 +56,38 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
ProgramSchedulePlayoutType = playoutType
|
||||
});
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ValidateChannel(CreatePlayout createPlayout) =>
|
||||
ChannelMustExist(createPlayout).BindT(ChannelMustNotHavePlayouts);
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel(
|
||||
TvContext dbContext,
|
||||
CreatePlayout createPlayout) =>
|
||||
dbContext.Channels
|
||||
.Include(c => c.Playouts)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == createPlayout.ChannelId)
|
||||
.Map(o => o.ToValidation<BaseError>("Channel does not exist"))
|
||||
.BindT(ChannelMustNotHavePlayouts);
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustExist(CreatePlayout createPlayout) =>
|
||||
(await _channelRepository.Get(createPlayout.ChannelId))
|
||||
.ToValidation<BaseError>("Channel does not exist.");
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustNotHavePlayouts(Channel channel) =>
|
||||
Optional(await _channelRepository.CountPlayouts(channel.Id))
|
||||
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) =>
|
||||
Optional(channel.Playouts.Count)
|
||||
.Filter(count => count == 0)
|
||||
.Map(_ => channel)
|
||||
.ToValidation<BaseError>("Channel already has one playout.");
|
||||
.ToValidation<BaseError>("Channel already has one playout");
|
||||
|
||||
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
private static Task<Validation<BaseError, ProgramSchedule>> ValidateProgramSchedule(
|
||||
TvContext dbContext,
|
||||
CreatePlayout createPlayout) =>
|
||||
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId))
|
||||
.ToValidation<BaseError>("ProgramSchedule does not exist.")
|
||||
.Bind(ProgramScheduleMustHaveItems);
|
||||
dbContext.ProgramSchedules
|
||||
.Include(ps => ps.Items)
|
||||
.SelectOneAsync(ps => ps.Id, ps => ps.Id == createPlayout.ProgramScheduleId)
|
||||
.Map(o => o.ToValidation<BaseError>("Program schedule does not exist"))
|
||||
.BindT(ProgramScheduleMustHaveItems);
|
||||
|
||||
private Validation<BaseError, ProgramSchedule> ProgramScheduleMustHaveItems(ProgramSchedule programSchedule) =>
|
||||
private static Validation<BaseError, ProgramSchedule> ProgramScheduleMustHaveItems(
|
||||
ProgramSchedule programSchedule) =>
|
||||
Optional(programSchedule)
|
||||
.Filter(ps => ps.Items.Any())
|
||||
.ToValidation<BaseError>("Program schedule must have items");
|
||||
|
||||
private Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(CreatePlayout createPlayout) =>
|
||||
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(
|
||||
CreatePlayout createPlayout) =>
|
||||
Optional(createPlayout.ProgramSchedulePlayoutType)
|
||||
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None)
|
||||
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None");
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public record CreatePlayoutResponse(int PlayoutId);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public record DeletePlayout(int PlayoutId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeletePlayout(int PlayoutId) : IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Task>>
|
||||
public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public DeletePlayoutHandler(IPlayoutRepository playoutRepository) =>
|
||||
_playoutRepository = playoutRepository;
|
||||
public DeletePlayoutHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, Task>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeletePlayout request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await PlayoutMustExist(request))
|
||||
.Map(DoDeletion)
|
||||
.ToEither<Task>();
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
private Task DoDeletion(int playoutId) => _playoutRepository.Delete(playoutId);
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||
.OrderBy(p => p.Id)
|
||||
.FirstOrDefaultAsync(p => p.Id == request.PlayoutId, cancellationToken);
|
||||
|
||||
private async Task<Validation<BaseError, int>> PlayoutMustExist(
|
||||
DeletePlayout deletePlayout) =>
|
||||
(await _playoutRepository.Get(deletePlayout.PlayoutId))
|
||||
.ToValidation<BaseError>($"Playout {deletePlayout.PlayoutId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
foreach (Playout playout in maybePlayout)
|
||||
{
|
||||
dbContext.Playouts.Remove(playout);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return maybePlayout
|
||||
.Map(_ => Unit.Default)
|
||||
.ToEither(BaseError.New($"Playout {request.PlayoutId} does not exist."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public record UpdatePlayout(
|
||||
int PlayoutId,
|
||||
int ChannelId,
|
||||
int ProgramScheduleId,
|
||||
ProgramSchedulePlayoutType ProgramSchedulePlayoutType) : IRequest<Either<BaseError, PlayoutViewModel>>;
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
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.Playouts.Mapper;
|
||||
using Channel = ErsatzTV.Core.Domain.Channel;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Commands
|
||||
{
|
||||
public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseError, PlayoutViewModel>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
|
||||
public UpdatePlayoutHandler(
|
||||
IPlayoutRepository playoutRepository,
|
||||
IChannelRepository channelRepository,
|
||||
IProgramScheduleRepository programScheduleRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_playoutRepository = playoutRepository;
|
||||
_channelRepository = channelRepository;
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, PlayoutViewModel>> Handle(
|
||||
UpdatePlayout request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(c => ApplyUpdateRequest(c, request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<PlayoutViewModel> ApplyUpdateRequest(Playout p, UpdatePlayout update)
|
||||
{
|
||||
p.ChannelId = update.ChannelId;
|
||||
p.ProgramScheduleId = update.ProgramScheduleId;
|
||||
p.ProgramSchedulePlayoutType = update.ProgramSchedulePlayoutType;
|
||||
await _playoutRepository.Update(p);
|
||||
await _channel.WriteAsync(new BuildPlayout(p.Id));
|
||||
return ProjectToViewModel(p);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(UpdatePlayout request) =>
|
||||
(await PlayoutMustExist(request), await ChannelMustExist(request), await ProgramScheduleMustExist(request),
|
||||
ValidatePlayoutType(request))
|
||||
.Apply(
|
||||
(playoutToUpdate, _, _, _) => playoutToUpdate);
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> PlayoutMustExist(UpdatePlayout updatePlayout) =>
|
||||
(await _playoutRepository.Get(updatePlayout.PlayoutId))
|
||||
.ToValidation<BaseError>("Playout does not exist.");
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustExist(UpdatePlayout createPlayout) =>
|
||||
(await _channelRepository.Get(createPlayout.ChannelId))
|
||||
.ToValidation<BaseError>("Channel does not exist.");
|
||||
|
||||
private async Task<Validation<BaseError, ProgramSchedule>>
|
||||
ProgramScheduleMustExist(UpdatePlayout createPlayout) =>
|
||||
(await _programScheduleRepository.Get(createPlayout.ProgramScheduleId))
|
||||
.ToValidation<BaseError>("ProgramSchedule does not exist.");
|
||||
|
||||
private Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(UpdatePlayout createPlayout) =>
|
||||
Optional(createPlayout.ProgramSchedulePlayoutType)
|
||||
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None)
|
||||
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None");
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,17 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static PlayoutViewModel ProjectToViewModel(Playout playout) =>
|
||||
new(
|
||||
playout.Id,
|
||||
Project(playout.Channel),
|
||||
Project(playout.ProgramSchedule),
|
||||
playout.ProgramSchedulePlayoutType);
|
||||
|
||||
internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) =>
|
||||
new(
|
||||
GetDisplayTitle(playoutItem.MediaItem),
|
||||
playoutItem.StartOffset,
|
||||
GetDisplayDuration(playoutItem.MediaItem));
|
||||
|
||||
private static PlayoutChannelViewModel Project(Channel channel) =>
|
||||
new(channel.Id, channel.Number, channel.Name);
|
||||
|
||||
private static PlayoutProgramScheduleViewModel Project(ProgramSchedule programSchedule) =>
|
||||
new(programSchedule.Id, programSchedule.Name);
|
||||
|
||||
private static string GetDisplayTitle(MediaItem mediaItem)
|
||||
{
|
||||
switch (mediaItem)
|
||||
@@ -31,9 +19,16 @@ namespace ErsatzTV.Application.Playouts
|
||||
case Episode e:
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
return e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00} - {em.Title}")
|
||||
.IfNone("[unknown episode]");
|
||||
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList();
|
||||
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList();
|
||||
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0)
|
||||
{
|
||||
return "[unknown episode]";
|
||||
}
|
||||
|
||||
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
|
||||
var titlesString = $"{string.Join('/', episodeTitles)}";
|
||||
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||
case MusicVideo mv:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts
|
||||
{
|
||||
public record PagedPlayoutItemsViewModel(int TotalCount, List<PlayoutItemViewModel> Page);
|
||||
}
|
||||
8
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
Normal file
8
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Application.Playouts
|
||||
{
|
||||
public record PlayoutNameViewModel(
|
||||
int PlayoutId,
|
||||
string ChannelName,
|
||||
string ChannelNumber,
|
||||
string ScheduleName);
|
||||
}
|
||||
@@ -3,5 +3,5 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public record GetAllPlayouts : IRequest<List<PlayoutViewModel>>;
|
||||
public record GetAllPlayouts : IRequest<List<PlayoutNameViewModel>>;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Playouts.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<PlayoutViewModel>>
|
||||
public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<PlayoutNameViewModel>>
|
||||
{
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllPlayoutsHandler(IPlayoutRepository playoutRepository) =>
|
||||
_playoutRepository = playoutRepository;
|
||||
public GetAllPlayoutsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<List<PlayoutViewModel>> Handle(
|
||||
public async Task<List<PlayoutNameViewModel>> Handle(
|
||||
GetAllPlayouts request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_playoutRepository.GetAll().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
return await dbContext.Playouts
|
||||
.Filter(p => p.Channel != null && p.ProgramSchedule != null)
|
||||
.Map(p => new PlayoutNameViewModel(p.Id, p.Channel.Name, p.Channel.Number, p.ProgramSchedule.Name))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public record GetPlayoutById(int Id) : IRequest<Option<PlayoutViewModel>>;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Playouts.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public class
|
||||
GetPlayoutByIdHandler : IRequestHandler<GetPlayoutById, Option<PlayoutViewModel>>
|
||||
{
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
|
||||
public GetPlayoutByIdHandler(IPlayoutRepository playoutRepository) =>
|
||||
_playoutRepository = playoutRepository;
|
||||
|
||||
public Task<Option<PlayoutViewModel>> Handle(
|
||||
GetPlayoutById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_playoutRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public record GetPlayoutItemsById(int PlayoutId) : IRequest<List<PlayoutItemViewModel>>;
|
||||
public record GetPlayoutItemsById(int PlayoutId, int PageNum, int PageSize) : IRequest<PagedPlayoutItemsViewModel>;
|
||||
}
|
||||
|
||||
@@ -2,24 +2,61 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Playouts.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts.Queries
|
||||
{
|
||||
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, List<PlayoutItemViewModel>>
|
||||
public class GetPlayoutItemsByIdHandler : IRequestHandler<GetPlayoutItemsById, PagedPlayoutItemsViewModel>
|
||||
{
|
||||
private readonly IPlayoutRepository _playoutRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetPlayoutItemsByIdHandler(IPlayoutRepository playoutRepository) =>
|
||||
_playoutRepository = playoutRepository;
|
||||
public GetPlayoutItemsByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<List<PlayoutItemViewModel>> Handle(
|
||||
public async Task<PagedPlayoutItemsViewModel> Handle(
|
||||
GetPlayoutItemsById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_playoutRepository.GetPlayoutItems(request.PlayoutId)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
int totalCount = await dbContext.PlayoutItems
|
||||
.CountAsync(i => i.PlayoutId == request.PlayoutId, cancellationToken);
|
||||
|
||||
List<PlayoutItemViewModel> page = await dbContext.PlayoutItems
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Movie).MovieMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Movie).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as MusicVideo).Artist)
|
||||
.ThenInclude(mm => mm.ArtistMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).MediaVersions)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).Season)
|
||||
.ThenInclude(s => s.SeasonMetadata)
|
||||
.Include(i => i.MediaItem)
|
||||
.ThenInclude(mi => (mi as Episode).Season.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.Filter(i => i.PlayoutId == request.PlayoutId)
|
||||
.OrderBy(i => i.Start)
|
||||
.Skip(request.PageNum * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.ToListAsync(cancellationToken)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new PagedPlayoutItemsViewModel(totalCount, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,39 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.ProgramSchedules.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase, IRequestHandler<AddProgramScheduleItem,
|
||||
Either<BaseError, ProgramScheduleItemViewModel>>
|
||||
public class AddProgramScheduleItemHandler : ProgramScheduleItemCommandBase,
|
||||
IRequestHandler<AddProgramScheduleItem, Either<BaseError, ProgramScheduleItemViewModel>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public AddProgramScheduleItemHandler(
|
||||
IProgramScheduleRepository programScheduleRepository,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
: base(programScheduleRepository)
|
||||
{
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, ProgramScheduleItemViewModel>> Handle(
|
||||
public async Task<Either<BaseError, ProgramScheduleItemViewModel>> Handle(
|
||||
AddProgramScheduleItem request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(programSchedule => PersistItem(request, programSchedule))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => PersistItem(dbContext, request, ps));
|
||||
}
|
||||
|
||||
private async Task<ProgramScheduleItemViewModel> PersistItem(
|
||||
TvContext dbContext,
|
||||
AddProgramScheduleItem request,
|
||||
ProgramSchedule programSchedule)
|
||||
{
|
||||
@@ -43,7 +46,7 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
ProgramScheduleItem item = BuildItem(programSchedule, nextIndex, request);
|
||||
programSchedule.Items.Add(item);
|
||||
|
||||
await _programScheduleRepository.Update(programSchedule);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// rebuild any playouts that use this schedule
|
||||
foreach (Playout playout in programSchedule.Playouts)
|
||||
@@ -54,8 +57,10 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
return ProjectToViewModel(item);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ProgramSchedule>> Validate(AddProgramScheduleItem request) =>
|
||||
ProgramScheduleMustExist(request.ProgramScheduleId)
|
||||
private static Task<Validation<BaseError, ProgramSchedule>> Validate(
|
||||
TvContext dbContext,
|
||||
AddProgramScheduleItem request) =>
|
||||
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
|
||||
.BindT(programSchedule => PlayoutModeMustBeValid(request, programSchedule));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
string Name,
|
||||
PlaybackOrder MediaCollectionPlaybackOrder,
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
|
||||
}
|
||||
|
||||
@@ -1,63 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.ProgramSchedules.Mapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static LanguageExt.Prelude;
|
||||
using ValidationT_AsyncSync_Extensions = LanguageExt.ValidationT_AsyncSync_Extensions;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public class
|
||||
CreateProgramScheduleHandler : IRequestHandler<CreateProgramSchedule,
|
||||
Either<BaseError, ProgramScheduleViewModel>>
|
||||
public class CreateProgramScheduleHandler :
|
||||
IRequestHandler<CreateProgramSchedule, Either<BaseError, CreateProgramScheduleResult>>
|
||||
{
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreateProgramScheduleHandler(IProgramScheduleRepository programScheduleRepository) =>
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
public CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public Task<Either<BaseError, ProgramScheduleViewModel>> Handle(
|
||||
public async Task<Either<BaseError, CreateProgramScheduleResult>> Handle(
|
||||
CreateProgramSchedule request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(PersistProgramSchedule)
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<ProgramScheduleViewModel> PersistProgramSchedule(ProgramSchedule c) =>
|
||||
_programScheduleRepository.Add(c).Map(ProjectToViewModel);
|
||||
|
||||
private Task<Validation<BaseError, ProgramSchedule>> Validate(CreateProgramSchedule request) =>
|
||||
ValidateName(request)
|
||||
.MapT(
|
||||
name =>
|
||||
{
|
||||
bool keepMultiPartEpisodesTogether =
|
||||
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
|
||||
request.KeepMultiPartEpisodesTogether;
|
||||
return new ProgramSchedule
|
||||
{
|
||||
Name = name,
|
||||
MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
|
||||
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
|
||||
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows
|
||||
};
|
||||
});
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateName(CreateProgramSchedule createProgramSchedule)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<string> allNames = await _programScheduleRepository.GetAll()
|
||||
.Map(list => list.Map(c => c.Name).ToList());
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => PersistProgramSchedule(dbContext, ps));
|
||||
}
|
||||
|
||||
private static async Task<CreateProgramScheduleResult> PersistProgramSchedule(
|
||||
TvContext dbContext,
|
||||
ProgramSchedule programSchedule)
|
||||
{
|
||||
await dbContext.ProgramSchedules.AddAsync(programSchedule);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return new CreateProgramScheduleResult(programSchedule.Id);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, ProgramSchedule>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateProgramSchedule request) =>
|
||||
ValidationT_AsyncSync_Extensions.MapT(
|
||||
ValidateName(dbContext, request),
|
||||
name =>
|
||||
{
|
||||
bool keepMultiPartEpisodesTogether =
|
||||
request.MediaCollectionPlaybackOrder == PlaybackOrder.Shuffle &&
|
||||
request.KeepMultiPartEpisodesTogether;
|
||||
return new ProgramSchedule
|
||||
{
|
||||
Name = name,
|
||||
MediaCollectionPlaybackOrder = request.MediaCollectionPlaybackOrder,
|
||||
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
|
||||
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows
|
||||
};
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||
TvContext dbContext,
|
||||
CreateProgramSchedule createProgramSchedule)
|
||||
{
|
||||
Validation<BaseError, string> result1 = createProgramSchedule.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createProgramSchedule.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
var result2 = Optional(createProgramSchedule.Name)
|
||||
.Filter(name => !allNames.Contains(name))
|
||||
int duplicateNameCount = await dbContext.ProgramSchedules
|
||||
.CountAsync(ps => ps.Name == createProgramSchedule.Name);
|
||||
|
||||
var result2 = Optional(duplicateNameCount)
|
||||
.Filter(count => count == 0)
|
||||
.ToValidation<BaseError>("Schedule name must be unique");
|
||||
|
||||
return (result1, result2).Apply((_, _) => createProgramSchedule.Name);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public record CreateProgramScheduleResult(int ProgramScheduleId) : EntityIdResult(ProgramScheduleId);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeleteProgramSchedule(int ProgramScheduleId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedule, Either<BaseError, Task>>
|
||||
public class DeleteProgramScheduleHandler : IRequestHandler<DeleteProgramSchedule, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public DeleteProgramScheduleHandler(IProgramScheduleRepository programScheduleRepository) =>
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
public DeleteProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, Task>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteProgramSchedule request,
|
||||
CancellationToken cancellationToken) =>
|
||||
(await ProgramScheduleMustExist(request))
|
||||
.Map(DoDeletion)
|
||||
.ToEither<Task>();
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, ProgramSchedule> validation = await ProgramScheduleMustExist(dbContext, request);
|
||||
return await validation.Apply(ps => DoDeletion(dbContext, ps));
|
||||
}
|
||||
|
||||
private Task DoDeletion(int programScheduleId) => _programScheduleRepository.Delete(programScheduleId);
|
||||
private static Task<Unit> DoDeletion(TvContext dbContext, ProgramSchedule programSchedule)
|
||||
{
|
||||
dbContext.ProgramSchedules.Remove(programSchedule);
|
||||
return dbContext.SaveChangesAsync().ToUnit();
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, int>> ProgramScheduleMustExist(
|
||||
DeleteProgramSchedule deleteProgramSchedule) =>
|
||||
(await _programScheduleRepository.Get(deleteProgramSchedule.ProgramScheduleId))
|
||||
.ToValidation<BaseError>($"ProgramSchedule {deleteProgramSchedule.ProgramScheduleId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
private Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
TvContext dbContext,
|
||||
DeleteProgramSchedule request) =>
|
||||
dbContext.ProgramSchedules
|
||||
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.ProgramScheduleId)
|
||||
.Map(o => o.ToValidation<BaseError>($"ProgramSchedule {request.ProgramScheduleId} does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using LanguageExt;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public abstract class ProgramScheduleItemCommandBase
|
||||
{
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
protected static Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
TvContext dbContext,
|
||||
int programScheduleId) =>
|
||||
dbContext.ProgramSchedules
|
||||
.Include(ps => ps.Items)
|
||||
.Include(ps => ps.Playouts)
|
||||
.SelectOneAsync(ps => ps.Id, ps => ps.Id == programScheduleId)
|
||||
.Map(o => o.ToValidation<BaseError>("[ProgramScheduleId] does not exist."));
|
||||
|
||||
protected ProgramScheduleItemCommandBase(IProgramScheduleRepository programScheduleRepository) =>
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
|
||||
protected async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(int programScheduleId) =>
|
||||
(await _programScheduleRepository.GetWithPlayouts(programScheduleId))
|
||||
.ToValidation<BaseError>("[ProgramScheduleId] does not exist.");
|
||||
|
||||
protected Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
|
||||
protected static Validation<BaseError, ProgramSchedule> PlayoutModeMustBeValid(
|
||||
IProgramScheduleItemRequest item,
|
||||
ProgramSchedule programSchedule)
|
||||
{
|
||||
@@ -79,6 +81,13 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
|
||||
}
|
||||
|
||||
break;
|
||||
case ProgramScheduleItemCollectionType.Artist:
|
||||
if (item.MediaItemId is null)
|
||||
{
|
||||
return BaseError.New("[MediaItem] is required for collection type 'Artist'");
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return BaseError.New("[CollectionType] is invalid");
|
||||
|
||||
@@ -6,43 +6,46 @@ using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.ProgramSchedules.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
{
|
||||
public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase,
|
||||
IRequestHandler<ReplaceProgramScheduleItems,
|
||||
Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>
|
||||
IRequestHandler<ReplaceProgramScheduleItems, Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IProgramScheduleRepository _programScheduleRepository;
|
||||
|
||||
public ReplaceProgramScheduleItemsHandler(
|
||||
IProgramScheduleRepository programScheduleRepository,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
: base(programScheduleRepository)
|
||||
{
|
||||
_programScheduleRepository = programScheduleRepository;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle(
|
||||
public async Task<Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>>> Handle(
|
||||
ReplaceProgramScheduleItems request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(programSchedule => PersistItems(request, programSchedule))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => PersistItems(dbContext, request, ps));
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<ProgramScheduleItemViewModel>> PersistItems(
|
||||
TvContext dbContext,
|
||||
ReplaceProgramScheduleItems request,
|
||||
ProgramSchedule programSchedule)
|
||||
{
|
||||
dbContext.RemoveRange(programSchedule.Items);
|
||||
programSchedule.Items = request.Items.Map(i => BuildItem(programSchedule, i.Index, i)).ToList();
|
||||
|
||||
await _programScheduleRepository.Update(programSchedule);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// rebuild any playouts that use this schedule
|
||||
foreach (Playout playout in programSchedule.Playouts)
|
||||
@@ -53,12 +56,14 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
return programSchedule.Items.Map(ProjectToViewModel);
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ProgramSchedule>> Validate(ReplaceProgramScheduleItems request) =>
|
||||
ProgramScheduleMustExist(request.ProgramScheduleId)
|
||||
private Task<Validation<BaseError, ProgramSchedule>> Validate(
|
||||
TvContext dbContext,
|
||||
ReplaceProgramScheduleItems request) =>
|
||||
ProgramScheduleMustExist(dbContext, request.ProgramScheduleId)
|
||||
.BindT(programSchedule => PlayoutModesMustBeValid(request, programSchedule))
|
||||
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
|
||||
|
||||
private Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
|
||||
private static Validation<BaseError, ProgramSchedule> PlayoutModesMustBeValid(
|
||||
ReplaceProgramScheduleItems request,
|
||||
ProgramSchedule programSchedule) =>
|
||||
request.Items.Map(item => PlayoutModeMustBeValid(item, programSchedule)).Sequence()
|
||||
|
||||
@@ -11,5 +11,5 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
string Name,
|
||||
PlaybackOrder MediaCollectionPlaybackOrder,
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, ProgramScheduleViewModel>>;
|
||||
bool TreatCollectionsAsShows) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user