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,24 +6,25 @@ 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;
public async Task<Either<BaseError, Artwork>> Handle( public async Task<Either<BaseError, Artwork>> Handle(
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
.AsNoTracking() .AsNoTracking()
.SelectOneAsync(a => a.Id, a => a.Id == request.Id) .SelectOneAsync(a => a.Id, a => a.Id == request.Id)
.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,68 +40,69 @@ 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, watermarkId,
watermarkId, fillerPresetId) =>
fillerPresetId) => {
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
{ {
var artwork = new List<Artwork>(); string logo = request.Logo.Path;
if (!string.IsNullOrWhiteSpace(request.Logo?.Path)) if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{ {
string logo = request.Logo.Path; logo = logo.Replace("iptv/logos/", string.Empty);
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal)) }
artwork.Add(
new Artwork
{ {
logo = logo.Replace("iptv/logos/", string.Empty); Path = logo,
} ArtworkKind = ArtworkKind.Logo,
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType)
? request.Logo.ContentType
: null,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
artwork.Add( var channel = new Channel(Guid.NewGuid())
new Artwork {
{ Name = name,
Path = logo, Number = number,
ArtworkKind = ArtworkKind.Logo, Group = request.Group,
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null, Categories = request.Categories,
DateAdded = DateTime.UtcNow, FFmpegProfileId = ffmpegProfileId,
DateUpdated = DateTime.UtcNow ProgressMode = request.ProgressMode,
}); StreamingMode = request.StreamingMode,
} Artwork = artwork,
StreamSelectorMode = request.StreamSelectorMode,
StreamSelector = request.StreamSelector,
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
PreferredAudioTitle = request.PreferredAudioTitle,
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
SongVideoMode = request.SongVideoMode,
ActiveMode = request.ActiveMode
};
var channel = new Channel(Guid.NewGuid()) foreach (int id in watermarkId)
{ {
Name = name, channel.WatermarkId = id;
Number = number, }
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
ProgressMode = request.ProgressMode,
StreamingMode = request.StreamingMode,
Artwork = artwork,
StreamSelectorMode = request.StreamSelectorMode,
StreamSelector = request.StreamSelector,
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
PreferredAudioTitle = request.PreferredAudioTitle,
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
SongVideoMode = request.SongVideoMode,
ActiveMode = request.ActiveMode
};
foreach (int id in watermarkId) foreach (int id in fillerPresetId)
{ {
channel.WatermarkId = id; channel.FallbackFillerId = id;
} }
foreach (int id in fillerPresetId) return channel;
{ });
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) => private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name) createChannel.NotEmpty(c => c.Name)
@@ -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,14 +14,13 @@ 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, channels,
channels, request.UserAgent,
request.UserAgent, request.AccessToken));
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode) private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{ {
@@ -16,10 +16,9 @@ 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());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) => private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
@@ -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,9 +54,8 @@ 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(
EmbyMediaSource embyMediaSource) EmbyMediaSource embyMediaSource)
@@ -9,25 +9,25 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<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">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" /> <PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" /> <PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" /> <PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" /> <ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" /> <ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -42,34 +42,33 @@ 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, HardwareAcceleration = request.HardwareAcceleration,
HardwareAcceleration = request.HardwareAcceleration, VaapiDriver = request.VaapiDriver,
VaapiDriver = request.VaapiDriver, VaapiDevice = request.VaapiDevice,
VaapiDevice = request.VaapiDevice, QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames, ResolutionId = resolutionId,
ResolutionId = resolutionId, ScalingBehavior = request.ScalingBehavior,
ScalingBehavior = request.ScalingBehavior, VideoFormat = request.VideoFormat,
VideoFormat = request.VideoFormat, VideoProfile = request.VideoProfile,
VideoProfile = request.VideoProfile, VideoPreset = request.VideoPreset,
VideoPreset = request.VideoPreset, AllowBFrames = request.AllowBFrames,
AllowBFrames = request.AllowBFrames, BitDepth = request.BitDepth,
BitDepth = request.BitDepth, VideoBitrate = request.VideoBitrate,
VideoBitrate = request.VideoBitrate, VideoBufferSize = request.VideoBufferSize,
VideoBufferSize = request.VideoBufferSize, TonemapAlgorithm = request.TonemapAlgorithm,
TonemapAlgorithm = request.TonemapAlgorithm, AudioFormat = request.AudioFormat,
AudioFormat = request.AudioFormat, AudioBitrate = request.AudioBitrate,
AudioBitrate = request.AudioBitrate, AudioBufferSize = request.AudioBufferSize,
AudioBufferSize = request.AudioBufferSize, NormalizeLoudnessMode = request.NormalizeLoudnessMode,
NormalizeLoudnessMode = request.NormalizeLoudnessMode, AudioChannels = request.AudioChannels,
AudioChannels = request.AudioChannels, AudioSampleRate = request.AudioSampleRate,
AudioSampleRate = request.AudioSampleRate, NormalizeFramerate = request.NormalizeFramerate,
NormalizeFramerate = request.NormalizeFramerate, DeinterlaceVideo = request.DeinterlaceVideo
DeinterlaceVideo = request.DeinterlaceVideo });
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) => private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name) createFFmpegProfile.NotEmpty(x => x.Name)
@@ -16,10 +16,9 @@ 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());
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) => private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
@@ -13,12 +13,11 @@ 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 () => {
{ var guid = Guid.NewGuid();
Guid 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)
Either<BaseError, CachedImagePathViewModel>>; : IRequest<
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,9 +48,8 @@ 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(
JellyfinMediaSource jellyfinMediaSource) JellyfinMediaSource jellyfinMediaSource)
@@ -54,9 +54,9 @@ 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)
.WithArguments(arguments) .WithArguments(arguments)
@@ -64,13 +64,12 @@ 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(), MediaKind = request.MediaKind,
MediaKind = request.MediaKind, MediaSourceId = lms.Id
MediaSourceId = lms.Id })
})
.Map(o => o.ToValidation<BaseError>("LocalMediaSource does not exist.")); .Map(o => o.ToValidation<BaseError>("LocalMediaSource does not exist."));
} }
@@ -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,9 +102,8 @@ 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(
TvContext dbContext, TvContext dbContext,
@@ -112,19 +111,18 @@ 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 Name = request.Name,
{ Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(),
Name = request.Name, MediaKind = existing.MediaKind,
Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(), MediaSourceId = existing.Id
MediaKind = existing.MediaKind, };
MediaSourceId = existing.Id
};
return new Parameters(existing, incoming); return new Parameters(existing, incoming);
}) })
.Map(o => o.ToValidation<BaseError>("LocalLibrary does not exist.")); .Map(o => o.ToValidation<BaseError>("LocalLibrary does not exist."));
private static string NormalizePath(string path) => private static string NormalizePath(string path) =>
@@ -14,10 +14,9 @@ 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) .ToList());
.ToList());
} }
@@ -15,12 +15,11 @@ 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) .Map(ProjectToViewModel).ToList());
.Map(ProjectToViewModel).ToList());
private static bool ShouldIncludeLibrary(Library library) => private static bool ShouldIncludeLibrary(Library library) =>
library switch library switch
@@ -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,9 +27,9 @@ 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));
} }
int count = entries.Count(); int count = entries.Count();
+8 -9
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,13 +183,12 @@ 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, false,
false, string.Empty))
string.Empty))
.ToList(), .ToList(),
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(), collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
// collection view doesn't use local paths // collection view doesn't use local paths
@@ -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,12 +39,11 @@ 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>() });
});
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext, TvContext dbContext,
@@ -51,45 +51,42 @@ 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(i =>
MultiCollectionItems = request.Items.Bind( {
i => if (i.CollectionId.HasValue)
{ {
if (i.CollectionId.HasValue) return Some(
new MultiCollectionItem
{ {
return Some( CollectionId = i.CollectionId.Value,
new MultiCollectionItem ScheduleAsGroup = i.ScheduleAsGroup,
{ PlaybackOrder = i.PlaybackOrder
CollectionId = i.CollectionId.Value, });
ScheduleAsGroup = i.ScheduleAsGroup, }
PlaybackOrder = i.PlaybackOrder
});
}
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) {
return Some(
new MultiCollectionSmartItem
{ {
return Some( SmartCollectionId = i.SmartCollectionId.Value,
new MultiCollectionSmartItem ScheduleAsGroup = i.ScheduleAsGroup,
{ PlaybackOrder = i.PlaybackOrder
SmartCollectionId = i.SmartCollectionId.Value, });
ScheduleAsGroup = i.ScheduleAsGroup, }
PlaybackOrder = i.PlaybackOrder
});
}
return Option<MultiCollectionSmartItem>.None; return Option<MultiCollectionSmartItem>.None;
}) })
.ToList() .ToList()
}); });
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext, TvContext dbContext,
@@ -25,12 +25,11 @@ 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 });
});
private static async Task<Validation<BaseError, string>> ValidatePlaylistName( private static async Task<Validation<BaseError, string>> ValidatePlaylistName(
TvContext dbContext, TvContext dbContext,
@@ -48,12 +48,11 @@ 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 });
});
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext, TvContext dbContext,
@@ -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,15 +50,14 @@ 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, MultiCollectionId = c.Id,
MultiCollectionId = c.Id, ScheduleAsGroup = i.ScheduleAsGroup,
ScheduleAsGroup = i.ScheduleAsGroup, PlaybackOrder = i.PlaybackOrder
PlaybackOrder = i.PlaybackOrder })
})
.ToList(); .ToList();
var toRemove = c.MultiCollectionItems var toRemove = c.MultiCollectionItems
.Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId)) .Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId))
@@ -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,15 +84,14 @@ 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, MultiCollectionId = c.Id,
MultiCollectionId = c.Id, ScheduleAsGroup = i.ScheduleAsGroup,
ScheduleAsGroup = i.ScheduleAsGroup, PlaybackOrder = i.PlaybackOrder
PlaybackOrder = i.PlaybackOrder })
})
.ToList(); .ToList();
var toRemoveSmart = c.MultiCollectionSmartItems var toRemoveSmart = c.MultiCollectionSmartItems
.Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId)) .Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId))
@@ -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,13 +37,12 @@ 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, Seed = new Random().Next()
Seed = new Random().Next() });
});
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
@@ -50,13 +50,12 @@ 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, ProgramSchedulePlayoutType = playoutType
ProgramSchedulePlayoutType = playoutType });
});
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
@@ -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,13 +51,12 @@ 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, ProgramSchedulePlayoutType = playoutType
ProgramSchedulePlayoutType = playoutType });
});
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
@@ -50,14 +50,13 @@ 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, ProgramSchedulePlayoutType = playoutType,
ProgramSchedulePlayoutType = playoutType, Seed = new Random().Next()
Seed = new Random().Next() });
});
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
@@ -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:
+10 -14
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,18 +50,16 @@ 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]");
case Song s: case Song s:
string songArtist = s.SongMetadata.HeadOrNone() string songArtist = s.SongMetadata.HeadOrNone()
@@ -70,10 +67,9 @@ 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]");
case Image i: case Image i:
return i.ImageMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty).IfNone("[unknown image]"); return i.ImageMetadata.HeadOrNone().Map(im => im.Title ?? string.Empty).IfNone("[unknown image]");
@@ -19,17 +19,16 @@ 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, p.Channel.Number,
p.Channel.Number, p.Channel.ProgressMode,
p.Channel.ProgressMode, p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name, p.TemplateFile,
p.TemplateFile, p.ExternalJsonFile,
p.ExternalJsonFile, p.DailyRebuildTime))
p.DailyRebuildTime))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
} }
@@ -17,16 +17,15 @@ 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, p.Channel.Number,
p.Channel.Number, p.Channel.ProgressMode,
p.Channel.ProgressMode, p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name, p.TemplateFile,
p.TemplateFile, p.ExternalJsonFile,
p.ExternalJsonFile, p.DailyRebuildTime));
p.DailyRebuildTime));
} }
} }
@@ -20,13 +20,12 @@ 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 => {
{ await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken);
await _channel.WriteAsync(new TryCompletePlexPinFlow(pin), cancellationToken); return Right<BaseError, string>(pin.Url);
return Right<BaseError, string>(pin.Url); })
})
); );
} }
@@ -10,8 +10,9 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Plex; namespace ErsatzTV.Application.Plex;
public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler, IRequestHandler<SynchronizePlexMediaSources, public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler,
Either<BaseError, List<PlexMediaSource>>> IRequestHandler<SynchronizePlexMediaSources,
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,
Either<BaseError, PlexConnectionParametersViewModel>> IRequestHandler<GetPlexConnectionParameters,
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,20 +30,19 @@ 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;
return new ProgramSchedule
{ {
bool keepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether; Name = name,
return new ProgramSchedule KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
{ TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows,
Name = name, ShuffleScheduleItems = request.ShuffleScheduleItems,
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether, RandomStartPoint = request.RandomStartPoint,
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows, FixedStartTimeBehavior = request.FixedStartTimeBehavior
ShuffleScheduleItems = request.ShuffleScheduleItems, };
RandomStartPoint = request.RandomStartPoint, });
FixedStartTimeBehavior = request.FixedStartTimeBehavior
};
});
private static async Task<Validation<BaseError, string>> ValidateName( private static async Task<Validation<BaseError, string>> ValidateName(
TvContext dbContext, TvContext dbContext,
@@ -12,15 +12,14 @@ 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, ps.TreatCollectionsAsShows,
ps.TreatCollectionsAsShows, ps.ShuffleScheduleItems,
ps.ShuffleScheduleItems, ps.RandomStartPoint,
ps.RandomStartPoint, ps.FixedStartTimeBehavior))
ps.FixedStartTimeBehavior))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
} }
@@ -52,9 +52,8 @@ 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());
} }
// shuffled schedule items supports a limited set of property values // shuffled schedule items supports a limited set of property values
@@ -42,14 +42,13 @@ 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, Height = request.Height,
Height = request.Height, IsCustom = true
IsCustom = true });
});
private static async Task<Validation<BaseError, Unit>> ResolutionMustBeUnique( private static async Task<Validation<BaseError, Unit>> ResolutionMustBeUnique(
TvContext dbContext, TvContext dbContext,
@@ -25,13 +25,12 @@ 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, Minutes = 30
Minutes = 30 });
});
private static async Task<Validation<BaseError, string>> ValidateBlockName( private static async Task<Validation<BaseError, string>> ValidateBlockName(
TvContext dbContext, TvContext dbContext,
@@ -25,12 +25,11 @@ 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 });
});
private static async Task<Validation<BaseError, string>> ValidateDecoName( private static async Task<Validation<BaseError, string>> ValidateDecoName(
TvContext dbContext, TvContext dbContext,
@@ -26,12 +26,11 @@ 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 }));
}));
private static Validation<BaseError, string> ValidateName(CreateDecoTemplate createDecoTemplate) => private static Validation<BaseError, string> ValidateName(CreateDecoTemplate createDecoTemplate) =>
createDecoTemplate.NotEmpty(x => x.Name) createDecoTemplate.NotEmpty(x => x.Name)
@@ -26,12 +26,11 @@ 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 }));
}));
private static Validation<BaseError, string> ValidateName(CreateTemplate createTemplate) => private static Validation<BaseError, string> ValidateName(CreateTemplate createTemplate) =>
createTemplate.NotEmpty(x => x.Name) createTemplate.NotEmpty(x => x.Name)
@@ -13,9 +13,8 @@ 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);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)
@@ -16,9 +16,8 @@ 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);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)
@@ -71,15 +71,14 @@ 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( i.BlockId,
i.BlockId, i.StartTime,
i.StartTime, i.StartTime + TimeSpan.FromMinutes(block.Minutes));
i.StartTime + TimeSpan.FromMinutes(block.Minutes)); })
})
.ToList(); .ToList();
foreach (BlockTemplateItem item in allTemplateItems) foreach (BlockTemplateItem item in allTemplateItems)
@@ -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,10 +18,9 @@ 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,10 +16,9 @@ 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,12 +42,11 @@ 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) }));
}));
return result; return result;
} }
@@ -17,10 +17,9 @@ 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))
.Take(10) .Take(10)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@@ -19,10 +19,9 @@ 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))
.Take(10) .Take(10)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@@ -14,10 +14,9 @@ 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))
.ThenBy(s => s.Year) .ThenBy(s => s.Year)
.Take(10) .Take(10)
@@ -19,10 +19,9 @@ 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))
.Take(10) .Take(10)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@@ -19,10 +19,9 @@ 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))
.Take(10) .Take(10)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
@@ -20,10 +20,9 @@ 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))
.ThenBy(s => s.Year) .ThenBy(s => s.Year)
.Take(10) .Take(10)
@@ -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,15 +585,14 @@ 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( fileName.Replace("live", string.Empty).Split('.')[0],
fileName.Replace("live", string.Empty).Split('.')[0], CultureInfo.InvariantCulture);
CultureInfo.InvariantCulture); return new Segment(file, sequenceNumber);
return new Segment(file, sequenceNumber); })
})
.ToList(); .ToList();
var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList();
@@ -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,21 +45,20 @@ 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 "hls-direct" => StreamingMode.HttpLiveStreamingDirect,
{ "segmenter" => StreamingMode.HttpLiveStreamingSegmenter,
"hls-direct" => StreamingMode.HttpLiveStreamingDirect, "segmenter-v2" => StreamingMode.HttpLiveStreamingSegmenterV2,
"segmenter" => StreamingMode.HttpLiveStreamingSegmenter, "ts" => StreamingMode.TransportStreamHybrid,
"segmenter-v2" => StreamingMode.HttpLiveStreamingSegmenterV2, "ts-legacy" => StreamingMode.TransportStream,
"ts" => StreamingMode.TransportStreamHybrid, _ => channel.StreamingMode
"ts-legacy" => StreamingMode.TransportStream, };
_ => channel.StreamingMode
};
return channel; return channel;
}) })
.Map(o => o.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist.")); .Map(o => o.ToValidation<BaseError>($"Channel number {request.ChannelNumber} does not exist."));
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) => private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
@@ -227,9 +227,8 @@ 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);
bool disableWatermarks = playoutItemWithPath.PlayoutItem.DisableWatermarks; bool disableWatermarks = playoutItemWithPath.PlayoutItem.DisableWatermarks;
@@ -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,11 +214,10 @@ 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);
result.AddRange(episodeIds); result.AddRange(episodeIds);
@@ -228,11 +225,10 @@ 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);
result.AddRange(movieIds); result.AddRange(movieIds);
@@ -240,11 +236,10 @@ 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);
result.AddRange(musicVideoIds); result.AddRange(musicVideoIds);
@@ -252,11 +247,10 @@ 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);
result.AddRange(otherVideoIds); result.AddRange(otherVideoIds);
@@ -284,12 +278,10 @@ 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(s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path) ||
.Filter( FileDoesntExist(mediaItem.Id, s));
s => s.IsExtracted == false || string.IsNullOrWhiteSpace(s.Path) ||
FileDoesntExist(mediaItem.Id, s));
// find cache paths for each subtitle // find cache paths for each subtitle
foreach (Subtitle subtitle in subtitles) foreach (Subtitle subtitle in subtitles)
+13 -13
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,19 +22,19 @@ 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([]));
internal static TelevisionSeasonViewModel ProjectToViewModel( internal static TelevisionSeasonViewModel ProjectToViewModel(
@@ -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,35 +38,34 @@ 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 Name = request.Name,
{ Image = null,
Name = request.Name, OriginalContentType = null,
Image = null, Mode = request.Mode,
OriginalContentType = null, ImageSource = request.ImageSource,
Mode = request.Mode, Location = request.Location,
ImageSource = request.ImageSource, Size = request.Size,
Location = request.Location, WidthPercent = request.Width,
Size = request.Size, HorizontalMarginPercent = request.HorizontalMargin,
WidthPercent = request.Width, VerticalMarginPercent = request.VerticalMargin,
HorizontalMarginPercent = request.HorizontalMargin, FrequencyMinutes = request.FrequencyMinutes,
VerticalMarginPercent = request.VerticalMargin, DurationSeconds = request.DurationSeconds,
FrequencyMinutes = request.FrequencyMinutes, Opacity = request.Opacity,
DurationSeconds = request.DurationSeconds, PlaceWithinSourceContent = request.PlaceWithinSourceContent
Opacity = request.Opacity, };
PlaceWithinSourceContent = request.PlaceWithinSourceContent
};
if (request.ImageSource == ChannelWatermarkImageSource.Custom) if (request.ImageSource == ChannelWatermarkImageSource.Custom)
{ {
watermark.Image = request.Image?.Path; watermark.Image = request.Image?.Path;
watermark.OriginalContentType = request.Image?.ContentType; watermark.OriginalContentType = request.Image?.ContentType;
} }
return watermark; return watermark;
}); });
private static Validation<BaseError, string> ValidateName(CreateWatermark request) => private static Validation<BaseError, string> ValidateName(CreateWatermark request) =>
request.NotEmpty(x => x.Name) request.NotEmpty(x => x.Name)
@@ -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;
+24 -24
View File
@@ -7,32 +7,32 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<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="LanguageExt.Core" Version="4.4.9" /> <PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="NSubstitute" Version="5.3.0" /> <PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.3.2" /> <PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
<PackageReference Include="Serilog" Version="4.3.0" /> <PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" /> <PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="Shouldly" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" /> <ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure.Sqlite\ErsatzTV.Infrastructure.Sqlite.csproj" /> <ProjectReference Include="..\ErsatzTV.Infrastructure.Sqlite\ErsatzTV.Infrastructure.Sqlite.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" /> <ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -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,17 +123,17 @@ 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:
- "eng" - "eng"
- 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,17 +318,17 @@ 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:
- "eng" - "eng"
- 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,17 +351,17 @@ 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:
- "eng" - "eng"
- 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();

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