Compare commits

...
Author SHA1 Message Date
Jason Dove f69c58c6bf update changelog for release v0.4.3-alpha [no ci] 2022-03-05 19:49:31 -06:00
Jason DoveandGitHub 44e90b0ecc more bug reporting (#679) 2022-03-05 18:21:53 -06:00
Jason DoveandGitHub dcc8f19a6b remove transient IDbConnection (#678) 2022-03-05 14:06:27 -06:00
Jason DoveandGitHub fc1a051df5 fix path replacement bug (#677) 2022-03-05 12:21:26 -06:00
Jason DoveandGitHub a2e7e6df1e fix thread sync bug in hls segmenter (#676) 2022-03-05 11:26:29 -06:00
Jason DoveandGitHub 6c06fbe621 fix mid-roll filler scheduling bug (#675) 2022-03-04 21:40:08 -06:00
Jason DoveandGitHub a3260b2316 fix album_artist metadata (#674) 2022-03-04 21:10:55 -06:00
Jason DoveandGitHub ea339a1622 add song album_artist metadata (#673) 2022-03-04 18:44:54 -06:00
Jason DoveandGitHub 58697496fa metadata stack trace improvements (#672)
* improve stack traces from local metadata provider

* more metadata line number fixes
2022-03-04 15:44:51 -06:00
Jason DoveandGitHub ec1b2502f1 rework bugsnag integration (#671) 2022-03-03 21:39:46 -06:00
Jason DoveandGitHub 1ab98578ab refactor namespaces and imports (#670)
* re-namespace

* optimize usings

* more usings

* more of the same

* more implicit/global usings

* cleanup all usings

* minor fixes
2022-03-03 15:36:07 -06:00
Jason DoveandGitHub 748581bf5a update dependencies (#669) 2022-03-03 08:46:08 -06:00
Jason DoveandGitHub 2058c44949 minor ui fixes (#668)
* remove unused package

* fix controller name

* catch and ignore jsruntime exceptions

* separate debug stage from public stage
2022-03-02 20:45:07 -06:00
Jason DoveandGitHub 24ef5e68eb add error reporting health check (#667) 2022-03-02 14:51:15 -06:00
Jason Dove b1c905233f add thank you page to docs [no build] 2022-03-02 09:19:54 -06:00
Jason DoveandGitHub 452f361384 use cancellation tokens in ui (#666)
* use cancellation tokens in pages

* use cancellation token in shared ui

* use cancellation tokens in artwork controller
2022-03-01 21:31:48 -06:00
Jason DoveandGitHub 9e2f445785 add bugsnag error reporting (#665) 2022-03-01 18:41:24 -06:00
Jason DoveandGitHub ea72e7b689 use new transcoder logic by default (#664) 2022-03-01 14:29:47 -06:00
Jason DoveandGitHub 19a7f90d52 ffmpeg lib hardware accel fixes (#663)
* more scaling fixes

* qsv fixes

* cleanup

* nvidia fixes

* vaapi fixes

* test nvidia by default
2022-03-01 14:03:36 -06:00
Jason DoveandGitHub 572a3be33e limit framerate normalization to 24fps and above (#662) 2022-02-28 19:22:11 -06:00
Jason DoveandGitHub f8412c4f5c more nvidia fixes (#661)
* dont use mpeg2_cuvid with interlaced content

* fix nvidia scaling when padding is not needed
2022-02-28 18:35:49 -06:00
Jason DoveandGitHub 6b0ced6be9 fix nvidia watermark (#660)
* add watermark transcoding tests

* nvidia fixes

* update changelog
2022-02-28 14:21:42 -06:00
Jason DoveandGitHub 19161b12ea fix chronological and shuffle-in-order song sorting (#659) 2022-02-28 13:41:22 -06:00
Jason Dove fd3ef90880 update changelog for release v0.4.2-alpha [no ci] 2022-02-26 19:23:18 -06:00
Jason DoveandGitHub 696b29c9e9 fix videotoolbox acceleration with new transcoder (#658)
* fix videotoolbox acceleration with new transcoder

* cleanup
2022-02-26 18:45:05 -06:00
Jason DoveandGitHub 70c37df596 fix vaapi watermarks with new transcoder (#657) 2022-02-26 14:00:23 -06:00
Jason DoveandGitHub 040785b0d7 fix qsv scaling and watermarks with new transcoder (#656)
* fix qsv scaling

* fix qsv watermarks
2022-02-26 11:40:16 -06:00
Jason DoveandGitHub b25f783343 hide unused local libraries (#655) 2022-02-25 21:16:38 -06:00
Jason DoveandGitHub a21f62ff8c add watermark support to experimental transcoder logic (#654)
* wip

* fix songs again

* don't use lists of video and audio input files

* test concat command output

* move concat pipeline

* start to use ffmpeg state

* add ffmpeg state and audio state

* audio state is required

* attach input options directly to input files

* move filters to input files

* add watermark support
2022-02-25 21:06:17 -06:00
Jason DoveandGitHub 78fdc9c57a add option to shuffle schedule items (#652)
* add schedule setting

* it works

* fix tests

* update readme

* rebuild all playouts
2022-02-22 21:20:40 -06:00
Jason DoveandGitHub f6c42f3ff5 add configurable channel group and categories (#651) 2022-02-21 21:04:12 -06:00
Jason DoveandGitHub c92b6cb909 fix song playback (#644) 2022-02-17 21:52:28 -06:00
Jason DoveandGitHub a2e1dc8bfb don't deinterlace using nvidia decoders (#643) 2022-02-17 13:28:03 -06:00
Jason DoveandGitHub 8a6093ce8d properly specify audio codec even when source has correct format (#641) 2022-02-16 11:29:54 -06:00
Jason DoveandGitHub 1d6279cee8 log problematic playlists (#640) 2022-02-16 08:18:22 -06:00
Jason DoveandGitHub 66ab0b3990 use single thread and disable framerate normalization (#639)
* try one thread for everything

* add (unused) framerate filter

* disable framerate normalization by default

* update dependencies
2022-02-16 06:01:28 -06:00
Jason DoveandGitHub a7922beaed qsv and logging fixes (#637)
* improve pipeline logging

* fix qsv acceleration

* fix qsv parameter order
2022-02-15 12:41:06 -06:00
Jason DoveandGitHub a1d9d6790e fix copy codec when transcoding is disabled (#636) 2022-02-15 09:22:40 -06:00
Jason DoveandGitHub 2f2d7952dd use vaapi driver settings with new transcoder logic (#635)
* add LIBVA_DRIVER_NAME env var

* add vaapi device name

* add FFREPORT env var

* fixes
2022-02-15 08:44:49 -06:00
Jason DoveandGitHub c96b800b52 ffmpeg lib fixes (#633)
* try to fix vaapi inconsistencies

* log unexpected data

* vaapi fixes

* disable 444 test

* add qsv deinterlace filter; qsv fixes

* add videotoolbox acceleration
2022-02-14 22:01:26 -06:00
Jason Dove c05882f4a6 fix docker builds 2022-02-14 14:36:35 -06:00
Jason DoveandGitHub 5a442a06a0 start to use new ffmpeg library (#632)
* start to add ffmpeg library

* start to hook ffmpeg lib into main app

* improvements

* more progress

* make pipeline builder configurable

* more options

* move more logic down into ffmpeg lib

* ffmpeg lib desired state refactoring

* add software scaling and padding

* add loudness normalization and software deinterlace

* add metadata output options

* add setsar filter

* use built-in scaling logic

* fixes

* initial nvidia support

* nvidia improvements

* support hls mode

* print old arguments at debug level

* fix package reference

* start to add qsv support

* formatting

* fix tests

* add timeout to transcode tests

* show successful ffmpeg arguments

* add vaapi support

* add more software decoders

* add experimental transcoder option

* call existing ffmpeg process service for unimplemented features

* fix nvidia mpeg2video bug

* update changelog

* ignore some neglected unit tests
2022-02-14 14:34:00 -06:00
Jason Dove 640fed0a43 fix hls segmenter bug with unknown packet duration 2022-02-11 18:37:02 -06:00
1326 changed files with 91898 additions and 53249 deletions
+44 -1
View File
@@ -5,6 +5,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.4.3-alpha] - 2022-03-05
### Fixed
- Fix song sorting with `Chronological` and `Shuffle In Order` playback orders
- Fix watermark on scaled and/or padded video with NVIDIA acceleration
- Fix playback of interlaced mpeg2video content with NVIDIA acceleration
- Fix playback of all interlaced content with QSV acceleration
- Fix adding songs to collections from search results page
- Fix bug scheduling mid-roll filler with content that contains one chapter
- No mid-roll filler will be inserted for content with zero or one chapters
- Fix thread sync bug with `HLS Segmenter` (and `MPEG-TS`) streaming modes
- Fix path replacement bug when media server path is left blank
### Added
- Add automated error reporting via Bugsnag
- This can be disabled by editing the `appsettings.json` file or by setting the `Bugsnag:Enable` environment variable to `false`
- Add `album_artist` to song metadata and to search index
- Display `album_artist` on some song videos when it's different than the `artist`
### Changed
- Framerate normalization will never normalize framerate below 24fps
- Instead, content with a lower framerate will be normalized up to 24fps
- `Shuffle In Order` will group songs by album artist instead of by track artist
## [0.4.2-alpha] - 2022-02-26
### Fixed
- Add improved but experimental transcoder logic, which can be toggled on and off in `Settings`
- Fix `HLS Segmenter` bug when source video packet contains no duration (`N/A`)
- Fix green line at the bottom of some content scaled using QSV acceleration
### Added
- Add configurable channel group (M3U) and categories (XMLTV)
- Add `Shuffle Schedule Items` option to schedule configuration
- When this is enabled, schedule items will be shuffled rather than looped in order
- **To support this, all playouts will be rebuilt (one time) after upgrading to this version**
### Changed
- Disable framerate normalization by default and on all ffmpeg profiles
- If framerate normalization is desired (not typically needed), it can be re-enabled manually
- Show watermarks over songs
- Hide unused local libraries
## [0.4.1-alpha] - 2022-02-10
### Fixed
- Normalize smart quotes in search queries as they are unsupported by the search library
@@ -967,7 +1008,9 @@ 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.4.1-alpha...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.3-alpha...HEAD
[0.4.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.2-alpha...v0.4.3-alpha
[0.4.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
+13 -15
View File
@@ -1,16 +1,14 @@
using System.Collections.Generic;
using System.Globalization;
using System.Globalization;
namespace ErsatzTV.Application.Artists
{
public record ArtistViewModel(
string Name,
string Disambiguation,
string Biography,
string Thumbnail,
string FanArt,
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
}
namespace ErsatzTV.Application.Artists;
public record ArtistViewModel(
string Name,
string Disambiguation,
string Biography,
string Thumbnail,
string FanArt,
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
+35 -41
View File
@@ -1,46 +1,40 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using ErsatzTV.Core.Domain;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Artists
namespace ErsatzTV.Application.Artists;
internal static class Mapper
{
internal static class Mapper
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
return new ArtistViewModel(
metadata.Title,
metadata.Disambiguation,
metadata.Biography,
Artwork(metadata, ArtworkKind.Thumbnail),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(),
metadata.Moods.Map(m => m.Name).ToList(),
LanguagesForArtist(languages));
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.ToList();
}
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
return new ArtistViewModel(
metadata.Title,
metadata.Disambiguation,
metadata.Biography,
Artwork(metadata, ArtworkKind.Thumbnail),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(),
metadata.Moods.Map(m => m.Name).ToList(),
LanguagesForArtist(languages));
}
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.ToList();
}
}
@@ -1,8 +1,5 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using MediatR;
using ErsatzTV.Application.MediaItems;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
}
namespace ErsatzTV.Application.Artists;
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
@@ -1,29 +1,22 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.Artists.Queries
namespace ErsatzTV.Application.Artists;
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
{
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
{
private readonly IArtistRepository _artistRepository;
private readonly IArtistRepository _artistRepository;
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = 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());
}
}
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());
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Artists;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
}
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
@@ -1,38 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists.Queries
namespace ErsatzTV.Application.Artists;
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
{
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
}
@@ -1,16 +1,17 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels
{
public record ChannelViewModel(
int Id,
string Number,
string Name,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
int PlayoutCount);
}
namespace ErsatzTV.Application.Channels;
public record ChannelViewModel(
int Id,
string Number,
string Name,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
int PlayoutCount);
@@ -1,18 +1,17 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannel
(
string Name,
string Number,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
}
namespace ErsatzTV.Application.Channels;
public record CreateChannel
(
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
@@ -1,162 +1,155 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
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),
await FillerPresetMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
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 static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
{
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"),
() =>
{
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
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."));
}
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
{
return Option<int>.None;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => PersistChannel(dbContext, c));
}
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
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),
await FillerPresetMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
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 static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
{
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"),
() =>
{
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
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."));
}
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
{
return Option<int>.None;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
}
namespace ErsatzTV.Application.Channels;
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
@@ -1,9 +1,5 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Channels.Commands
{
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
}
namespace ErsatzTV.Application.Channels;
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
@@ -1,28 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>>
{
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) =>
(await ChannelMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) =>
(await ChannelMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId);
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId);
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) =>
(await _channelRepository.Get(deleteChannel.ChannelId))
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
.Map(c => c.Id);
}
}
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) =>
(await _channelRepository.Get(deleteChannel.ChannelId))
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
.Map(c => c.Id);
}
@@ -1,19 +1,18 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands
{
public record UpdateChannel
(
int ChannelId,
string Name,
string Number,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
}
namespace ErsatzTV.Application.Channels;
public record UpdateChannel
(
int ChannelId,
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
@@ -1,122 +1,115 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
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;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
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(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))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
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 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 static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
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))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
}
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.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");
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
}
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.Group = update.Group;
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
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 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 static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
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))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
}
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.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");
}
+25 -26
View File
@@ -1,30 +1,29 @@
using System.Linq;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels
namespace ErsatzTV.Application.Channels;
internal static class Mapper
{
internal static class Mapper
{
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
new(
channel.Id,
channel.Number,
channel.Name,
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0);
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
new(
channel.Id,
channel.Number,
channel.Name,
channel.Group,
channel.Categories,
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
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);
}
}
private static string GetWatermark(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark))
.Match(a => a.Path, string.Empty);
}
@@ -1,7 +1,3 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Channels;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
}
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
@@ -1,21 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
}
}
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
}
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
@@ -1,20 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
@@ -1,6 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries;
namespace ErsatzTV.Application.Channels;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;
@@ -1,18 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries;
namespace ErsatzTV.Application.Channels;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
@@ -31,7 +23,7 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
{
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
@@ -58,12 +50,23 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
if (result < 24)
{
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
24,
result);
return 24;
}
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Iptv;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
@@ -1,20 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
{
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
}
}
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
}
@@ -1,8 +1,5 @@
using System.Collections.Generic;
using ErsatzTV.Core.Hdhr;
using MediatR;
using ErsatzTV.Core.Hdhr;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
@@ -1,22 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
{
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}
}
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Iptv;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
@@ -1,56 +1,50 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
{
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
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)
{
private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
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)
{
var result = new List<Channel>();
foreach (Channel channel in channels)
switch (mode.ToLowerInvariant())
{
switch (mode.ToLowerInvariant())
{
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
return result;
}
return result;
}
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
}
namespace ErsatzTV.Application.Configuration;
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
@@ -1,21 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration.Commands
namespace ErsatzTV.Application.Configuration;
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
{
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
private readonly IConfigElementRepository _configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
private readonly IConfigElementRepository _configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Configuration;
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,33 +1,28 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
namespace ErsatzTV.Application.Configuration;
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Where(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
}
}
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Where(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Configuration;
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,66 +1,59 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using System.Threading.Channels;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
namespace ErsatzTV.Application.Configuration;
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Configuration
{
public record ConfigElementViewModel(string Key, string Value);
}
namespace ErsatzTV.Application.Configuration;
public record ConfigElementViewModel(string Key, string Value);
+6 -7
View File
@@ -1,10 +1,9 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Configuration
namespace ErsatzTV.Application.Configuration;
internal static class Mapper
{
internal static class Mapper
{
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
}
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
@@ -1,8 +1,5 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
}
namespace ErsatzTV.Application.Configuration;
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
@@ -1,22 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Configuration.Mapper;
namespace ErsatzTV.Application.Configuration.Queries
namespace ErsatzTV.Application.Configuration;
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
}
}
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
}
@@ -1,6 +1,3 @@
using MediatR;
namespace ErsatzTV.Application.Configuration;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetLibraryRefreshInterval : IRequest<int>;
}
public record GetLibraryRefreshInterval : IRequest<int>;
@@ -1,21 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
namespace ErsatzTV.Application.Configuration;
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
}
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
@@ -1,6 +1,3 @@
using MediatR;
namespace ErsatzTV.Application.Configuration;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetPlayoutDaysToBuild : IRequest<int>;
}
public record GetPlayoutDaysToBuild : IRequest<int>;
@@ -1,21 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
namespace ErsatzTV.Application.Configuration;
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2));
}
}
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2));
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Emby;
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,46 +1,41 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
{
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{
private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_entityLocker = entityLocker;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
return Unit.Default;
}
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_entityLocker = entityLocker;
_searchIndex = searchIndex;
}
}
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
return Unit.Default;
}
}
@@ -1,8 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Emby;
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,60 +1,56 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
{
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
}
@@ -1,8 +1,6 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;
}
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;
@@ -1,118 +1,111 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
{
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
{
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_logger = logger;
_searchIndex = searchIndex;
}
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
await maybeLibraries.Match(
async libraries =>
{
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
},
error =>
{
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
return Task.CompletedTask;
});
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_logger = logger;
_searchIndex = searchIndex;
}
}
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
await maybeLibraries.Match(
async libraries =>
{
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
},
error =>
{
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
return Task.CompletedTask;
});
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
@@ -1,23 +1,20 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
IEmbyBackgroundServiceRequest
{
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
IEmbyBackgroundServiceRequest
{
int EmbyLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
int EmbyLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
@@ -1,181 +1,172 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyLibraryByIdHandler :
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{
public class SynchronizeEmbyLibraryByIdHandler :
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyMovieLibraryScanner embyMovieLibraryScanner,
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
ILibraryRepository libraryRepository,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyMovieLibraryScanner embyMovieLibraryScanner,
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
ILibraryRepository libraryRepository,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(ISynchronizeEmbyLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
switch (parameters.Library.MediaKind)
{
case LibraryMediaKind.Movies:
await _embyMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
case LibraryMediaKind.Shows:
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
}
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
}
_entityLocker.UnlockLibrary(parameters.Library.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters,
embyLibrary,
request.ForceScan,
libraryRefreshInterval,
ffprobePath
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizeEmbyLibraryById request) =>
EmbyMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source for library {request.EmbyLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath);
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_logger = logger;
}
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(ISynchronizeEmbyLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
switch (parameters.Library.MediaKind)
{
case LibraryMediaKind.Movies:
await _embyMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
case LibraryMediaKind.Shows:
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
}
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
}
_entityLocker.UnlockLibrary(parameters.Library.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters,
embyLibrary,
request.ForceScan,
libraryRefreshInterval,
ffprobePath
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizeEmbyLibraryById request) =>
EmbyMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source for library {request.EmbyLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath);
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
@@ -1,11 +1,7 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
}
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
@@ -1,41 +1,35 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
{
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
return mediaSources;
}
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
}
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
return mediaSources;
}
}
@@ -1,11 +1,8 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
namespace ErsatzTV.Application.Emby;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
}
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
@@ -1,42 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
{
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
return Unit.Default;
}
_mediaSourceRepository = mediaSourceRepository;
_searchIndex = searchIndex;
}
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
return Unit.Default;
}
}
@@ -1,12 +1,9 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
namespace ErsatzTV.Application.Emby;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
}
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
@@ -1,55 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
{
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
private readonly IMediaSourceRepository _mediaSourceRepository;
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateEmbyPathReplacements request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
private Task<Unit> MergePathReplacements(
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{
private readonly IMediaSourceRepository _mediaSourceRepository;
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
var incoming = request.PathReplacements.Map(Project).ToList();
public Task<Either<BaseError, Unit>> Handle(
UpdateEmbyPathReplacements request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
private Task<Unit> MergePathReplacements(
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
var incoming = request.PathReplacements.Map(Project).ToList();
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
}
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyConnectionParametersViewModel(string Address);
}
namespace ErsatzTV.Application.Emby;
public record EmbyConnectionParametersViewModel(string Address);
@@ -1,8 +1,7 @@
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby
{
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
: LibraryViewModel("Emby", Id, Name, MediaKind);
}
namespace ErsatzTV.Application.Emby;
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
: LibraryViewModel("Emby", Id, Name, MediaKind);
@@ -1,9 +1,8 @@
using ErsatzTV.Application.MediaSources;
namespace ErsatzTV.Application.Emby
{
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
Id,
Name,
Address);
}
namespace ErsatzTV.Application.Emby;
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
Id,
Name,
Address);
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
}
namespace ErsatzTV.Application.Emby;
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
+13 -14
View File
@@ -1,19 +1,18 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby
namespace ErsatzTV.Application.Emby;
internal static class Mapper
{
internal static class Mapper
{
internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) =>
new(
embyMediaSource.Id,
embyMediaSource.ServerName,
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) =>
new(
embyMediaSource.Id,
embyMediaSource.ServerName,
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
}
}
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
}
@@ -1,7 +1,3 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
}
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
@@ -1,24 +1,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>>
{
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyMediaSourceViewModel>> Handle(
GetAllEmbyMediaSources request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
}
}
public Task<List<EmbyMediaSourceViewModel>> Handle(
GetAllEmbyMediaSources request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
}
@@ -1,8 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
}
namespace ErsatzTV.Application.Emby;
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
@@ -1,73 +1,66 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
{
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{
_memoryCache = memoryCache;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
GetEmbyConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
_memoryCache = memoryCache;
_mediaSourceRepository = mediaSourceRepository;
}
}
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
GetEmbyConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
}
@@ -1,7 +1,3 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
}
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
@@ -1,26 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
{
public class
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyLibraryViewModel>> Handle(
GetEmbyLibrariesBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}
public Task<List<EmbyLibraryViewModel>> Handle(
GetEmbyLibrariesBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
}
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
@@ -1,23 +1,18 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
{
public class
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Option<EmbyMediaSourceViewModel>> Handle(
GetEmbyMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
}
}
public Task<Option<EmbyMediaSourceViewModel>> Handle(
GetEmbyMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
}
@@ -1,8 +1,4 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyPathReplacementsBySourceId
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
}
public record GetEmbyPathReplacementsBySourceId
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
@@ -1,26 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId,
List<EmbyPathReplacementViewModel>>
{
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId,
List<EmbyPathReplacementViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyPathReplacementViewModel>> Handle(
GetEmbyPathReplacementsBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}
public Task<List<EmbyPathReplacementViewModel>> Handle(
GetEmbyPathReplacementsBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbySecrets : IRequest<EmbySecrets>;
}
namespace ErsatzTV.Application.Emby;
public record GetEmbySecrets : IRequest<EmbySecrets>;
@@ -1,19 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
namespace ErsatzTV.Application.Emby;
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets>
{
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets>
{
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbySecretStore _embySecretStore;
public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) =>
_embySecretStore = embySecretStore;
public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) =>
_embySecretStore = embySecretStore;
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
_embySecretStore.ReadSecrets();
}
}
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
_embySecretStore.ReadSecrets();
}
+3 -4
View File
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application
{
public record EntityIdResult(int Id);
}
namespace ErsatzTV.Application;
public record EntityIdResult(int Id);
@@ -3,12 +3,14 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.0.0" />
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -0,0 +1,43 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=emby_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=emby_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=hdhr_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=hdhr_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=health_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=images_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=images_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=jellyfin_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=jellyfin_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=libraries_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=libraries_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=logs_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=maintenance_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacards_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacollections_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacollections_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediaitems_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediasources_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=movies_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playouts_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playouts_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
@@ -1,9 +1,6 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CopyFFmpegProfile
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record CopyFFmpegProfile
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
@@ -1,37 +1,32 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
public class
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CopyFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformCopy)
.Bind(v => v.ToEitherAsync());
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CopyFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformCopy)
.Bind(v => v.ToEitherAsync());
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel);
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel);
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request);
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request);
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
}
}
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
}
@@ -1,29 +1,26 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CreateFFmpegProfile(
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record CreateFFmpegProfile(
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
@@ -1,80 +1,75 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{
public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
}
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
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
{
Name = name,
ThreadCount = threadCount,
Transcode = request.Transcode,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
ResolutionId = resolutionId,
NormalizeVideo = request.NormalizeVideo,
VideoCodec = request.VideoCodec,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioCodec = request.AudioCodec,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio,
NormalizeFramerate = request.NormalizeFramerate
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
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"));
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, profile => PersistFFmpegProfile(dbContext, profile));
}
}
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
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
{
Name = name,
ThreadCount = threadCount,
Transcode = request.Transcode,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
ResolutionId = resolutionId,
NormalizeVideo = request.NormalizeVideo,
VideoCodec = request.VideoCodec,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioCodec = request.AudioCodec,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio,
NormalizeFramerate = request.NormalizeFramerate
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
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"));
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,43 +1,38 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
{
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
DeleteFFmpegProfile request,
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 static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
return LanguageExt.Unit.Default;
}
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"));
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p));
}
}
private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
return LanguageExt.Unit.Default;
}
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"));
}
@@ -1,10 +1,7 @@
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
/// <summary>
/// Requests a new ffmpeg profile (view model) that contains
/// appropriate default values.
/// </summary>
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
}
/// <summary>
/// Requests a new ffmpeg profile (view model) that contains
/// appropriate default values.
/// </summary>
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
@@ -1,39 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
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;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
{
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
int defaultResolutionId = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
.IfNoneAsync(0);
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
List<Resolution> allResolutions = await dbContext.Resolutions
.ToListAsync(cancellationToken);
int defaultResolutionId = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
.IfNoneAsync(0);
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());
List<Resolution> allResolutions = await dbContext.Resolutions
.ToListAsync(cancellationToken);
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
}
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
}
}
}
@@ -1,30 +1,27 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegProfile(
int FFmpegProfileId,
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record UpdateFFmpegProfile(
int FFmpegProfileId,
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
@@ -1,87 +1,82 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
{
public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
{
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.Transcode = update.Transcode;
p.HardwareAcceleration = update.HardwareAcceleration;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
p.ResolutionId = update.ResolutionId;
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
p.VideoCodec = update.VideoCodec;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id);
}
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 static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
dbContext.FFmpegProfiles
.SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>("FFmpegProfile does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
private static Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
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"));
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
}
}
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
{
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.Transcode = update.Transcode;
p.HardwareAcceleration = update.HardwareAcceleration;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
p.ResolutionId = update.ResolutionId;
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
p.VideoCodec = update.VideoCodec;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id);
}
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 static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
dbContext.FFmpegProfiles
.SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>("FFmpegProfile does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
private static Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
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"));
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest<Either<BaseError, Unit>>;
@@ -1,129 +1,128 @@
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
namespace ErsatzTV.Application.FFmpegProfiles;
public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
{
public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
public UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem)
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
public UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem)
{
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
}
public Task<Either<BaseError, Unit>> Handle(
UpdateFFmpegSettings request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyUpdate(request))
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
(await FFmpegMustExist(request), await FFprobeMustExist(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
{
if (!_localFileSystem.FileExists(path))
{
return BaseError.New($"{name} path does not exist");
}
var startInfo = new ProcessStartInfo
{
FileName = path,
Arguments = "-version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
var test = new Process
{
StartInfo = startInfo
};
test.Start();
string output = await test.StandardOutput.ReadToEndAsync();
await test.WaitForExitAsync();
return test.ExitCode == 0 && output.Contains($"{name} version")
? Unit.Default
: BaseError.New($"Unable to verify {name} version");
}
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{
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 _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);
}
if (request.Settings.GlobalFallbackFillerId is not null)
{
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegGlobalFallbackFillerId,
request.Settings.GlobalFallbackFillerId.Value);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSegmenterTimeout,
request.Settings.HlsSegmenterIdleTimeout);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegWorkAheadSegmenters,
request.Settings.WorkAheadSegmenterLimit);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegInitialSegmentCount,
request.Settings.InitialSegmentCount);
return Unit.Default;
}
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
}
}
public Task<Either<BaseError, Unit>> Handle(
UpdateFFmpegSettings request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => ApplyUpdate(request))
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
(await FFmpegMustExist(request), await FFprobeMustExist(request))
.Apply((_, _) => Unit.Default);
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
{
if (!_localFileSystem.FileExists(path))
{
return BaseError.New($"{name} path does not exist");
}
var startInfo = new ProcessStartInfo
{
FileName = path,
Arguments = "-version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
var test = new Process
{
StartInfo = startInfo
};
test.Start();
string output = await test.StandardOutput.ReadToEndAsync();
await test.WaitForExitAsync();
return test.ExitCode == 0 && output.Contains($"{name} version")
? Unit.Default
: BaseError.New($"Unable to verify {name} version");
}
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{
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 _configElementRepository.Upsert(
ConfigElementKey.FFmpegUseLegacyTranscoder,
request.Settings.UseLegacyTranscoder.ToString());
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);
}
if (request.Settings.GlobalFallbackFillerId is not null)
{
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegGlobalFallbackFillerId,
request.Settings.GlobalFallbackFillerId.Value);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSegmenterTimeout,
request.Settings.HlsSegmenterIdleTimeout);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegWorkAheadSegmenters,
request.Settings.WorkAheadSegmenterLimit);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegInitialSegmentCount,
request.Settings.InitialSegmentCount);
return Unit.Default;
}
}
@@ -2,27 +2,26 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
namespace ErsatzTV.Application.FFmpegProfiles
{
public record FFmpegProfileViewModel(
int Id,
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
ResolutionViewModel Resolution,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate);
}
namespace ErsatzTV.Application.FFmpegProfiles;
public record FFmpegProfileViewModel(
int Id,
string Name,
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
ResolutionViewModel Resolution,
bool NormalizeVideo,
string VideoCodec,
int VideoBitrate,
int VideoBufferSize,
string AudioCodec,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
bool NormalizeFramerate);
@@ -1,16 +1,16 @@
namespace ErsatzTV.Application.FFmpegProfiles
namespace ErsatzTV.Application.FFmpegProfiles;
public class FFmpegSettingsViewModel
{
public class FFmpegSettingsViewModel
{
public string FFmpegPath { get; set; }
public string FFprobePath { get; set; }
public int DefaultFFmpegProfileId { get; set; }
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public int? GlobalWatermarkId { get; set; }
public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
}
}
public string FFmpegPath { get; set; }
public string FFprobePath { get; set; }
public int DefaultFFmpegProfileId { get; set; }
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public int? GlobalWatermarkId { get; set; }
public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
public bool UseLegacyTranscoder { get; set; }
}
+29 -30
View File
@@ -1,34 +1,33 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles
{
internal static class Mapper
{
internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) =>
new(
profile.Id,
profile.Name,
profile.ThreadCount,
profile.Transcode,
profile.HardwareAcceleration,
profile.VaapiDriver,
profile.VaapiDevice,
Project(profile.Resolution),
profile.NormalizeVideo,
profile.VideoCodec,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.AudioCodec,
profile.AudioBitrate,
profile.AudioBufferSize,
profile.NormalizeLoudness,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeAudio,
profile.NormalizeVideo && profile.NormalizeFramerate);
namespace ErsatzTV.Application.FFmpegProfiles;
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}
}
internal static class Mapper
{
internal static FFmpegProfileViewModel ProjectToViewModel(FFmpegProfile profile) =>
new(
profile.Id,
profile.Name,
profile.ThreadCount,
profile.Transcode,
profile.HardwareAcceleration,
profile.VaapiDriver,
profile.VaapiDevice,
Project(profile.Resolution),
profile.NormalizeVideo,
profile.VideoCodec,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.AudioCodec,
profile.AudioBitrate,
profile.AudioBufferSize,
profile.NormalizeLoudness,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeAudio,
profile.NormalizeVideo && profile.NormalizeFramerate);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
}
@@ -1,7 +1,3 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
}
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
@@ -1,31 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
namespace ErsatzTV.Application.FFmpegProfiles;
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetFFmpegProfileById(int Id) : IRequest<Option<FFmpegProfileViewModel>>;
}
public record GetFFmpegProfileById(int Id) : IRequest<Option<FFmpegProfileViewModel>>;
@@ -1,30 +1,25 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Infrastructure.Data;
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
namespace ErsatzTV.Application.FFmpegProfiles;
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
{
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request,
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);
}
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);
}
}
}
@@ -1,6 +1,3 @@
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public record GetFFmpegSettings : IRequest<FFmpegSettingsViewModel>;
}
public record GetFFmpegSettings : IRequest<FFmpegSettingsViewModel>;
@@ -1,65 +1,63 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
namespace ErsatzTV.Application.FFmpegProfiles;
public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpegSettingsViewModel>
{
public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpegSettingsViewModel>
private readonly IConfigElementRepository _configElementRepository;
public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<FFmpegSettingsViewModel> Handle(
GetFFmpegSettings request,
CancellationToken cancellationToken)
{
private readonly IConfigElementRepository _configElementRepository;
Option<string> ffmpegPath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath);
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
Option<int> defaultFFmpegProfileId =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
Option<bool> saveReports =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
Option<int> hlsSegmenterIdleTimeout =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
Option<bool> useLegacyTranscoder =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseLegacyTranscoder);
public GetFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<FFmpegSettingsViewModel> Handle(
GetFFmpegSettings request,
CancellationToken cancellationToken)
var result = new FFmpegSettingsViewModel
{
Option<string> ffmpegPath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath);
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
Option<int> defaultFFmpegProfileId =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
Option<bool> saveReports =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
Option<int> hlsSegmenterIdleTimeout =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
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"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1),
UseLegacyTranscoder = await useLegacyTranscoder.IfNoneAsync(false)
};
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"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
};
foreach (int watermarkId in watermark)
{
result.GlobalWatermarkId = watermarkId;
}
foreach (int fallbackFillerId in fallbackFiller)
{
result.GlobalFallbackFillerId = fallbackFillerId;
}
return result;
foreach (int watermarkId in watermark)
{
result.GlobalWatermarkId = watermarkId;
}
foreach (int fallbackFillerId in fallbackFiller)
{
result.GlobalFallbackFillerId = fallbackFillerId;
}
return result;
}
}
}
@@ -1,24 +1,19 @@
using System;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public record CreateFillerPreset(
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
) : IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Filler;
public record CreateFillerPreset(
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
) : IRequest<Either<BaseError, Unit>>;

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