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:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
+4
-4
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user