dependencies and code cleanup (#2117)

* fix validation in new form layout

* pin mediatr to last oss version

* update dependencies

* cleanup code in core

* cleanup code in ffmpeg

* cleanup code in infra

* cleanup code in scanner

* cleanup code in application

* cleanup main code

* cleanup test code

* solution-wide code cleanup
This commit is contained in:
Jason Dove
2025-07-06 15:56:17 +00:00
committed by GitHub
parent fa6a31b4fc
commit 7e30444857
313 changed files with 4207 additions and 4202 deletions
+3 -2
View File
@@ -3,10 +3,11 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2024.1.1", "version": "2025.1.4",
"commands": [ "commands": [
"jb" "jb"
] ],
"rollForward": false
} }
} }
} }
+4 -3
View File
@@ -29,9 +29,10 @@ internal static class Mapper
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages return languages
.Map( .Map(lang => allCultures.Filter(ci => string.Equals(
lang => allCultures.Filter( ci.ThreeLetterISOLanguageName,
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) lang,
StringComparison.OrdinalIgnoreCase)))
.Flatten() .Flatten()
.Distinct() .Distinct()
.ToList(); .ToList();
@@ -6,7 +6,8 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Artworks; namespace ErsatzTV.Application.Artworks;
public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<GetArtwork, Either<BaseError, Artwork>> public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetArtwork, Either<BaseError, Artwork>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory = dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory = dbContextFactory;
@@ -14,7 +15,8 @@ public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory) :
GetArtwork request, GetArtwork request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
try { try
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Artwork> artwork = await dbContext.Artwork Option<Artwork> artwork = await dbContext.Artwork
@@ -23,7 +25,6 @@ public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory) :
.MapT(Project); .MapT(Project);
return artwork.ToEither(BaseError.New("Artwork not found")); return artwork.ToEither(BaseError.New("Artwork not found"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -31,12 +32,11 @@ public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory) :
} }
} }
private static Artwork Project(Artwork artwork) private static Artwork Project(Artwork artwork) =>
new()
{ {
return new Artwork {
Id = artwork.Id, Id = artwork.Id,
Path = artwork.Path, Path = artwork.Path,
ArtworkKind = artwork.ArtworkKind ArtworkKind = artwork.ArtworkKind
}; };
}
} }
@@ -1,6 +1,6 @@
using ErsatzTV.Core.Domain;
using System.Net; using System.Net;
using ErsatzTV.Application.Artworks; using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels; namespace ErsatzTV.Application.Channels;
@@ -40,8 +40,7 @@ public class CreateChannelHandler(
await FFmpegProfileMustExist(dbContext, request), await FFmpegProfileMustExist(dbContext, request),
await WatermarkMustExist(dbContext, request), await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request)) await FillerPresetMustExist(dbContext, request))
.Apply( .Apply((
(
name, name,
number, number,
ffmpegProfileId, ffmpegProfileId,
@@ -62,7 +61,9 @@ public class CreateChannelHandler(
{ {
Path = logo, Path = logo,
ArtworkKind = ArtworkKind.Logo, ArtworkKind = ArtworkKind.Logo,
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null, OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType)
? request.Logo.ContentType
: null,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow DateUpdated = DateTime.UtcNow
}); });
@@ -168,8 +169,7 @@ public class CreateChannelHandler(
.Map(Optional) .Map(Optional)
.Filter(c => c > 0) .Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId)) .MapT(_ => Optional(createChannel.FallbackFillerId))
.Map( .Map(o => o.ToValidation<BaseError>(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist.")); $"Fallback filler {createChannel.FallbackFillerId} does not exist."));
} }
} }
@@ -296,7 +296,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
int finishIndex = j; int finishIndex = j;
while (finishIndex + 1 < sorted.Count && (sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup while (finishIndex + 1 < sorted.Count && (sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup
|| sorted[finishIndex + 1].FillerKind is FillerKind.GuideMode || sorted[finishIndex + 1].FillerKind is FillerKind.GuideMode
or FillerKind.PostRoll or FillerKind.Tail or FillerKind.Fallback or FillerKind.DecoDefault)) or FillerKind.PostRoll or FillerKind.Tail
or FillerKind.Fallback or FillerKind.DecoDefault))
{ {
finishIndex++; finishIndex++;
} }
@@ -12,5 +12,6 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) => public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll() _channelRepository.GetAll()
.Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active).Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList()); .Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active)
.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
} }
@@ -14,8 +14,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) => public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll() _channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode)) .Map(channels => EnsureMode(channels, request.Mode))
.Map( .Map(channels => new ChannelPlaylist(
channels => new ChannelPlaylist(
request.Scheme, request.Scheme,
request.Host, request.Host,
request.BaseUrl, request.BaseUrl,
@@ -16,8 +16,7 @@ public class UpdateLibraryRefreshIntervalHandler :
UpdateLibraryRefreshInterval request, UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT( .MapT(_ => _configElementRepository.Upsert(
_ => _configElementRepository.Upsert(
ConfigElementKey.LibraryRefreshInterval, ConfigElementKey.LibraryRefreshInterval,
request.LibraryRefreshInterval)) request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
@@ -43,7 +43,6 @@ public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathR
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist( private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) => UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId) _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map( .Map(v => v.ToValidation<BaseError>(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist.")); $"Emby media source {request.EmbyMediaSourceId} does not exist."));
} }
@@ -54,8 +54,7 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() => private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone()) _mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map( .Map(v => v.ToValidation<BaseError>(
v => v.ToValidation<BaseError>(
"Emby media source does not exist.")); "Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
@@ -12,7 +12,7 @@
<PackageReference Include="Bugsnag" Version="4.0.0" /> <PackageReference Include="Bugsnag" Version="4.0.0" />
<PackageReference Include="CliWrap" Version="3.9.0" /> <PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" /> <PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.5.0" /> <PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
@@ -42,8 +42,7 @@ public class CreateFFmpegProfileHandler :
TvContext dbContext, TvContext dbContext,
CreateFFmpegProfile request) => CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request)) (ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply( .Apply((name, threadCount, resolutionId) => new FFmpegProfile
(name, threadCount, resolutionId) => new FFmpegProfile
{ {
Name = name, Name = name,
ThreadCount = threadCount, ThreadCount = threadCount,
@@ -16,8 +16,7 @@ public class UpdateHDHRTunerCountHandler : IRequestHandler<UpdateHDHRTunerCount,
UpdateHDHRTunerCount request, UpdateHDHRTunerCount request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT( .MapT(_ => _configElementRepository.Upsert(
_ => _configElementRepository.Upsert(
ConfigElementKey.HDHRTunerCount, ConfigElementKey.HDHRTunerCount,
request.TunerCount.ToString(CultureInfo.InvariantCulture))) request.TunerCount.ToString(CultureInfo.InvariantCulture)))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
@@ -13,10 +13,9 @@ public class GetHDHRUUIDHandler : IRequestHandler<GetHDHRUUID, Guid>
public async Task<Guid> Handle(GetHDHRUUID request, CancellationToken cancellationToken) public async Task<Guid> Handle(GetHDHRUUID request, CancellationToken cancellationToken)
{ {
Option<Guid> maybeGuid = await _configElementRepository.GetValue<Guid>(ConfigElementKey.HDHRUUID); Option<Guid> maybeGuid = await _configElementRepository.GetValue<Guid>(ConfigElementKey.HDHRUUID);
return await maybeGuid.IfNoneAsync( return await maybeGuid.IfNoneAsync(async () =>
async () =>
{ {
Guid guid = Guid.NewGuid(); var guid = Guid.NewGuid();
await _configElementRepository.Upsert(ConfigElementKey.HDHRUUID, guid); await _configElementRepository.Upsert(ConfigElementKey.HDHRUUID, guid);
return guid; return guid;
}); });
@@ -4,4 +4,5 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images; namespace ErsatzTV.Application.Images;
// ReSharper disable once SuggestBaseTypeForParameter // ReSharper disable once SuggestBaseTypeForParameter
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType) : IRequest<Either<BaseError, string>>; public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType)
: IRequest<Either<BaseError, string>>;
@@ -97,9 +97,8 @@ public class UpdateImageFolderDurationHandler(IDbContextFactory<TvContext> dbCon
// update all images in this folder // update all images in this folder
await dbContext.ImageMetadata await dbContext.ImageMetadata
.Filter( .Filter(im =>
im => im.Image.MediaVersions.Any( im.Image.MediaVersions.Any(mv => mv.MediaFiles.Any(mf => mf.LibraryFolderId == currentFolder.Id)))
mv => mv.MediaFiles.Any(mf => mf.LibraryFolderId == currentFolder.Id)))
.ExecuteUpdateAsync( .ExecuteUpdateAsync(
setters => setters.SetProperty(im => im.DurationSeconds, effectiveDuration), setters => setters.SetProperty(im => im.DurationSeconds, effectiveDuration),
cancellationToken); cancellationToken);
@@ -3,5 +3,6 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images; namespace ErsatzTV.Application.Images;
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null) : IRequest< public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null)
: IRequest<
Either<BaseError, CachedImagePathViewModel>>; Either<BaseError, CachedImagePathViewModel>>;
@@ -43,7 +43,6 @@ public class UpdateJellyfinPathReplacementsHandler : IRequestHandler<UpdateJelly
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist( private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist(
UpdateJellyfinPathReplacements request) => UpdateJellyfinPathReplacements request) =>
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId) _mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
.Map( .Map(v => v.ToValidation<BaseError>(
v => v.ToValidation<BaseError>(
$"Jellyfin media source {request.JellyfinMediaSourceId} does not exist.")); $"Jellyfin media source {request.JellyfinMediaSourceId} does not exist."));
} }
@@ -48,8 +48,7 @@ public class GetJellyfinConnectionParametersHandler : IRequestHandler<GetJellyfi
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist() => private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist() =>
_mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone()) _mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone())
.Map( .Map(v => v.ToValidation<BaseError>(
v => v.ToValidation<BaseError>(
"Jellyfin media source does not exist.")); "Jellyfin media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
@@ -54,8 +54,8 @@ public abstract class CallLibraryScannerHandler<TRequest>
{ {
using var forcefulCts = new CancellationTokenSource(); using var forcefulCts = new CancellationTokenSource();
await using CancellationTokenRegistration link = cancellationToken.Register( await using CancellationTokenRegistration link =
() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10)) cancellationToken.Register(() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10))
); );
CommandResult process = await Cli.Wrap(scanner) CommandResult process = await Cli.Wrap(scanner)
@@ -64,8 +64,7 @@ public class CreateLocalLibraryHandler : LocalLibraryHandlerBase,
.OrderBy(lms => lms.Id) .OrderBy(lms => lms.Id)
.FirstOrDefaultAsync() .FirstOrDefaultAsync()
.Map(Optional) .Map(Optional)
.MapT( .MapT(lms => new LocalLibrary
lms => new LocalLibrary
{ {
Name = request.Name, Name = request.Name,
Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(), Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(),
@@ -44,10 +44,10 @@ public abstract class LocalLibraryHandlerBase
// Images and OtherVideos do not conflict // Images and OtherVideos do not conflict
if (isConflict) if (isConflict)
{ {
bool imagesAndOtherVideos = (path1.MediaKind is LibraryMediaKind.Images && bool imagesAndOtherVideos = path1.MediaKind is LibraryMediaKind.Images &&
path2.MediaKind is LibraryMediaKind.OtherVideos) path2.MediaKind is LibraryMediaKind.OtherVideos
|| (path2.MediaKind is LibraryMediaKind.Images && || path2.MediaKind is LibraryMediaKind.Images &&
path1.MediaKind is LibraryMediaKind.OtherVideos); path1.MediaKind is LibraryMediaKind.OtherVideos;
if (imagesAndOtherVideos) if (imagesAndOtherVideos)
{ {
@@ -102,8 +102,7 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
UpdateLocalLibrary request) => UpdateLocalLibrary request) =>
LocalLibraryMustExist(dbContext, request) LocalLibraryMustExist(dbContext, request)
.BindT(parameters => NameMustBeValid(request, parameters.Incoming).MapT(_ => parameters)) .BindT(parameters => NameMustBeValid(request, parameters.Incoming).MapT(_ => parameters))
.BindT( .BindT(parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id)
parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id)
.MapT(_ => parameters)); .MapT(_ => parameters));
private static Task<Validation<BaseError, Parameters>> LocalLibraryMustExist( private static Task<Validation<BaseError, Parameters>> LocalLibraryMustExist(
@@ -112,8 +111,7 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
dbContext.LocalLibraries dbContext.LocalLibraries
.Include(ll => ll.Paths) .Include(ll => ll.Paths)
.SelectOneAsync(ll => ll.Id, ll => ll.Id == request.Id) .SelectOneAsync(ll => ll.Id, ll => ll.Id == request.Id)
.MapT( .MapT(existing =>
existing =>
{ {
var incoming = new LocalLibrary var incoming = new LocalLibrary
{ {
@@ -14,8 +14,7 @@ public class GetAllLocalLibrariesHandler : IRequestHandler<GetAllLocalLibraries,
GetAllLocalLibraries request, GetAllLocalLibraries request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_libraryRepository.GetAll() _libraryRepository.GetAll()
.Map( .Map(list => list
list => list
.OfType<LocalLibrary>() .OfType<LocalLibrary>()
.OrderBy(l => l.MediaKind) .OrderBy(l => l.MediaKind)
.Map(ProjectToViewModel) .Map(ProjectToViewModel)
@@ -15,8 +15,7 @@ public class GetConfiguredLibrariesHandler : IRequestHandler<GetConfiguredLibrar
GetConfiguredLibraries request, GetConfiguredLibraries request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_libraryRepository.GetAll() _libraryRepository.GetAll()
.Map( .Map(list => list.Filter(ShouldIncludeLibrary)
list => list.Filter(ShouldIncludeLibrary)
.OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1) .OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1)
.ThenBy(l => l.GetType().Name) .ThenBy(l => l.GetType().Name)
.ThenBy(l => l.MediaKind) .ThenBy(l => l.MediaKind)
@@ -46,8 +46,13 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
.Map(jms => jms.Id) .Map(jms => jms.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return jellyfinMediaSourceIds.Map( return jellyfinMediaSourceIds.Map(id => new LibraryViewModel(
id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty)); "Jellyfin",
0,
"Collections",
0,
id,
string.Empty));
} }
private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections( private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections(
@@ -59,7 +64,6 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
.Map(pms => pms.Id) .Map(pms => pms.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return plexMediaSourceIds.Map( return plexMediaSourceIds.Map(id => new LibraryViewModel("Plex", 0, "Collections", 0, id, string.Empty));
id => new LibraryViewModel("Plex", 0, "Collections", 0, id, string.Empty));
} }
} }
@@ -27,8 +27,8 @@ public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, P
if (!string.IsNullOrWhiteSpace(request.Filter)) if (!string.IsNullOrWhiteSpace(request.Filter))
{ {
entries = entries.Filter( entries = entries.Filter(le =>
le => le.Level.ToString().Contains(request.Filter, StringComparison.OrdinalIgnoreCase) || le.Level.ToString().Contains(request.Filter, StringComparison.OrdinalIgnoreCase) ||
le.Message.Contains(request.Filter, StringComparison.OrdinalIgnoreCase)); le.Message.Contains(request.Filter, StringComparison.OrdinalIgnoreCase));
} }
+3 -4
View File
@@ -171,8 +171,8 @@ internal static class Mapper
Option<EmbyMediaSource> maybeEmby) => Option<EmbyMediaSource> maybeEmby) =>
new( new(
collection.Name, collection.Name,
collection.MediaItems.OfType<Movie>().Map( collection.MediaItems.OfType<Movie>().Map(m =>
m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with
{ {
CustomIndex = GetCustomIndex(collection, m.Id) CustomIndex = GetCustomIndex(collection, m.Id)
}).ToList(), }).ToList(),
@@ -183,8 +183,7 @@ internal static class Mapper
.ToList(), .ToList(),
// collection view doesn't use local paths // collection view doesn't use local paths
collection.MediaItems.OfType<Episode>() collection.MediaItems.OfType<Episode>()
.Map( .Map(e => ProjectToViewModel(
e => ProjectToViewModel(
e.EpisodeMetadata.Head(), e.EpisodeMetadata.Head(),
maybeJellyfin, maybeJellyfin,
maybeEmby, maybeEmby,
@@ -15,9 +15,9 @@ public class AddArtistToCollectionHandler :
IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>> IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddArtistToCollectionHandler( public AddArtistToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddEpisodeToCollectionHandler :
IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>> IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddEpisodeToCollectionHandler( public AddEpisodeToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -14,9 +14,9 @@ namespace ErsatzTV.Application.MediaCollections;
public class AddImageToCollectionHandler : IRequestHandler<AddImageToCollection, Either<BaseError, Unit>> public class AddImageToCollectionHandler : IRequestHandler<AddImageToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddImageToCollectionHandler( public AddImageToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,10 +15,10 @@ public class AddItemsToCollectionHandler :
IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>> IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly IMovieRepository _movieRepository; private readonly IMovieRepository _movieRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly ITelevisionRepository _televisionRepository; private readonly ITelevisionRepository _televisionRepository;
public AddItemsToCollectionHandler( public AddItemsToCollectionHandler(
@@ -15,9 +15,9 @@ public class AddMovieToCollectionHandler :
IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>> IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddMovieToCollectionHandler( public AddMovieToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddMusicVideoToCollectionHandler :
IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>> IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddMusicVideoToCollectionHandler( public AddMusicVideoToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddOtherVideoToCollectionHandler :
IRequestHandler<AddOtherVideoToCollection, Either<BaseError, Unit>> IRequestHandler<AddOtherVideoToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddOtherVideoToCollectionHandler( public AddOtherVideoToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddSeasonToCollectionHandler :
IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>> IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddSeasonToCollectionHandler( public AddSeasonToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddShowToCollectionHandler :
IRequestHandler<AddShowToCollection, Either<BaseError, Unit>> IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddShowToCollectionHandler( public AddShowToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -15,9 +15,9 @@ public class AddSongToCollectionHandler :
IRequestHandler<AddSongToCollection, Either<BaseError, Unit>> IRequestHandler<AddSongToCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public AddSongToCollectionHandler( public AddSongToCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -89,11 +89,11 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
return maybeList.Map(_ => Unit.Default); return maybeList.Map(_ => Unit.Default);
} }
private sealed record Parameters(string User, string List);
[GeneratedRegex(@"https:\/\/trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")] [GeneratedRegex(@"https:\/\/trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
private static partial Regex UriTraktListRegex(); private static partial Regex UriTraktListRegex();
[GeneratedRegex(@"([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")] [GeneratedRegex(@"([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
private static partial Regex ShorthandTraktListRegex(); private static partial Regex ShorthandTraktListRegex();
private sealed record Parameters(string User, string List);
} }
@@ -39,8 +39,7 @@ public class CreateCollectionHandler :
private static Task<Validation<BaseError, Collection>> Validate( private static Task<Validation<BaseError, Collection>> Validate(
TvContext dbContext, TvContext dbContext,
CreateCollection request) => CreateCollection request) =>
ValidateName(dbContext, request).MapT( ValidateName(dbContext, request).MapT(name => new Collection
name => new Collection
{ {
Name = name, Name = name,
MediaItems = new List<MediaItem>() MediaItems = new List<MediaItem>()
@@ -51,12 +51,10 @@ public class CreateMultiCollectionHandler :
private static Task<Validation<BaseError, MultiCollection>> Validate( private static Task<Validation<BaseError, MultiCollection>> Validate(
TvContext dbContext, TvContext dbContext,
CreateMultiCollection request) => CreateMultiCollection request) =>
ValidateName(dbContext, request).MapT( ValidateName(dbContext, request).MapT(name => new MultiCollection
name => new MultiCollection
{ {
Name = name, Name = name,
MultiCollectionItems = request.Items.Bind( MultiCollectionItems = request.Items.Bind(i =>
i =>
{ {
if (i.CollectionId.HasValue) if (i.CollectionId.HasValue)
{ {
@@ -72,8 +70,7 @@ public class CreateMultiCollectionHandler :
return Option<MultiCollectionItem>.None; return Option<MultiCollectionItem>.None;
}) })
.ToList(), .ToList(),
MultiCollectionSmartItems = request.Items.Bind( MultiCollectionSmartItems = request.Items.Bind(i =>
i =>
{ {
if (i.SmartCollectionId.HasValue) if (i.SmartCollectionId.HasValue)
{ {
@@ -25,8 +25,7 @@ public class CreatePlaylistHandler(IDbContextFactory<TvContext> dbContextFactory
} }
private static async Task<Validation<BaseError, Playlist>> Validate(TvContext dbContext, CreatePlaylist request) => private static async Task<Validation<BaseError, Playlist>> Validate(TvContext dbContext, CreatePlaylist request) =>
await ValidatePlaylistName(dbContext, request).MapT( await ValidatePlaylistName(dbContext, request).MapT(name => new Playlist
name => new Playlist
{ {
PlaylistGroupId = request.PlaylistGroupId, PlaylistGroupId = request.PlaylistGroupId,
Name = name Name = name
@@ -48,8 +48,7 @@ public class CreateSmartCollectionHandler :
private static Task<Validation<BaseError, SmartCollection>> Validate( private static Task<Validation<BaseError, SmartCollection>> Validate(
TvContext dbContext, TvContext dbContext,
CreateSmartCollection request) => CreateSmartCollection request) =>
ValidateName(dbContext, request).MapT( ValidateName(dbContext, request).MapT(name => new SmartCollection
name => new SmartCollection
{ {
Name = name, Name = name,
Query = request.Query Query = request.Query
@@ -14,9 +14,9 @@ namespace ErsatzTV.Application.MediaCollections;
public class RemoveItemsFromCollectionHandler : IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>> public class RemoveItemsFromCollectionHandler : IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository; private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
public RemoveItemsFromCollectionHandler( public RemoveItemsFromCollectionHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
@@ -50,8 +50,7 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
.Filter(i => i.CollectionId.HasValue) .Filter(i => i.CollectionId.HasValue)
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
.Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value)) .Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value))
.Map( .Map(i => new MultiCollectionItem
i => new MultiCollectionItem
{ {
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
CollectionId = i.CollectionId.Value, CollectionId = i.CollectionId.Value,
@@ -70,8 +69,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
// update existing items // update existing items
foreach (MultiCollectionItem item in c.MultiCollectionItems) foreach (MultiCollectionItem item in c.MultiCollectionItems)
{ {
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( foreach (UpdateMultiCollectionItem incoming in
i => i.CollectionId == item.CollectionId)) request.Items.Filter(i => i.CollectionId == item.CollectionId))
{ {
item.ScheduleAsGroup = incoming.ScheduleAsGroup; item.ScheduleAsGroup = incoming.ScheduleAsGroup;
item.PlaybackOrder = incoming.PlaybackOrder; item.PlaybackOrder = incoming.PlaybackOrder;
@@ -85,8 +84,7 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
.Filter(i => i.SmartCollectionId.HasValue) .Filter(i => i.SmartCollectionId.HasValue)
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
.Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value)) .Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value))
.Map( .Map(i => new MultiCollectionSmartItem
i => new MultiCollectionSmartItem
{ {
// ReSharper disable once PossibleInvalidOperationException // ReSharper disable once PossibleInvalidOperationException
SmartCollectionId = i.SmartCollectionId.Value, SmartCollectionId = i.SmartCollectionId.Value,
@@ -105,8 +103,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
// update existing items // update existing items
foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems) foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems)
{ {
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter( foreach (UpdateMultiCollectionItem incoming in request.Items.Filter(i =>
i => i.SmartCollectionId == item.SmartCollectionId)) i.SmartCollectionId == item.SmartCollectionId))
{ {
item.ScheduleAsGroup = incoming.ScheduleAsGroup; item.ScheduleAsGroup = incoming.ScheduleAsGroup;
item.PlaybackOrder = incoming.PlaybackOrder; item.PlaybackOrder = incoming.PlaybackOrder;
@@ -1,6 +1,4 @@
using System.Globalization; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata;
namespace ErsatzTV.Application.MediaItems; namespace ErsatzTV.Application.MediaItems;
+4 -3
View File
@@ -46,9 +46,10 @@ internal static class Mapper
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languageCodes return languageCodes
.Map( .Map(lang => allCultures.Filter(ci => string.Equals(
lang => allCultures.Filter( ci.ThreeLetterISOLanguageName,
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) lang,
StringComparison.OrdinalIgnoreCase)))
.Flatten() .Flatten()
.Map(ci => ci.EnglishName) .Map(ci => ci.EnglishName)
.Distinct() .Distinct()
@@ -19,7 +19,6 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{ {
private readonly IBlockPlayoutBuilder _blockPlayoutBuilder; private readonly IBlockPlayoutBuilder _blockPlayoutBuilder;
private readonly IBlockPlayoutFillerBuilder _blockPlayoutFillerBuilder; private readonly IBlockPlayoutFillerBuilder _blockPlayoutFillerBuilder;
private readonly IYamlPlayoutBuilder _yamlPlayoutBuilder;
private readonly IClient _client; private readonly IClient _client;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEntityLocker _entityLocker; private readonly IEntityLocker _entityLocker;
@@ -27,6 +26,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IFFmpegSegmenterService _ffmpegSegmenterService; private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder; private readonly IPlayoutBuilder _playoutBuilder;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IYamlPlayoutBuilder _yamlPlayoutBuilder;
public BuildPlayoutHandler( public BuildPlayoutHandler(
IClient client, IClient client,
@@ -37,8 +37,7 @@ public class CreateBlockPlayoutHandler(
TvContext dbContext, TvContext dbContext,
CreateBlockPlayout request) => CreateBlockPlayout request) =>
(await ValidateChannel(dbContext, request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidatePlayoutType(request))
.Apply( .Apply((channel, playoutType) => new Playout
(channel, playoutType) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ProgramSchedulePlayoutType = playoutType, ProgramSchedulePlayoutType = playoutType,
@@ -50,8 +50,7 @@ public class CreateExternalJsonPlayoutHandler
TvContext dbContext, TvContext dbContext,
CreateExternalJsonPlayout request) => CreateExternalJsonPlayout request) =>
(await ValidateChannel(dbContext, request), ValidateExternalJsonFile(request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidateExternalJsonFile(request), ValidatePlayoutType(request))
.Apply( .Apply((channel, externalJsonFile, playoutType) => new Playout
(channel, externalJsonFile, playoutType) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ExternalJsonFile = externalJsonFile, ExternalJsonFile = externalJsonFile,
@@ -41,6 +41,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
{ {
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Channel.Number, DateTimeOffset.Now, false)); await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Channel.Number, DateTimeOffset.Now, false));
} }
await _channel.WriteAsync(new RefreshChannelList()); await _channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id); return new CreatePlayoutResponse(playout.Id);
} }
@@ -50,8 +51,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
CreateFloodPlayout request) => CreateFloodPlayout request) =>
(await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request), (await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request),
ValidatePlayoutType(request)) ValidatePlayoutType(request))
.Apply( .Apply((channel, programSchedule, playoutType) => new Playout
(channel, programSchedule, playoutType) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
@@ -50,8 +50,7 @@ public class CreateYamlPlayoutHandler
TvContext dbContext, TvContext dbContext,
CreateYamlPlayout request) => CreateYamlPlayout request) =>
(await ValidateChannel(dbContext, request), ValidateYamlFile(request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidateYamlFile(request), ValidatePlayoutType(request))
.Apply( .Apply((channel, externalJsonFile, playoutType) => new Playout
(channel, externalJsonFile, playoutType) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
TemplateFile = externalJsonFile, TemplateFile = externalJsonFile,
@@ -26,8 +26,11 @@ public class ResetAllPlayoutsHandler(
case ProgramSchedulePlayoutType.Yaml: case ProgramSchedulePlayoutType.Yaml:
if (!locker.IsPlayoutLocked(playout.Id)) if (!locker.IsPlayoutLocked(playout.Id))
{ {
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken); await channel.WriteAsync(
new BuildPlayout(playout.Id, PlayoutBuildMode.Reset),
cancellationToken);
} }
break; break;
case ProgramSchedulePlayoutType.ExternalJson: case ProgramSchedulePlayoutType.ExternalJson:
case ProgramSchedulePlayoutType.None: case ProgramSchedulePlayoutType.None:
+4 -8
View File
@@ -1,5 +1,4 @@
using System.Globalization; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
@@ -51,16 +50,14 @@ internal static class Mapper
.Map(am => $"{am.Title} - ").IfNone(string.Empty); .Map(am => $"{am.Title} - ").IfNone(string.Empty);
return mv.MusicVideoMetadata.HeadOrNone() return mv.MusicVideoMetadata.HeadOrNone()
.Map(mvm => $"{artistName}{mvm.Title}") .Map(mvm => $"{artistName}{mvm.Title}")
.Map( .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? s ? s
: $"{s} ({playoutItem.ChapterTitle})") : $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown music video]"); .IfNone("[unknown music video]");
case OtherVideo ov: case OtherVideo ov:
return ov.OtherVideoMetadata.HeadOrNone() return ov.OtherVideoMetadata.HeadOrNone()
.Map(ovm => ovm.Title ?? string.Empty) .Map(ovm => ovm.Title ?? string.Empty)
.Map( .Map(s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
s => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? s ? s
: $"{s} ({playoutItem.ChapterTitle})") : $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown video]"); .IfNone("[unknown video]");
@@ -70,8 +67,7 @@ internal static class Mapper
.IfNone(string.Empty); .IfNone(string.Empty);
return s.SongMetadata.HeadOrNone() return s.SongMetadata.HeadOrNone()
.Map(sm => $"{songArtist}{sm.Title ?? string.Empty}") .Map(sm => $"{songArtist}{sm.Title ?? string.Empty}")
.Map( .Map(t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
t => string.IsNullOrWhiteSpace(playoutItem.ChapterTitle)
? t ? t
: $"{s} ({playoutItem.ChapterTitle})") : $"{s} ({playoutItem.ChapterTitle})")
.IfNone("[unknown song]"); .IfNone("[unknown song]");
@@ -19,8 +19,7 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
.AsNoTracking() .AsNoTracking()
.Include(p => p.ProgramSchedule) .Include(p => p.ProgramSchedule)
.Filter(p => p.Channel != null) .Filter(p => p.Channel != null)
.Map( .Map(p => new PlayoutNameViewModel(
p => new PlayoutNameViewModel(
p.Id, p.Id,
p.ProgramSchedulePlayoutType, p.ProgramSchedulePlayoutType,
p.Channel.Name, p.Channel.Name,
@@ -17,8 +17,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory
.Include(p => p.ProgramSchedule) .Include(p => p.ProgramSchedule)
.Include(p => p.Channel) .Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId) .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT( .MapT(p => new PlayoutNameViewModel(
p => new PlayoutNameViewModel(
p.Id, p.Id,
p.ProgramSchedulePlayoutType, p.ProgramSchedulePlayoutType,
p.Channel.Name, p.Channel.Name,
@@ -20,8 +20,7 @@ public class StartPlexPinFlowHandler : IRequestHandler<StartPlexPinFlow, Either<
public Task<Either<BaseError, string>> Handle( public Task<Either<BaseError, string>> Handle(
StartPlexPinFlow request, StartPlexPinFlow request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_plexTvApiClient.StartPinFlow().Bind( _plexTvApiClient.StartPinFlow().Bind(result => result.Match(
result => result.Match(
Left: error => Task.FromResult(Left<BaseError, string>(error)), Left: error => Task.FromResult(Left<BaseError, string>(error)),
Right: async pin => Right: async pin =>
{ {
@@ -10,7 +10,8 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Plex; namespace ErsatzTV.Application.Plex;
public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler, IRequestHandler<SynchronizePlexMediaSources, public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler,
IRequestHandler<SynchronizePlexMediaSources,
Either<BaseError, List<PlexMediaSource>>> Either<BaseError, List<PlexMediaSource>>>
{ {
private const string LocalhostUri = "http://localhost:32400"; private const string LocalhostUri = "http://localhost:32400";
@@ -56,8 +57,8 @@ public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler, IRe
} }
// delete removed servers // delete removed servers
foreach (PlexMediaSource removed in allExisting.Filter( foreach (PlexMediaSource removed in allExisting.Filter(s =>
s => servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier))) servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier)))
{ {
_logger.LogWarning( _logger.LogWarning(
"Deleting removed Plex server {ServerName}!", "Deleting removed Plex server {ServerName}!",
@@ -1,4 +1,5 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Plex; using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
@@ -12,7 +13,9 @@ public abstract class PlexBaseConnectionHandler(
IMediaSourceRepository mediaSourceRepository, IMediaSourceRepository mediaSourceRepository,
ILogger logger) ILogger logger)
{ {
protected async Task<Option<PlexConnection>> FindConnectionToActivate(PlexMediaSource server, PlexServerAuthToken token) protected async Task<Option<PlexConnection>> FindConnectionToActivate(
PlexMediaSource server,
PlexServerAuthToken token)
{ {
Option<PlexConnection> result = Option<PlexConnection>.None; Option<PlexConnection> result = Option<PlexConnection>.None;
@@ -43,7 +46,8 @@ public abstract class PlexBaseConnectionHandler(
tasks.Remove(completed); tasks.Remove(completed);
} }
Option<PlexConnection> maybeBest = successfulTimes.OrderByDescending(kv => kv.Value).Select(kvp => kvp.Key).HeadOrNone(); Option<PlexConnection> maybeBest =
successfulTimes.OrderByDescending(kv => kv.Value).Select(kvp => kvp.Key).HeadOrNone();
foreach (PlexConnection connection in maybeBest) foreach (PlexConnection connection in maybeBest)
{ {
connection.IsActive = true; connection.IsActive = true;
@@ -60,12 +64,16 @@ public abstract class PlexBaseConnectionHandler(
return result; return result;
} }
private async Task PingPlexConnection(PlexConnection connection, PlexServerAuthToken token, ConcurrentDictionary<PlexConnection, TimeSpan> successfulTimes, CancellationToken cancellationToken) private async Task PingPlexConnection(
PlexConnection connection,
PlexServerAuthToken token,
ConcurrentDictionary<PlexConnection, TimeSpan> successfulTimes,
CancellationToken cancellationToken)
{ {
try try
{ {
logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri); logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
var sw = new System.Diagnostics.Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
bool pingResult = await plexServerApiClient.Ping(connection, token, cancellationToken); bool pingResult = await plexServerApiClient.Ping(connection, token, cancellationToken);
sw.Stop(); sw.Stop();
@@ -8,14 +8,15 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Plex; namespace ErsatzTV.Application.Plex;
public class GetPlexConnectionParametersHandler : PlexBaseConnectionHandler, IRequestHandler<GetPlexConnectionParameters, public class GetPlexConnectionParametersHandler : PlexBaseConnectionHandler,
IRequestHandler<GetPlexConnectionParameters,
Either<BaseError, PlexConnectionParametersViewModel>> Either<BaseError, PlexConnectionParametersViewModel>>
{ {
private readonly ILogger<GetPlexConnectionParametersHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache; private readonly IMemoryCache _memoryCache;
private readonly IPlexServerApiClient _plexServerApiClient;
private readonly IPlexSecretStore _plexSecretStore; private readonly IPlexSecretStore _plexSecretStore;
private readonly ILogger<GetPlexConnectionParametersHandler> _logger; private readonly IPlexServerApiClient _plexServerApiClient;
public GetPlexConnectionParametersHandler( public GetPlexConnectionParametersHandler(
IMemoryCache memoryCache, IMemoryCache memoryCache,
@@ -49,7 +50,8 @@ public class GetPlexConnectionParametersHandler : PlexBaseConnectionHandler, IRe
foreach (PlexServerAuthToken token in maybeToken) foreach (PlexServerAuthToken token in maybeToken)
{ {
// try to keep the same connection // try to keep the same connection
Option<PlexConnection> maybeActiveConnection = mediaSource.Connections.Filter(c => c.IsActive).HeadOrNone(); Option<PlexConnection> maybeActiveConnection =
mediaSource.Connections.Filter(c => c.IsActive).HeadOrNone();
foreach (PlexConnection activeConnection in maybeActiveConnection) foreach (PlexConnection activeConnection in maybeActiveConnection)
{ {
if (await _plexServerApiClient.Ping(activeConnection, token, cancellationToken)) if (await _plexServerApiClient.Ping(activeConnection, token, cancellationToken))
@@ -30,8 +30,7 @@ public class CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContext
private static Task<Validation<BaseError, ProgramSchedule>> Validate( private static Task<Validation<BaseError, ProgramSchedule>> Validate(
TvContext dbContext, TvContext dbContext,
CreateProgramSchedule request) => CreateProgramSchedule request) =>
ValidateName(dbContext, request).MapT( ValidateName(dbContext, request).MapT(name =>
name =>
{ {
bool keepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; bool keepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether;
return new ProgramSchedule return new ProgramSchedule
@@ -12,8 +12,7 @@ public class GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContex
{ {
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ProgramSchedules return await dbContext.ProgramSchedules
.Map( .Map(ps => new ProgramScheduleViewModel(
ps => new ProgramScheduleViewModel(
ps.Id, ps.Id,
ps.Name, ps.Name,
ps.KeepMultiPartEpisodesTogether, ps.KeepMultiPartEpisodesTogether,
@@ -52,8 +52,7 @@ public class GetProgramScheduleItemsHandler :
.Include(i => i.FallbackFiller) .Include(i => i.FallbackFiller)
.Include(i => i.Watermark) .Include(i => i.Watermark)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map( .Map(programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
programScheduleItems => programScheduleItems.Map(ProjectToViewModel)
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList()); .Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());
} }
@@ -42,8 +42,7 @@ public class CreateCustomResolutionHandler : IRequestHandler<CreateCustomResolut
TvContext dbContext, TvContext dbContext,
CreateCustomResolution request) => CreateCustomResolution request) =>
ResolutionMustBeUnique(dbContext, request) ResolutionMustBeUnique(dbContext, request)
.MapT( .MapT(_ => new Resolution
_ => new Resolution
{ {
Name = $"{request.Width}x{request.Height}", Name = $"{request.Width}x{request.Height}",
Width = request.Width, Width = request.Width,
@@ -25,8 +25,7 @@ public class CreateBlockHandler(IDbContextFactory<TvContext> dbContextFactory)
} }
private static async Task<Validation<BaseError, Block>> Validate(TvContext dbContext, CreateBlock request) => private static async Task<Validation<BaseError, Block>> Validate(TvContext dbContext, CreateBlock request) =>
await ValidateBlockName(dbContext, request).MapT( await ValidateBlockName(dbContext, request).MapT(name => new Block
name => new Block
{ {
BlockGroupId = request.BlockGroupId, BlockGroupId = request.BlockGroupId,
Name = name, Name = name,
@@ -25,8 +25,7 @@ public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
} }
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, CreateDeco request) => private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, CreateDeco request) =>
await ValidateDecoName(dbContext, request).MapT( await ValidateDecoName(dbContext, request).MapT(name => new Deco
name => new Deco
{ {
DecoGroupId = request.DecoGroupId, DecoGroupId = request.DecoGroupId,
Name = name Name = name
@@ -26,8 +26,7 @@ public class CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFac
private static Task<Validation<BaseError, DecoTemplate>> Validate(CreateDecoTemplate request) => private static Task<Validation<BaseError, DecoTemplate>> Validate(CreateDecoTemplate request) =>
Task.FromResult( Task.FromResult(
ValidateName(request).Map( ValidateName(request).Map(name => new DecoTemplate
name => new DecoTemplate
{ {
DecoTemplateGroupId = request.DecoTemplateGroupId, DecoTemplateGroupId = request.DecoTemplateGroupId,
Name = name Name = name
@@ -26,8 +26,7 @@ public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory
private static Task<Validation<BaseError, Template>> Validate(CreateTemplate request) => private static Task<Validation<BaseError, Template>> Validate(CreateTemplate request) =>
Task.FromResult( Task.FromResult(
ValidateName(request).Map( ValidateName(request).Map(name => new Template
name => new Template
{ {
TemplateGroupId = request.TemplateGroupId, TemplateGroupId = request.TemplateGroupId,
Name = name Name = name
@@ -13,8 +13,7 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Filter( .Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml) p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
@@ -16,8 +16,7 @@ public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFact
Option<Playout> maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Items) .Include(p => p.Items)
.Include(p => p.PlayoutHistory) .Include(p => p.PlayoutHistory)
.Filter( .Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml) p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
@@ -71,8 +71,7 @@ public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF
.ToListAsync() .ToListAsync()
.Map(list => list.ToDictionary(b => b.Id, b => b)); .Map(list => list.ToDictionary(b => b.Id, b => b));
var allTemplateItems = request.Items.Map( var allTemplateItems = request.Items.Map(i =>
i =>
{ {
Block block = allBlocks[i.BlockId]; Block block = allBlocks[i.BlockId];
return new BlockTemplateItem( return new BlockTemplateItem(
@@ -88,8 +88,8 @@ public class UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
} }
Option<Deco> maybeExisting = await dbContext.Decos Option<Deco> maybeExisting = await dbContext.Decos
.FirstOrDefaultAsync( .FirstOrDefaultAsync(d =>
d => d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name) d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name)
.Map(Optional); .Map(Optional);
return maybeExisting.IsSome return maybeExisting.IsSome
@@ -18,8 +18,7 @@ public class GetDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbContextF
.Filter(i => i.DecoTemplateId == request.DecoTemplateId) .Filter(i => i.DecoTemplateId == request.DecoTemplateId)
.Include(i => i.Deco) .Include(i => i.Deco)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map( .Map(items => items
items => items
.Map(Mapper.ProjectToViewModel) .Map(Mapper.ProjectToViewModel)
.Filter(i => i.StartTime < i.EndTime || i.EndTime.TimeOfDay == TimeSpan.Zero) .Filter(i => i.StartTime < i.EndTime || i.EndTime.TimeOfDay == TimeSpan.Zero)
.ToList()); .ToList());
@@ -16,8 +16,7 @@ public class GetTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFacto
.Filter(i => i.TemplateId == request.TemplateId) .Filter(i => i.TemplateId == request.TemplateId)
.Include(i => i.Block) .Include(i => i.Block)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.Map( .Map(items => items
items => items
.Map(Mapper.ProjectToViewModel) .Map(Mapper.ProjectToViewModel)
.Filter(i => i.StartTime < i.EndTime || i.EndTime.TimeOfDay == TimeSpan.Zero) .Filter(i => i.StartTime < i.EndTime || i.EndTime.TimeOfDay == TimeSpan.Zero)
.ToList()); .ToList());
@@ -30,5 +30,6 @@ public class QuerySearchIndexAllItemsHandler : IRequestHandler<QuerySearchIndexA
await GetIds(LuceneSearchIndex.ImageType, request.Query)); await GetIds(LuceneSearchIndex.ImageType, request.Query));
private async Task<List<int>> GetIds(string type, string query) => private async Task<List<int>> GetIds(string type, string query) =>
(await _searchIndex.Search(_client, $"type:{type} AND ({query})", string.Empty, 0, 0)).Items.Map(i => i.Id).ToList(); (await _searchIndex.Search(_client, $"type:{type} AND ({query})", string.Empty, 0, 0)).Items.Map(i => i.Id)
.ToList();
} }
@@ -42,8 +42,7 @@ public class QuerySearchTargetsHandler : IRequestHandler<QuerySearchTargets, Lis
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
result.AddRange( result.AddRange(
schedules.SelectMany( schedules.SelectMany(s => new[]
s => new[]
{ {
new SearchTargetViewModel(s.Id, s.Name, SearchTargetKind.Schedule), new SearchTargetViewModel(s.Id, s.Name, SearchTargetKind.Schedule),
new SearchTargetViewModel(s.Id, s.Name, SearchTargetKind.ScheduleItems) new SearchTargetViewModel(s.Id, s.Name, SearchTargetKind.ScheduleItems)
@@ -17,8 +17,7 @@ public class SearchArtistsHandler : IRequestHandler<SearchArtists, List<NamedMed
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ArtistMetadata return await dbContext.ArtistMetadata
.AsNoTracking() .AsNoTracking()
.Where( .Where(a => EF.Functions.Like(
a => EF.Functions.Like(
EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(a => EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation)) .OrderBy(a => EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation))
@@ -19,8 +19,7 @@ public class SearchCollectionsHandler : IRequestHandler<SearchCollections, List<
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Collections return await dbContext.Collections
.AsNoTracking() .AsNoTracking()
.Where( .Where(c => EF.Functions.Like(
c => EF.Functions.Like(
EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation))
@@ -14,8 +14,7 @@ public class SearchMoviesHandler(IDbContextFactory<TvContext> dbContextFactory)
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MovieMetadata return await dbContext.MovieMetadata
.AsNoTracking() .AsNoTracking()
.Where( .Where(s => EF.Functions.Like(
s => EF.Functions.Like(
EF.Functions.Collate(s.Title + " " + s.Year, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(s.Title + " " + s.Year, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(a => EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation)) .OrderBy(a => EF.Functions.Collate(a.Title, TvContext.CaseInsensitiveCollation))
@@ -19,8 +19,7 @@ public class SearchMultiCollectionsHandler : IRequestHandler<SearchMultiCollecti
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.MultiCollections return await dbContext.MultiCollections
.AsNoTracking() .AsNoTracking()
.Where( .Where(c => EF.Functions.Like(
c => EF.Functions.Like(
EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation))
@@ -19,8 +19,7 @@ public class SearchSmartCollectionsHandler : IRequestHandler<SearchSmartCollecti
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.SmartCollections return await dbContext.SmartCollections
.AsNoTracking() .AsNoTracking()
.Where( .Where(c => EF.Functions.Like(
c => EF.Functions.Like(
EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation)) .OrderBy(c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation))
@@ -20,8 +20,7 @@ public class SearchTelevisionShowsHandler : IRequestHandler<SearchTelevisionShow
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ShowMetadata return await dbContext.ShowMetadata
.AsNoTracking() .AsNoTracking()
.Where( .Where(s => EF.Functions.Like(
s => EF.Functions.Like(
EF.Functions.Collate(s.Title + " " + s.Year, TvContext.CaseInsensitiveCollation), EF.Functions.Collate(s.Title + " " + s.Year, TvContext.CaseInsensitiveCollation),
$"%{request.Query}%")) $"%{request.Query}%"))
.OrderBy(s => EF.Functions.Collate(s.Title, TvContext.CaseInsensitiveCollation)) .OrderBy(s => EF.Functions.Collate(s.Title, TvContext.CaseInsensitiveCollation))
@@ -174,7 +174,9 @@ public class HlsSessionWorker : IHlsSessionWorker
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
// time shift on-demand playout if needed // time shift on-demand playout if needed
await _mediator.Send(new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), cancellationToken); await _mediator.Send(
new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true),
cancellationToken);
bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit(); bool initialWorkAhead = Volatile.Read(ref _workAheadCount) < await GetWorkAheadLimit();
_state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime; _state = initialWorkAhead ? HlsSessionState.SeekAndWorkAhead : HlsSessionState.SeekAndRealtime;
@@ -583,8 +585,7 @@ public class HlsSessionWorker : IHlsSessionWorker
Directory.GetFiles( Directory.GetFiles(
Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber), Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber),
"live*.mp4")) "live*.mp4"))
.Map( .Map(file =>
file =>
{ {
string fileName = Path.GetFileName(file); string fileName = Path.GetFileName(file);
var sequenceNumber = int.Parse( var sequenceNumber = int.Parse(
@@ -126,7 +126,9 @@ public class HlsSessionWorkerV2 : IHlsSessionWorker
PlaylistStart = _transcodedUntil; PlaylistStart = _transcodedUntil;
// time shift on-demand playout if needed // time shift on-demand playout if needed
await _mediator.Send(new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true), cancellationToken); await _mediator.Send(
new TimeShiftOnDemandPlayout(_channelNumber, _transcodedUntil, true),
cancellationToken);
// start concat/segmenter process // start concat/segmenter process
// other transcode processes will be started by incoming requests from concat/segmenter process // other transcode processes will be started by incoming requests from concat/segmenter process
@@ -45,8 +45,7 @@ public abstract class FFmpegProcessHandler<T> : IRequestHandler<T, Either<BaseEr
.Include(c => c.Artwork) .Include(c => c.Artwork)
.Include(c => c.Watermark) .Include(c => c.Watermark)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber) .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber)
.MapT( .MapT(channel =>
channel =>
{ {
channel.StreamingMode = request.Mode.ToLowerInvariant() switch channel.StreamingMode = request.Mode.ToLowerInvariant() switch
{ {
@@ -227,8 +227,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
Option<ChannelWatermark> maybeGlobalWatermark = await dbContext.ConfigElements Option<ChannelWatermark> maybeGlobalWatermark = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId) .GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId)
.BindT( .BindT(watermarkId => dbContext.ChannelWatermarks
watermarkId => dbContext.ChannelWatermarks
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId)); .SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
Option<ChannelWatermark> playoutItemWatermark = Optional(playoutItemWithPath.PlayoutItem.Watermark); Option<ChannelWatermark> playoutItemWatermark = Optional(playoutItemWithPath.PlayoutItem.Watermark);
@@ -261,7 +260,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
// override watermark as song_progress_overlay.png // override watermark as song_progress_overlay.png
if (videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true }) if (videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true })
{ {
double ratio = channel.FFmpegProfile.Resolution.Width / (double)channel.FFmpegProfile.Resolution.Height; double ratio = channel.FFmpegProfile.Resolution.Width /
(double)channel.FFmpegProfile.Resolution.Height;
bool is43 = Math.Abs(ratio - 4.0 / 3.0) < 0.01; bool is43 = Math.Abs(ratio - 4.0 / 3.0) < 0.01;
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png"; string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
@@ -98,10 +98,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
// only check the requested playout if subtitles are enabled // only check the requested playout if subtitles are enabled
Option<Playout> requestedPlayout = await dbContext.Playouts Option<Playout> requestedPlayout = await dbContext.Playouts
.AsNoTracking() .AsNoTracking()
.Filter( .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None || p.ProgramSchedule.Items.Any(psi =>
p.ProgramSchedule.Items.Any( psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1)); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId.IfNone(-1));
playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id)); playoutIdsToCheck.AddRange(requestedPlayout.Map(p => p.Id));
@@ -111,10 +110,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
{ {
playoutIdsToCheck = dbContext.Playouts playoutIdsToCheck = dbContext.Playouts
.AsNoTracking() .AsNoTracking()
.Filter( .Filter(p => p.Channel.SubtitleMode != ChannelSubtitleMode.None ||
p => p.Channel.SubtitleMode != ChannelSubtitleMode.None || p.ProgramSchedule.Items.Any(psi =>
p.ProgramSchedule.Items.Any( psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
psi => psi.SubtitleMode != null && psi.SubtitleMode != ChannelSubtitleMode.None))
.Map(p => p.Id) .Map(p => p.Id)
.ToList(); .ToList();
} }
@@ -216,10 +214,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
List<int> episodeIds = await dbContext.EpisodeMetadata List<int> episodeIds = await dbContext.EpisodeMetadata
.AsNoTracking() .AsNoTracking()
.Filter(em => mediaItemIds.Contains(em.EpisodeId)) .Filter(em => mediaItemIds.Contains(em.EpisodeId))
.Filter( .Filter(em => em.Subtitles.Any(s => s.SubtitleKind == SubtitleKind.Embedded &&
em => em.Subtitles.Any( s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" &&
s => s.SubtitleKind == SubtitleKind.Embedded && s.Codec != "dvdsub" &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(em => em.EpisodeId) .Map(em => em.EpisodeId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -228,10 +225,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
List<int> movieIds = await dbContext.MovieMetadata List<int> movieIds = await dbContext.MovieMetadata
.AsNoTracking() .AsNoTracking()
.Filter(mm => mediaItemIds.Contains(mm.MovieId)) .Filter(mm => mediaItemIds.Contains(mm.MovieId))
.Filter( .Filter(mm => mm.Subtitles.Any(s => s.SubtitleKind == SubtitleKind.Embedded &&
mm => mm.Subtitles.Any( s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" &&
s => s.SubtitleKind == SubtitleKind.Embedded && s.Codec != "dvdsub" &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MovieId) .Map(mm => mm.MovieId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -240,10 +236,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
List<int> musicVideoIds = await dbContext.MusicVideoMetadata List<int> musicVideoIds = await dbContext.MusicVideoMetadata
.AsNoTracking() .AsNoTracking()
.Filter(mm => mediaItemIds.Contains(mm.MusicVideoId)) .Filter(mm => mediaItemIds.Contains(mm.MusicVideoId))
.Filter( .Filter(mm => mm.Subtitles.Any(s => s.SubtitleKind == SubtitleKind.Embedded &&
mm => mm.Subtitles.Any( s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" &&
s => s.SubtitleKind == SubtitleKind.Embedded && s.Codec != "dvdsub" &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(mm => mm.MusicVideoId) .Map(mm => mm.MusicVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -252,10 +247,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
List<int> otherVideoIds = await dbContext.OtherVideoMetadata List<int> otherVideoIds = await dbContext.OtherVideoMetadata
.AsNoTracking() .AsNoTracking()
.Filter(ovm => mediaItemIds.Contains(ovm.OtherVideoId)) .Filter(ovm => mediaItemIds.Contains(ovm.OtherVideoId))
.Filter( .Filter(ovm => ovm.Subtitles.Any(s => s.SubtitleKind == SubtitleKind.Embedded &&
ovm => ovm.Subtitles.Any( s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" &&
s => s.SubtitleKind == SubtitleKind.Embedded && s.Codec != "dvdsub" &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")) s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs"))
.Map(ovm => ovm.OtherVideoId) .Map(ovm => ovm.OtherVideoId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -284,11 +278,9 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
// find each subtitle that needs extraction // find each subtitle that needs extraction
IEnumerable<Subtitle> subtitles = allSubtitles IEnumerable<Subtitle> subtitles = allSubtitles
.Filter(s => s.SubtitleKind == SubtitleKind.Embedded) .Filter(s => s.SubtitleKind == SubtitleKind.Embedded)
.Filter( .Filter(s => s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s => s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" &&
s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs") s.Codec != "vobsub" && s.Codec != "pgssub" && s.Codec != "pgs")
.Filter( .Filter(s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path) ||
s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path) ||
FileDoesntExist(mediaItem.Id, s)); FileDoesntExist(mediaItem.Id, s));
// find cache paths for each subtitle // find cache paths for each subtitle
+10 -10
View File
@@ -1,5 +1,4 @@
using System.Globalization; using System.Globalization;
using ErsatzTV.Application.MediaCards;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Jellyfin;
@@ -23,17 +22,17 @@ internal static class Mapper
show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty), show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone([]), show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone([]),
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Where(t => string.IsNullOrWhiteSpace(t.ExternalTypeId)).Map(g => g.Name).ToList()).IfNone([]), show.ShowMetadata.HeadOrNone().Map(m =>
m.Tags.Where(t => string.IsNullOrWhiteSpace(t.ExternalTypeId)).Map(g => g.Name).ToList()).IfNone([]),
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()).IfNone([]), show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()).IfNone([]),
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Where(t => t.ExternalTypeId == Tag.PlexNetworkTypeId).Map(g => g.Name).ToList()).IfNone([]), show.ShowMetadata.HeadOrNone().Map(m =>
m.Tags.Where(t => t.ExternalTypeId == Tag.PlexNetworkTypeId).Map(g => g.Name).ToList()).IfNone([]),
show.ShowMetadata.HeadOrNone() show.ShowMetadata.HeadOrNone()
.Map( .Map(m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone([]), .Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone([]),
LanguagesForShow(languages), LanguagesForShow(languages),
show.ShowMetadata.HeadOrNone() show.ShowMetadata.HeadOrNone()
.Map( .Map(m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby)) .Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
.ToList()) .ToList())
.IfNone([])); .IfNone([]));
@@ -104,9 +103,10 @@ internal static class Mapper
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures); CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages return languages
.Map( .Map(lang => allCultures.Filter(ci => string.Equals(
lang => allCultures.Filter( ci.ThreeLetterISOLanguageName,
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase))) lang,
StringComparison.OrdinalIgnoreCase)))
.Flatten() .Flatten()
.Distinct() .Distinct()
.ToList(); .ToList();
@@ -38,8 +38,7 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
private static Validation<BaseError, ChannelWatermark> Validate(CreateWatermark request) => private static Validation<BaseError, ChannelWatermark> Validate(CreateWatermark request) =>
ValidateName(request) ValidateName(request)
.Map( .Map(_ =>
_ =>
{ {
var watermark = new ChannelWatermark var watermark = new ChannelWatermark
{ {
@@ -1,6 +1,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using Shouldly;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.Domain; namespace ErsatzTV.Core.Tests.Domain;
@@ -3,10 +3,10 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime; using ErsatzTV.FFmpeg.Runtime;
using Shouldly;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.Emby; namespace ErsatzTV.Core.Tests.Emby;
@@ -14,12 +14,6 @@ public class CustomStreamSelectorTests
[TestFixture] [TestFixture]
public class SelectStreams public class SelectStreams
{ {
private static readonly string TestFileName = Path.Combine(FileSystemLayout.ChannelStreamSelectorsFolder, "test.yml");
private Channel _channel;
private MediaItemAudioVersion _audioVersion;
private List<Subtitle> _subtitles;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@@ -41,16 +35,24 @@ public class CustomStreamSelectorTests
]; ];
} }
private static readonly string TestFileName = Path.Combine(
FileSystemLayout.ChannelStreamSelectorsFolder,
"test.yml");
private Channel _channel;
private MediaItemAudioVersion _audioVersion;
private List<Subtitle> _subtitles;
[Test] [Test]
public async Task Should_Select_eng_Audio_Exact_Match() public async Task Should_Select_eng_Audio_Exact_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "eng" - "eng"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -71,11 +73,11 @@ items:
public async Task Should_Select_und_Audio_Missing_Language() public async Task Should_Select_und_Audio_Missing_Language()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: ["und"] - audio_language: ["und"]
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -96,11 +98,11 @@ items:
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Audio_Languages() public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Audio_Languages()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: ["en", "eng"] - audio_language: ["en", "eng"]
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -121,9 +123,9 @@ items:
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Items() public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Items()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "de" - "de"
subtitle_language: subtitle_language:
@@ -131,7 +133,7 @@ items:
- audio_language: - audio_language:
- "eng" - "eng"
disable_subtitles: true disable_subtitles: true
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -152,12 +154,12 @@ items:
public async Task Should_Select_eng_Audio_Pattern_Match() public async Task Should_Select_eng_Audio_Pattern_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -178,12 +180,12 @@ items:
public async Task Should_Select_en_Audio_Pattern_Match() public async Task Should_Select_en_Audio_Pattern_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
"""; """;
_audioVersion = GetTestAudioVersion("en"); _audioVersion = GetTestAudioVersion("en");
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
@@ -205,13 +207,13 @@ items:
public async Task disable_subtitles_Should_Select_No_Subtitles() public async Task disable_subtitles_Should_Select_No_Subtitles()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "eng" - "eng"
disable_subtitles: true disable_subtitles: true
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -226,14 +228,14 @@ items:
public async Task Should_Select_eng_Subtitle_Exact_Match() public async Task Should_Select_eng_Subtitle_Exact_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "ja" - "ja"
subtitle_language: subtitle_language:
- "eng" - "eng"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -254,14 +256,14 @@ items:
public async Task Should_Select_eng_Subtitle_Pattern_Match() public async Task Should_Select_eng_Subtitle_Pattern_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "ja" - "ja"
subtitle_language: subtitle_language:
- "en*" - "en*"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -282,14 +284,14 @@ items:
public async Task Should_Select_en_Subtitle_Pattern_Match() public async Task Should_Select_en_Subtitle_Pattern_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "ja" - "ja"
subtitle_language: subtitle_language:
- "en*" - "en*"
"""; """;
_audioVersion = GetTestAudioVersion("en"); _audioVersion = GetTestAudioVersion("en");
_subtitles = _subtitles =
@@ -316,9 +318,9 @@ items:
public async Task Should_Select_No_Subtitle_Exact_Match_Multiple_Items() public async Task Should_Select_No_Subtitle_Exact_Match_Multiple_Items()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "de" - "de"
subtitle_language: subtitle_language:
@@ -326,7 +328,7 @@ items:
- audio_language: - audio_language:
- "eng" - "eng"
disable_subtitles: true disable_subtitles: true
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -349,9 +351,9 @@ items:
public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Multiple_Items() public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Multiple_Items()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "ja" - "ja"
subtitle_language: subtitle_language:
@@ -359,7 +361,7 @@ items:
- audio_language: - audio_language:
- "eng" - "eng"
disable_subtitles: true disable_subtitles: true
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -388,14 +390,14 @@ items:
public async Task Should_Ignore_Blocked_Audio_Title() public async Task Should_Ignore_Blocked_Audio_Title()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
audio_title_blocklist: audio_title_blocklist:
- "riff" - "riff"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -416,14 +418,14 @@ items:
public async Task Should_Select_Allowed_Audio_Title() public async Task Should_Select_Allowed_Audio_Title()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
audio_title_allowlist: audio_title_allowlist:
- "movie" - "movie"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -444,16 +446,16 @@ items:
public async Task Should_Ignore_Blocked_Subtitle_Title() public async Task Should_Ignore_Blocked_Subtitle_Title()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "*" - "*"
subtitle_language: subtitle_language:
- "en" - "en"
subtitle_title_blocklist: subtitle_title_blocklist:
- "signs" - "signs"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -474,16 +476,16 @@ items:
public async Task Should_Select_Allowed_Subtitle_Title() public async Task Should_Select_Allowed_Subtitle_Title()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "*" - "*"
subtitle_language: subtitle_language:
- "en" - "en"
subtitle_title_allowlist: subtitle_title_allowlist:
- "songs" - "songs"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -504,13 +506,13 @@ items:
public async Task Should_Select_Condition_Forced_Subtitle() public async Task Should_Select_Condition_Forced_Subtitle()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "*" - "*"
subtitle_condition: "forced" subtitle_condition: "forced"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -531,13 +533,13 @@ items:
public async Task Should_Select_Condition_External_Subtitle() public async Task Should_Select_Condition_External_Subtitle()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "*" - "*"
subtitle_condition: "lang like 'en%' and external" subtitle_condition: "lang like 'en%' and external"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -558,13 +560,13 @@ items:
public async Task Should_Select_Condition_Audio_Title() public async Task Should_Select_Condition_Audio_Title()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
audio_condition: "title like '%movie%'" audio_condition: "title like '%movie%'"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -585,13 +587,13 @@ items:
public async Task Should_Select_Condition_Audio_Channels() public async Task Should_Select_Condition_Audio_Channels()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "en*" - "en*"
audio_condition: "channels > 2" audio_condition: "channels > 2"
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -612,12 +614,12 @@ items:
public async Task Should_Select_Prioritized_Audio_Language() public async Task Should_Select_Prioritized_Audio_Language()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: ["en*","ja"] - audio_language: ["en*","ja"]
audio_title_blocklist: ["riff"] audio_title_blocklist: ["riff"]
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -638,13 +640,13 @@ items:
public async Task Should_Select_Prioritized_Subtitle_Language() public async Task Should_Select_Prioritized_Subtitle_Language()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: - audio_language:
- "*" - "*"
subtitle_language: ["jp","en*"] subtitle_language: ["jp","en*"]
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -665,14 +667,14 @@ items:
public async Task Should_Select_No_Streams_When_Languages_Do_Not_Match() public async Task Should_Select_No_Streams_When_Languages_Do_Not_Match()
{ {
const string YAML = const string YAML =
""" """
--- ---
items: items:
- audio_language: ["en"] - audio_language: ["en"]
subtitle_language: ["es*","de*"] subtitle_language: ["es*","de*"]
- audio_language: ["ja"] - audio_language: ["ja"]
subtitle_language: ["es*","de*"] subtitle_language: ["es*","de*"]
"""; """;
var streamSelector = new CustomStreamSelector( var streamSelector = new CustomStreamSelector(
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]), new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
@@ -697,7 +699,7 @@ items:
MediaStreamKind = MediaStreamKind.Audio, MediaStreamKind = MediaStreamKind.Audio,
Channels = 2, Channels = 2,
Language = "ja", Language = "ja",
Title = "Some Title", Title = "Some Title"
}, },
new MediaStream new MediaStream
{ {
@@ -714,14 +716,14 @@ items:
MediaStreamKind = MediaStreamKind.Audio, MediaStreamKind = MediaStreamKind.Audio,
Channels = 6, Channels = 6,
Language = englishLanguage, Language = englishLanguage,
Title = "Movie Title", Title = "Movie Title"
}, },
new MediaStream new MediaStream
{ {
Index = 3, Index = 3,
MediaStreamKind = MediaStreamKind.Audio, MediaStreamKind = MediaStreamKind.Audio,
Channels = 2, Channels = 2,
Title = "Who Knows", Title = "Who Knows"
} }
] ]
}; };
@@ -1,8 +1,8 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using Shouldly;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.FFmpeg; namespace ErsatzTV.Core.Tests.FFmpeg;
@@ -3,10 +3,10 @@ using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Scripting; using ErsatzTV.Infrastructure.Scripting;
using Shouldly;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.FFmpeg; namespace ErsatzTV.Core.Tests.FFmpeg;
@@ -31,7 +31,7 @@ public class FFmpegStreamSelectorTests
MediaStreamKind = MediaStreamKind.Audio, MediaStreamKind = MediaStreamKind.Audio,
Channels = 2, Channels = 2,
Language = "ja", Language = "ja",
Title = "Some Title", Title = "Some Title"
}, },
new MediaStream new MediaStream
{ {
@@ -63,7 +63,12 @@ public class FFmpegStreamSelectorTests
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
Substitute.For<ILogger<FFmpegStreamSelector>>()); Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, "jpn", "Whatever"); Option<MediaStream> selectedStream = await selector.SelectAudioStream(
audioVersion,
StreamingMode.TransportStream,
channel,
"jpn",
"Whatever");
selectedStream.IsSome.ShouldBeTrue(); selectedStream.IsSome.ShouldBeTrue();
foreach (MediaStream stream in selectedStream) foreach (MediaStream stream in selectedStream)
{ {
@@ -86,7 +91,7 @@ public class FFmpegStreamSelectorTests
MediaStreamKind = MediaStreamKind.Audio, MediaStreamKind = MediaStreamKind.Audio,
Channels = 2, Channels = 2,
Language = "ja", Language = "ja",
Title = "Some Title", Title = "Some Title"
}, },
new MediaStream new MediaStream
{ {
@@ -118,7 +123,12 @@ public class FFmpegStreamSelectorTests
Substitute.For<ILocalFileSystem>(), Substitute.For<ILocalFileSystem>(),
Substitute.For<ILogger<FFmpegStreamSelector>>()); Substitute.For<ILogger<FFmpegStreamSelector>>());
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, null, channel.PreferredAudioTitle); Option<MediaStream> selectedStream = await selector.SelectAudioStream(
audioVersion,
StreamingMode.TransportStream,
channel,
null,
channel.PreferredAudioTitle);
selectedStream.IsSome.ShouldBeTrue(); selectedStream.IsSome.ShouldBeTrue();
foreach (MediaStream stream in selectedStream) foreach (MediaStream stream in selectedStream)
{ {
@@ -143,8 +153,8 @@ public class FFmpegStreamSelectorTests
{ {
StreamIndex = 1, StreamIndex = 1,
SubtitleKind = SubtitleKind.Sidecar, SubtitleKind = SubtitleKind.Sidecar,
Language = "he", Language = "he"
}, }
}; };
var channel = new Channel(Guid.NewGuid()); var channel = new Channel(Guid.NewGuid());
@@ -1,9 +1,9 @@
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using Shouldly;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.FFmpeg; namespace ErsatzTV.Core.Tests.FFmpeg;
@@ -1,6 +1,6 @@
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using Shouldly;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.FFmpeg; namespace ErsatzTV.Core.Tests.FFmpeg;
@@ -28,7 +28,10 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
public Task<List<MediaItem>> GetMultiCollectionItemsByName(string name) => throw new NotSupportedException(); public Task<List<MediaItem>> GetMultiCollectionItemsByName(string name) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask(); public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
public Task<List<MediaItem>> GetSmartCollectionItemsByName(string name) => throw new NotSupportedException(); public Task<List<MediaItem>> GetSmartCollectionItemsByName(string name) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(string query, string smartCollectionName) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(string query, string smartCollectionName) =>
throw new NotSupportedException();
public Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids) => throw new NotSupportedException(); public Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids) => throw new NotSupportedException();
public Task<List<MediaItem>> GetPlaylistItems(int id) => throw new NotSupportedException(); public Task<List<MediaItem>> GetPlaylistItems(int id) => throw new NotSupportedException();
public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException(); public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException();
@@ -66,7 +66,7 @@ public class FakeTelevisionRepository : ITelevisionRepository
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException(); public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
public Task<bool> AddGenre(EpisodeMetadata metadata, Genre genre) => throw new NotSupportedException(); public Task<bool> AddGenre(EpisodeMetadata metadata, Genre genre) => throw new NotSupportedException();
public Task<bool> AddTag(ErsatzTV.Core.Domain.Metadata metadata, Tag tag) => throw new NotSupportedException(); public Task<bool> AddTag(Core.Domain.Metadata metadata, Tag tag) => throw new NotSupportedException();
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException(); public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();
public Task<bool> AddActor(ShowMetadata metadata, Actor actor) => throw new NotSupportedException(); public Task<bool> AddActor(ShowMetadata metadata, Actor actor) => throw new NotSupportedException();
@@ -1,6 +1,6 @@
using ErsatzTV.Core.Iptv; using ErsatzTV.Core.Iptv;
using Shouldly;
using NUnit.Framework; using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Core.Tests.Iptv; namespace ErsatzTV.Core.Tests.Iptv;

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