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

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