Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076a88230e | ||
|
|
f06a04ed0e | ||
|
|
07d690a31f | ||
|
|
001453714a | ||
|
|
d303bc0158 | ||
|
|
51b671dec7 | ||
|
|
a5e1cc7c3d | ||
|
|
9ba6686c44 | ||
|
|
104d4a0cbd | ||
|
|
22c4fe2a27 | ||
|
|
7e0bdfdb40 | ||
|
|
6bdaca0222 | ||
|
|
67aa3a5a46 | ||
|
|
a0332e242c | ||
|
|
cd74859d28 | ||
|
|
470fba275b | ||
|
|
e42b000b7f | ||
|
|
489f8d92ff | ||
|
|
527d3c6e4b | ||
|
|
c33c037188 | ||
|
|
4c70d61d48 | ||
|
|
00fdc272e9 | ||
|
|
f04c18c810 | ||
|
|
eca58dbe7f | ||
|
|
cf9479d2a9 | ||
|
|
b6331331b0 | ||
|
|
ed365cfa43 | ||
|
|
b3a1e71570 | ||
|
|
454343d14f | ||
|
|
c0a6677861 | ||
|
|
2efcbca2da | ||
|
|
f96efa9b2f | ||
|
|
f46041305c | ||
|
|
493a496b91 | ||
|
|
739d074bc6 | ||
|
|
c5c28cb92d | ||
|
|
636bf0715b | ||
|
|
0ca15ee7a8 | ||
|
|
6565240eeb | ||
|
|
d64188927c | ||
|
|
0ecec3cb07 | ||
|
|
a8e861abc0 | ||
|
|
76446e0d69 | ||
|
|
c6d90ad750 | ||
|
|
e5a9ef6196 | ||
|
|
8439d6fd54 | ||
|
|
1773691c39 | ||
|
|
940cdd10a3 | ||
|
|
6beb9f7e33 | ||
|
|
898a21dcd9 | ||
|
|
a01888792a | ||
|
|
8b1f8dd36b | ||
|
|
e9b26d6bdb | ||
|
|
79b2e9dbfe | ||
|
|
9ba0cbd84f | ||
|
|
d5b48d2601 | ||
|
|
aa938baec8 | ||
|
|
a13f964200 | ||
|
|
0da9701f9c | ||
|
|
b3f4c22f49 | ||
|
|
50fafbfb98 | ||
|
|
914d128610 | ||
|
|
1a2f36f561 | ||
|
|
96887fbd79 | ||
|
|
c07e2afff4 | ||
|
|
4953617f79 | ||
|
|
1587ac7d62 |
+12
-14
@@ -51,16 +51,17 @@ jobs:
|
|||||||
final="${tag2/prealpha/$short}"
|
final="${tag2/prealpha/$short}"
|
||||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx Base
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: builder-base
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Set up Docker Buildx NVIDIA
|
||||||
uses: actions/cache@v2.1.4
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
id: builder-nvidia
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
- name: Set up Docker Buildx VAAPI
|
||||||
restore-keys: |
|
uses: docker/setup-buildx-action@v1
|
||||||
${{ runner.os }}-buildx-
|
id: builder-vaapi
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
@@ -71,6 +72,7 @@ jobs:
|
|||||||
- name: Build and push base
|
- name: Build and push base
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-base.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -79,12 +81,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:develop
|
jasongdove/ersatztv:develop
|
||||||
jasongdove/ersatztv:${{ github.sha }}
|
jasongdove/ersatztv:${{ github.sha }}
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push nvidia
|
- name: Build and push nvidia
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/nvidia/Dockerfile
|
file: ./docker/nvidia/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -93,12 +94,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:develop-nvidia
|
jasongdove/ersatztv:develop-nvidia
|
||||||
jasongdove/ersatztv:${{ github.sha }}-nvidia
|
jasongdove/ersatztv:${{ github.sha }}-nvidia
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push vaapi
|
- name: Build and push vaapi
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/vaapi/Dockerfile
|
file: ./docker/vaapi/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -107,5 +107,3 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:develop-vaapi
|
jasongdove/ersatztv:develop-vaapi
|
||||||
jasongdove/ersatztv:${{ github.sha }}-vaapi
|
jasongdove/ersatztv:${{ github.sha }}-vaapi
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|||||||
@@ -83,16 +83,17 @@ jobs:
|
|||||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||||
echo "DOCKER_TAG=${tag/-prealpha/}" >> $GITHUB_ENV
|
echo "DOCKER_TAG=${tag/-prealpha/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx Base
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: builder-base
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Set up Docker Buildx NVIDIA
|
||||||
uses: actions/cache@v2.1.4
|
uses: docker/setup-buildx-action@v1
|
||||||
with:
|
id: builder-nvidia
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
- name: Set up Docker Buildx VAAPI
|
||||||
restore-keys: |
|
uses: docker/setup-buildx-action@v1
|
||||||
${{ runner.os }}-buildx-
|
id: builder-vaapi
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
@@ -103,6 +104,7 @@ jobs:
|
|||||||
- name: Build and push base
|
- name: Build and push base
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-base.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -111,12 +113,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:latest
|
jasongdove/ersatztv:latest
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}
|
jasongdove/ersatztv:${{ env.DOCKER_TAG }}
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push nvidia
|
- name: Build and push nvidia
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/nvidia/Dockerfile
|
file: ./docker/nvidia/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -125,12 +126,11 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:latest-nvidia
|
jasongdove/ersatztv:latest-nvidia
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
|
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|
||||||
- name: Build and push vaapi
|
- name: Build and push vaapi
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
|
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
file: ./docker/vaapi/Dockerfile
|
file: ./docker/vaapi/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
@@ -139,5 +139,3 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
jasongdove/ersatztv:latest-vaapi
|
jasongdove/ersatztv:latest-vaapi
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
|
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.Channels
|
|||||||
string Name,
|
string Name,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode);
|
StreamingMode StreamingMode);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Number,
|
string Number,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -9,6 +11,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
|||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static ErsatzTV.Application.Channels.Mapper;
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels.Commands
|
namespace ErsatzTV.Application.Channels.Commands
|
||||||
{
|
{
|
||||||
@@ -36,9 +39,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
_channelRepository.Add(c).Map(ProjectToViewModel);
|
_channelRepository.Add(c).Map(ProjectToViewModel);
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
|
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
|
||||||
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request))
|
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request),
|
||||||
|
ValidatePreferredLanguage(request))
|
||||||
.Apply(
|
.Apply(
|
||||||
(name, number, ffmpegProfileId) =>
|
(name, number, ffmpegProfileId, preferredLanguageCode) =>
|
||||||
{
|
{
|
||||||
var artwork = new List<Artwork>();
|
var artwork = new List<Artwork>();
|
||||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||||
@@ -59,7 +63,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
Number = number,
|
Number = number,
|
||||||
FFmpegProfileId = ffmpegProfileId,
|
FFmpegProfileId = ffmpegProfileId,
|
||||||
StreamingMode = request.StreamingMode,
|
StreamingMode = request.StreamingMode,
|
||||||
Artwork = artwork
|
Artwork = artwork,
|
||||||
|
PreferredLanguageCode = preferredLanguageCode
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,6 +72,14 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
createChannel.NotEmpty(c => c.Name)
|
createChannel.NotEmpty(c => c.Name)
|
||||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||||
|
Optional(createChannel.PreferredLanguageCode)
|
||||||
|
.Filter(
|
||||||
|
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||||
|
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||||
|
|
||||||
|
|
||||||
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
|
private async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
|
||||||
{
|
{
|
||||||
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Number,
|
string Number,
|
||||||
int FFmpegProfileId,
|
int FFmpegProfileId,
|
||||||
string Logo,
|
string Logo,
|
||||||
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -32,6 +33,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
c.Name = update.Name;
|
c.Name = update.Name;
|
||||||
c.Number = update.Number;
|
c.Number = update.Number;
|
||||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||||
|
c.PreferredLanguageCode = update.PreferredLanguageCode;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||||
{
|
{
|
||||||
@@ -65,8 +67,9 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
||||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request))
|
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
|
||||||
.Apply((channelToUpdate, _, _) => channelToUpdate);
|
ValidatePreferredLanguage(request))
|
||||||
|
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
|
||||||
|
|
||||||
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
||||||
_channelRepository.Get(updateChannel.ChannelId)
|
_channelRepository.Get(updateChannel.ChannelId)
|
||||||
@@ -92,5 +95,12 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
|
|
||||||
return BaseError.New("Channel number must be unique");
|
return BaseError.New("Channel number must be unique");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||||
|
Optional(updateChannel.PreferredLanguageCode)
|
||||||
|
.Filter(
|
||||||
|
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||||
|
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToValidation<BaseError>("Preferred language code is invalid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.Application.Channels
|
|||||||
channel.Name,
|
channel.Name,
|
||||||
channel.FFmpegProfileId,
|
channel.FFmpegProfileId,
|
||||||
GetLogo(channel),
|
GetLogo(channel),
|
||||||
|
channel.PreferredLanguageCode,
|
||||||
channel.StreamingMode);
|
channel.StreamingMode);
|
||||||
|
|
||||||
private static string GetLogo(Channel channel) =>
|
private static string GetLogo(Channel channel) =>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static ErsatzTV.Application.Channels.Mapper;
|
using static ErsatzTV.Application.Channels.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Channels.Queries
|
namespace ErsatzTV.Application.Channels.Queries
|
||||||
{
|
{
|
||||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Application.Channels.Queries
|
|||||||
|
|
||||||
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||||
|
|
||||||
public Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||||
_channelRepository.GetAll().Map(channels => channels.Map(ProjectToViewModel).ToList());
|
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using MediatR;
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
{
|
{
|
||||||
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : IRequest;
|
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,77 @@
|
|||||||
using System.Threading;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
|
||||||
using Unit = MediatR.Unit;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
{
|
{
|
||||||
public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings>
|
public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler<UpdateFFmpegSettings, Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
|
|
||||||
public UpdateFFmpegSettingsHandler(IConfigElementRepository configElementRepository) =>
|
public UpdateFFmpegSettingsHandler(
|
||||||
_configElementRepository = configElementRepository;
|
IConfigElementRepository configElementRepository,
|
||||||
|
ILocalFileSystem localFileSystem)
|
||||||
public async Task<Unit> Handle(UpdateFFmpegSettings request, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
|
_configElementRepository = configElementRepository;
|
||||||
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
|
_localFileSystem = localFileSystem;
|
||||||
Option<ConfigElement> defaultFFmpegProfileId =
|
}
|
||||||
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
|
|
||||||
|
|
||||||
ffmpegPath.Match(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateFFmpegSettings request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(_ => ApplyUpdate(request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
||||||
|
(await FFmpegMustExist(request), await FFprobeMustExist(request))
|
||||||
|
.Apply((_, _) => Unit.Default);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
||||||
|
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
||||||
|
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
||||||
|
|
||||||
|
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
||||||
|
{
|
||||||
|
if (!_localFileSystem.FileExists(path))
|
||||||
|
{
|
||||||
|
return BaseError.New($"{name} path does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = path,
|
||||||
|
Arguments = "-version",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false
|
||||||
|
};
|
||||||
|
|
||||||
|
var test = new Process
|
||||||
|
{
|
||||||
|
StartInfo = startInfo
|
||||||
|
};
|
||||||
|
|
||||||
|
test.Start();
|
||||||
|
string output = await test.StandardOutput.ReadToEndAsync();
|
||||||
|
await test.WaitForExitAsync();
|
||||||
|
return test.ExitCode == 0 && output.Contains($"{name} version")
|
||||||
|
? Unit.Default
|
||||||
|
: BaseError.New($"Unable to verify {name} version");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
|
||||||
|
{
|
||||||
|
await _configElementRepository.Get(ConfigElementKey.FFmpegPath).Match(
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.FFmpegPath;
|
ce.Value = request.Settings.FFmpegPath;
|
||||||
@@ -35,7 +84,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_configElementRepository.Add(ce);
|
||||||
});
|
});
|
||||||
|
|
||||||
ffprobePath.Match(
|
await _configElementRepository.Get(ConfigElementKey.FFprobePath).Match(
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.FFprobePath;
|
ce.Value = request.Settings.FFprobePath;
|
||||||
@@ -48,7 +97,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_configElementRepository.Add(ce);
|
||||||
});
|
});
|
||||||
|
|
||||||
defaultFFmpegProfileId.Match(
|
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId).Match(
|
||||||
ce =>
|
ce =>
|
||||||
{
|
{
|
||||||
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
||||||
@@ -64,7 +113,45 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
_configElementRepository.Add(ce);
|
_configElementRepository.Add(ce);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Unit.Value;
|
await _configElementRepository.Get(ConfigElementKey.FFmpegSaveReports).Match(
|
||||||
|
ce =>
|
||||||
|
{
|
||||||
|
ce.Value = request.Settings.SaveReports.ToString();
|
||||||
|
_configElementRepository.Update(ce);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var ce = new ConfigElement
|
||||||
|
{
|
||||||
|
Key = ConfigElementKey.FFmpegSaveReports.Key,
|
||||||
|
Value = request.Settings.SaveReports.ToString()
|
||||||
|
};
|
||||||
|
_configElementRepository.Add(ce);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _configElementRepository.Get(ConfigElementKey.FFmpegPreferredLanguageCode).Match(
|
||||||
|
ce =>
|
||||||
|
{
|
||||||
|
ce.Value = request.Settings.PreferredLanguageCode;
|
||||||
|
_configElementRepository.Update(ce);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var ce = new ConfigElement
|
||||||
|
{
|
||||||
|
Key = ConfigElementKey.FFmpegPreferredLanguageCode.Key,
|
||||||
|
Value = request.Settings.PreferredLanguageCode
|
||||||
|
};
|
||||||
|
_configElementRepository.Add(ce);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,7 @@
|
|||||||
public string FFmpegPath { get; set; }
|
public string FFmpegPath { get; set; }
|
||||||
public string FFprobePath { get; set; }
|
public string FFprobePath { get; set; }
|
||||||
public int DefaultFFmpegProfileId { get; set; }
|
public int DefaultFFmpegProfileId { get; set; }
|
||||||
|
public string PreferredLanguageCode { get; set; }
|
||||||
|
public bool SaveReports { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
||||||
Option<int> defaultFFmpegProfileId =
|
Option<int> defaultFFmpegProfileId =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegDefaultProfileId);
|
||||||
|
Option<bool> saveReports =
|
||||||
|
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
|
||||||
|
Option<string> preferredLanguageCode =
|
||||||
|
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||||
|
|
||||||
return new FFmpegSettingsViewModel
|
return new FFmpegSettingsViewModel
|
||||||
{
|
{
|
||||||
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
||||||
FFprobePath = ffprobePath.IfNone(string.Empty),
|
FFprobePath = ffprobePath.IfNone(string.Empty),
|
||||||
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0)
|
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0),
|
||||||
|
SaveReports = saveReports.IfNone(false),
|
||||||
|
PreferredLanguageCode = preferredLanguageCode.IfNone("eng")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Threading;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Libraries.Commands
|
namespace ErsatzTV.Application.Libraries.Commands
|
||||||
@@ -11,9 +13,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
|
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
private readonly ILibraryRepository _libraryRepository;
|
private readonly ILibraryRepository _libraryRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
|
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository, ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
_libraryRepository = libraryRepository;
|
_libraryRepository = libraryRepository;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
DeleteLocalLibraryPath request,
|
DeleteLocalLibraryPath request,
|
||||||
@@ -22,8 +28,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.MapT(DoDeletion)
|
.MapT(DoDeletion)
|
||||||
.Bind(t => t.ToEitherAsync());
|
.Bind(t => t.ToEitherAsync());
|
||||||
|
|
||||||
private Task<Unit> DoDeletion(LibraryPath libraryPath) =>
|
private async Task<Unit> DoDeletion(LibraryPath libraryPath)
|
||||||
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit();
|
{
|
||||||
|
List<int> ids = await _libraryRepository.GetMediaIdsByLocalPath(libraryPath.Id);
|
||||||
|
await _searchIndex.RemoveItems(ids);
|
||||||
|
await _libraryRepository.DeleteLocalPath(libraryPath.Id);
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) =>
|
private async Task<Validation<BaseError, LibraryPath>> MediaSourceMustExist(DeleteLocalLibraryPath request) =>
|
||||||
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
|
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
List<MovieCardViewModel> MovieCards,
|
List<MovieCardViewModel> MovieCards,
|
||||||
List<TelevisionShowCardViewModel> ShowCards,
|
List<TelevisionShowCardViewModel> ShowCards,
|
||||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||||
List<TelevisionEpisodeCardViewModel> EpisodeCards);
|
List<TelevisionEpisodeCardViewModel> EpisodeCards)
|
||||||
|
{
|
||||||
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(Season season) =>
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(Season season) =>
|
||||||
new(
|
new(
|
||||||
season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty),
|
season.Show.ShowMetadata.HeadOrNone().Match(m => m.Title ?? string.Empty, () => string.Empty),
|
||||||
season.Id,
|
season.Id,
|
||||||
season.SeasonNumber,
|
season.SeasonNumber,
|
||||||
GetSeasonName(season.SeasonNumber),
|
GetSeasonName(season.SeasonNumber),
|
||||||
@@ -32,12 +32,16 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
new(
|
new(
|
||||||
episodeMetadata.EpisodeId,
|
episodeMetadata.EpisodeId,
|
||||||
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
||||||
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Map(m => m.Title).IfNone(string.Empty),
|
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match(
|
||||||
|
m => m.Title ?? string.Empty,
|
||||||
|
() => string.Empty),
|
||||||
episodeMetadata.Episode.Season.ShowId,
|
episodeMetadata.Episode.Season.ShowId,
|
||||||
episodeMetadata.Episode.SeasonId,
|
episodeMetadata.Episode.SeasonId,
|
||||||
episodeMetadata.Episode.EpisodeNumber,
|
episodeMetadata.Episode.EpisodeNumber,
|
||||||
episodeMetadata.Title,
|
episodeMetadata.Title,
|
||||||
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Map(em => em.Plot).IfNone(string.Empty),
|
episodeMetadata.Episode.EpisodeMetadata.HeadOrNone().Match(
|
||||||
|
em => em.Plot ?? string.Empty,
|
||||||
|
() => string.Empty),
|
||||||
GetThumbnail(episodeMetadata));
|
GetThumbnail(episodeMetadata));
|
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) =>
|
internal static MovieCardViewModel ProjectToViewModel(MovieMetadata movieMetadata) =>
|
||||||
@@ -52,11 +56,20 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
ProjectToViewModel(Collection collection) =>
|
ProjectToViewModel(Collection collection) =>
|
||||||
new(
|
new(
|
||||||
collection.Name,
|
collection.Name,
|
||||||
collection.MediaItems.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Movie>().Map(
|
||||||
|
m => ProjectToViewModel(m.MovieMetadata.Head()) with
|
||||||
|
{
|
||||||
|
CustomIndex = GetCustomIndex(collection, m.Id)
|
||||||
|
}).ToList(),
|
||||||
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
||||||
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
||||||
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
||||||
.ToList());
|
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||||
|
|
||||||
|
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||||
|
Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId))
|
||||||
|
.Map(ci => ci.CustomIndex ?? 0)
|
||||||
|
.IfNone(0);
|
||||||
|
|
||||||
internal static SearchCardResultsViewModel ProjectToSearchResults(List<MediaItem> items) =>
|
internal static SearchCardResultsViewModel ProjectToSearchResults(List<MediaItem> items) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MediaCardViewModel(string Title, string Subtitle, string SortTitle, string Poster);
|
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards);
|
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, Option<SearchPageMap> PageMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
{
|
{
|
||||||
public record MovieCardViewModel
|
public record MovieCardViewModel
|
||||||
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||||
|
MovieId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster)
|
||||||
{
|
{
|
||||||
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public record GetMovieCards(int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public class
|
|
||||||
GetMovieCardsHandler : IRequestHandler<GetMovieCards, MovieCardResultsViewModel>
|
|
||||||
{
|
|
||||||
private readonly IMovieRepository _movieRepository;
|
|
||||||
|
|
||||||
public GetMovieCardsHandler(IMovieRepository movieRepository) => _movieRepository = movieRepository;
|
|
||||||
|
|
||||||
public async Task<MovieCardResultsViewModel> Handle(GetMovieCards request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
int count = await _movieRepository.GetMovieCount();
|
|
||||||
|
|
||||||
List<MovieCardViewModel> results = await _movieRepository
|
|
||||||
.GetPagedMovies(request.PageNumber, request.PageSize)
|
|
||||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
|
||||||
|
|
||||||
return new MovieCardResultsViewModel(count, results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using ErsatzTV.Core;
|
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public record GetSearchCards(string Query) : IRequest<Either<BaseError, SearchCardResultsViewModel>>;
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ErsatzTV.Core;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public class GetSearchCardsHandler : IRequestHandler<GetSearchCards, Either<BaseError, SearchCardResultsViewModel>>
|
|
||||||
{
|
|
||||||
private readonly ISearchRepository _searchRepository;
|
|
||||||
|
|
||||||
public GetSearchCardsHandler(ISearchRepository searchRepository) => _searchRepository = searchRepository;
|
|
||||||
|
|
||||||
public Task<Either<BaseError, SearchCardResultsViewModel>> Handle(
|
|
||||||
GetSearchCards request,
|
|
||||||
CancellationToken cancellationToken) =>
|
|
||||||
request.Query.Split(":").Head() switch
|
|
||||||
{
|
|
||||||
"genre" => GenreSearch(request.Query.Replace("genre:", string.Empty)),
|
|
||||||
"tag" => TagSearch(request.Query.Replace("tag:", string.Empty)),
|
|
||||||
_ => TitleSearch(request.Query)
|
|
||||||
};
|
|
||||||
|
|
||||||
private Task<Either<BaseError, SearchCardResultsViewModel>> TitleSearch(string query) =>
|
|
||||||
Try(_searchRepository.SearchMediaItemsByTitle(query)).Sequence()
|
|
||||||
.Map(ProjectToSearchResults)
|
|
||||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
|
||||||
|
|
||||||
private Task<Either<BaseError, SearchCardResultsViewModel>> GenreSearch(string query) =>
|
|
||||||
Try(_searchRepository.SearchMediaItemsByGenre(query)).Sequence()
|
|
||||||
.Map(ProjectToSearchResults)
|
|
||||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
|
||||||
|
|
||||||
private Task<Either<BaseError, SearchCardResultsViewModel>> TagSearch(string query) =>
|
|
||||||
Try(_searchRepository.SearchMediaItemsByTag(query)).Sequence()
|
|
||||||
.Map(ProjectToSearchResults)
|
|
||||||
.Map(t => t.ToEither(ex => BaseError.New($"Failed to search: {ex.Message}")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public record GetTelevisionShowCards(int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
|
||||||
{
|
|
||||||
public class
|
|
||||||
GetTelevisionShowCardsHandler : IRequestHandler<GetTelevisionShowCards, TelevisionShowCardResultsViewModel>
|
|
||||||
{
|
|
||||||
private readonly ITelevisionRepository _televisionRepository;
|
|
||||||
|
|
||||||
public GetTelevisionShowCardsHandler(ITelevisionRepository televisionRepository) =>
|
|
||||||
_televisionRepository = televisionRepository;
|
|
||||||
|
|
||||||
public async Task<TelevisionShowCardResultsViewModel> Handle(
|
|
||||||
GetTelevisionShowCards request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
int count = await _televisionRepository.GetShowCount();
|
|
||||||
|
|
||||||
List<TelevisionShowCardViewModel> results = await _televisionRepository
|
|
||||||
.GetPagedShows(request.PageNumber, request.PageSize)
|
|
||||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
|
||||||
|
|
||||||
return new TelevisionShowCardResultsViewModel(count, results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string Title,
|
string Title,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Poster) : MediaCardViewModel(
|
string Poster) : MediaCardViewModel(
|
||||||
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Poster,
|
string Poster,
|
||||||
string Placeholder) : MediaCardViewModel(
|
string Placeholder) : MediaCardViewModel(
|
||||||
|
TelevisionSeasonId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionShowCardResultsViewModel(int Count, List<TelevisionShowCardViewModel> Cards);
|
public record TelevisionShowCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<TelevisionShowCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
public record TelevisionShowCardViewModel
|
public record TelevisionShowCardViewModel
|
||||||
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||||
|
TelevisionShowId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
{
|
{
|
||||||
public record UpdateCollection
|
public record UpdateCollection
|
||||||
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>;
|
(int CollectionId, string Name) : MediatR.IRequest<Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
public Option<bool> UseCustomPlaybackOrder { get; set; } = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record UpdateCollectionCustomOrder
|
||||||
|
(
|
||||||
|
int CollectionId,
|
||||||
|
List<MediaItemCustomOrder> MediaItemCustomOrders) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
|
||||||
|
public record MediaItemCustomOrder(int MediaItemId, int CustomIndex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
UpdateCollectionCustomOrderHandler : MediatR.IRequestHandler<UpdateCollectionCustomOrder,
|
||||||
|
Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public UpdateCollectionCustomOrderHandler(
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
|
UpdateCollectionCustomOrder request,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
Validate(request)
|
||||||
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollectionCustomOrder request)
|
||||||
|
{
|
||||||
|
foreach (MediaItemCustomOrder updateItem in request.MediaItemCustomOrders)
|
||||||
|
{
|
||||||
|
Option<CollectionItem> maybeCollectionItem =
|
||||||
|
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
|
||||||
|
|
||||||
|
maybeCollectionItem.IfSome(ci => ci.CustomIndex = updateItem.CustomIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await _mediaCollectionRepository.Update(c))
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||||
|
request.CollectionId))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> Validate(UpdateCollectionCustomOrder request) =>
|
||||||
|
CollectionMustExist(request);
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
|
UpdateCollectionCustomOrder request) =>
|
||||||
|
_mediaCollectionRepository.Get(request.CollectionId)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
@@ -9,10 +11,16 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
{
|
{
|
||||||
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
public class UpdateCollectionHandler : MediatR.IRequestHandler<UpdateCollection, Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
public UpdateCollectionHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
public UpdateCollectionHandler(
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
_mediaCollectionRepository = mediaCollectionRepository;
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
UpdateCollection request,
|
UpdateCollection request,
|
||||||
@@ -21,10 +29,21 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.MapT(c => ApplyUpdateRequest(c, request))
|
.MapT(c => ApplyUpdateRequest(c, request))
|
||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection update)
|
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request)
|
||||||
{
|
{
|
||||||
c.Name = update.Name;
|
c.Name = request.Name;
|
||||||
await _mediaCollectionRepository.Update(c);
|
request.UseCustomPlaybackOrder.IfSome(
|
||||||
|
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
|
||||||
|
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(
|
||||||
|
request.CollectionId))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace ErsatzTV.Application.MediaCollections
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
{
|
{
|
||||||
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
|
public record MediaCollectionViewModel(int Id, string Name) : MediaCardViewModel(
|
||||||
|
Id,
|
||||||
Name,
|
Name,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
Name,
|
Name,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using MediatR;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaItems.Queries
|
|
||||||
{
|
|
||||||
public record SearchAllMediaItems(string SearchString) : IRequest<List<MediaItemSearchResultViewModel>>;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
|
||||||
using LanguageExt;
|
|
||||||
using MediatR;
|
|
||||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaItems.Queries
|
|
||||||
{
|
|
||||||
public class SearchAllMediaItemsHandler : IRequestHandler<SearchAllMediaItems, List<MediaItemSearchResultViewModel>>
|
|
||||||
{
|
|
||||||
private readonly IMediaItemRepository _mediaItemRepository;
|
|
||||||
|
|
||||||
public SearchAllMediaItemsHandler(IMediaItemRepository mediaItemRepository) =>
|
|
||||||
_mediaItemRepository = mediaItemRepository;
|
|
||||||
|
|
||||||
public Task<List<MediaItemSearchResultViewModel>>
|
|
||||||
Handle(SearchAllMediaItems request, CancellationToken cancellationToken) =>
|
|
||||||
_mediaItemRepository.Search(request.SearchString).Map(list => list.Map(ProjectToSearchViewModel).ToList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -61,21 +62,30 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||||
{
|
{
|
||||||
|
var sw = new Stopwatch();
|
||||||
|
sw.Start();
|
||||||
|
|
||||||
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
||||||
{
|
{
|
||||||
switch (localLibrary.MediaKind)
|
switch (localLibrary.MediaKind)
|
||||||
{
|
{
|
||||||
case LibraryMediaKind.Movies:
|
case LibraryMediaKind.Movies:
|
||||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||||
break;
|
break;
|
||||||
case LibraryMediaKind.Shows:
|
case LibraryMediaKind.Shows:
|
||||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, lastScan);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localLibrary.LastScan = DateTime.UtcNow;
|
localLibrary.LastScan = DateTime.UtcNow;
|
||||||
await _libraryRepository.UpdateLastScan(localLibrary);
|
await _libraryRepository.UpdateLastScan(localLibrary);
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Scan of library {Name} completed in {Duration}",
|
||||||
|
localLibrary.Name,
|
||||||
|
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Movies
|
|||||||
Artwork(metadata, ArtworkKind.Poster),
|
Artwork(metadata, ArtworkKind.Poster),
|
||||||
Artwork(metadata, ArtworkKind.FanArt),
|
Artwork(metadata, ArtworkKind.FanArt),
|
||||||
metadata.Genres.Map(g => g.Name).ToList(),
|
metadata.Genres.Map(g => g.Name).ToList(),
|
||||||
metadata.Tags.Map(t => t.Name).ToList());
|
metadata.Tags.Map(t => t.Name).ToList(),
|
||||||
|
metadata.Studios.Map(s => s.Name).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.Movies
|
|||||||
string Poster,
|
string Poster,
|
||||||
string FanArt,
|
string FanArt,
|
||||||
List<string> Genres,
|
List<string> Genres,
|
||||||
List<string> Tags);
|
List<string> Tags,
|
||||||
|
List<string> Studios);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Plex.Commands
|
||||||
|
{
|
||||||
|
public record SignOutOfPlex : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Plex.Commands
|
||||||
|
{
|
||||||
|
public class SignOutOfPlexHandler : MediatR.IRequestHandler<SignOutOfPlex, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IEntityLocker _entityLocker;
|
||||||
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
private readonly IPlexSecretStore _plexSecretStore;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public SignOutOfPlexHandler(
|
||||||
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
IPlexSecretStore plexSecretStore,
|
||||||
|
IEntityLocker entityLocker,
|
||||||
|
ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
_plexSecretStore = plexSecretStore;
|
||||||
|
_entityLocker = entityLocker;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(SignOutOfPlex request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<int> ids = await _mediaSourceRepository.DeleteAllPlex();
|
||||||
|
await _searchIndex.RemoveItems(ids);
|
||||||
|
await _plexSecretStore.DeleteAll();
|
||||||
|
_entityLocker.UnlockPlex();
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@ using LanguageExt;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.Plex.Commands
|
namespace ErsatzTV.Application.Plex.Commands
|
||||||
{
|
{
|
||||||
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
public record SynchronizePlexLibraries(int PlexMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||||
|
IPlexBackgroundServiceRequest;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
|
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().ToList();
|
||||||
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
|
var toAdd = libraries.Filter(library => existing.All(l => l.Key != library.Key)).ToList();
|
||||||
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
|
var toRemove = existing.Filter(library => libraries.All(l => l.Key != library.Key)).ToList();
|
||||||
connectionParameters.PlexMediaSource.Libraries.AddRange(toAdd);
|
return _mediaSourceRepository.UpdateLibraries(
|
||||||
toRemove.ForEach(c => connectionParameters.PlexMediaSource.Libraries.Remove(c));
|
connectionParameters.PlexMediaSource.Id,
|
||||||
|
toAdd,
|
||||||
return _mediaSourceRepository.Update(connectionParameters.PlexMediaSource);
|
toRemove);
|
||||||
},
|
},
|
||||||
error =>
|
error =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,17 +24,20 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
|
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
|
||||||
private readonly IPlexSecretStore _plexSecretStore;
|
private readonly IPlexSecretStore _plexSecretStore;
|
||||||
|
private readonly IPlexTelevisionLibraryScanner _plexTelevisionLibraryScanner;
|
||||||
|
|
||||||
public SynchronizePlexLibraryByIdHandler(
|
public SynchronizePlexLibraryByIdHandler(
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IPlexSecretStore plexSecretStore,
|
IPlexSecretStore plexSecretStore,
|
||||||
IPlexMovieLibraryScanner plexMovieLibraryScanner,
|
IPlexMovieLibraryScanner plexMovieLibraryScanner,
|
||||||
|
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
|
||||||
IEntityLocker entityLocker,
|
IEntityLocker entityLocker,
|
||||||
ILogger<SynchronizePlexLibraryByIdHandler> logger)
|
ILogger<SynchronizePlexLibraryByIdHandler> logger)
|
||||||
{
|
{
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
_plexSecretStore = plexSecretStore;
|
_plexSecretStore = plexSecretStore;
|
||||||
_plexMovieLibraryScanner = plexMovieLibraryScanner;
|
_plexMovieLibraryScanner = plexMovieLibraryScanner;
|
||||||
|
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner;
|
||||||
_entityLocker = entityLocker;
|
_entityLocker = entityLocker;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -67,8 +70,10 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
parameters.Library);
|
parameters.Library);
|
||||||
break;
|
break;
|
||||||
case LibraryMediaKind.Shows:
|
case LibraryMediaKind.Shows:
|
||||||
// TODO: plex tv scanner
|
await _plexTelevisionLibraryScanner.ScanLibrary(
|
||||||
// await _televisionFolderScanner.ScanFolder(parameters.LocalMediaSource, parameters.FFprobePath);
|
parameters.ConnectionParameters.ActiveConnection,
|
||||||
|
parameters.ConnectionParameters.PlexServerAuthToken,
|
||||||
|
parameters.Library);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -15,15 +17,21 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePlexMediaSources,
|
SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePlexMediaSources,
|
||||||
Either<BaseError, List<PlexMediaSource>>>
|
Either<BaseError, List<PlexMediaSource>>>
|
||||||
{
|
{
|
||||||
|
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IEntityLocker _entityLocker;
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||||
|
|
||||||
public SynchronizePlexMediaSourcesHandler(
|
public SynchronizePlexMediaSourcesHandler(
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
IPlexTvApiClient plexTvApiClient)
|
IPlexTvApiClient plexTvApiClient,
|
||||||
|
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||||
|
IEntityLocker entityLocker)
|
||||||
{
|
{
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
_plexTvApiClient = plexTvApiClient;
|
_plexTvApiClient = plexTvApiClient;
|
||||||
|
_channel = channel;
|
||||||
|
_entityLocker = entityLocker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, List<PlexMediaSource>>> Handle(
|
public Task<Either<BaseError, List<PlexMediaSource>>> Handle(
|
||||||
@@ -39,6 +47,13 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
await SynchronizeServer(allExisting, server);
|
await SynchronizeServer(allExisting, server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (PlexMediaSource mediaSource in await _mediaSourceRepository.GetAllPlex())
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new SynchronizePlexLibraries(mediaSource.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
_entityLocker.UnlockPlex();
|
||||||
|
|
||||||
return allExisting;
|
return allExisting;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,25 +64,24 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
await maybeExisting.Match(
|
await maybeExisting.Match(
|
||||||
existing =>
|
existing =>
|
||||||
{
|
{
|
||||||
|
existing.Platform = server.Platform;
|
||||||
|
existing.PlatformVersion = server.PlatformVersion;
|
||||||
existing.ProductVersion = server.ProductVersion;
|
existing.ProductVersion = server.ProductVersion;
|
||||||
existing.ServerName = server.ServerName;
|
existing.ServerName = server.ServerName;
|
||||||
MergeConnections(existing.Connections, server.Connections);
|
var toAdd = server.Connections
|
||||||
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
|
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||||
{
|
var toRemove = existing.Connections
|
||||||
existing.Connections.Head().IsActive = true;
|
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||||
}
|
return _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||||
|
|
||||||
return _mediaSourceRepository.Update(existing);
|
|
||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
await _mediaSourceRepository.Add(server);
|
|
||||||
if (server.Connections.Any())
|
if (server.Connections.Any())
|
||||||
{
|
{
|
||||||
server.Connections.Head().IsActive = true;
|
server.Connections.Head().IsActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _mediaSourceRepository.Update(server);
|
await _mediaSourceRepository.Add(server);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Plex.Commands
|
namespace ErsatzTV.Application.Plex.Commands
|
||||||
@@ -13,16 +14,23 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
Either<BaseError, Unit>>
|
Either<BaseError, Unit>>
|
||||||
{
|
{
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) =>
|
public UpdatePlexLibraryPreferencesHandler(
|
||||||
|
IMediaSourceRepository mediaSourceRepository,
|
||||||
|
ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
_mediaSourceRepository = mediaSourceRepository;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> Handle(
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
UpdatePlexLibraryPreferences request,
|
UpdatePlexLibraryPreferences request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
|
||||||
await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
List<int> ids = await _mediaSourceRepository.DisablePlexLibrarySync(toDisable);
|
||||||
|
await _searchIndex.RemoveItems(ids);
|
||||||
|
|
||||||
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
|
||||||
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
|
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using ErsatzTV.Core;
|
|||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Plex.Commands
|
namespace ErsatzTV.Application.Plex.Commands
|
||||||
{
|
{
|
||||||
@@ -35,20 +34,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
|||||||
var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
|
var toRemove = plexMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
|
||||||
var toUpdate = incoming.Except(toAdd).ToList();
|
var toUpdate = incoming.Except(toAdd).ToList();
|
||||||
|
|
||||||
plexMediaSource.PathReplacements.AddRange(toAdd);
|
return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove);
|
||||||
toRemove.ForEach(pr => plexMediaSource.PathReplacements.Remove(pr));
|
|
||||||
foreach (PlexPathReplacement pathReplacement in toUpdate)
|
|
||||||
{
|
|
||||||
Optional(plexMediaSource.PathReplacements.SingleOrDefault(pr => pr.Id == pathReplacement.Id))
|
|
||||||
.IfSome(
|
|
||||||
pr =>
|
|
||||||
{
|
|
||||||
pr.PlexPath = pathReplacement.PlexPath;
|
|
||||||
pr.LocalPath = pathReplacement.LocalPath;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return _mediaSourceRepository.Update(plexMediaSource).ToUnit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
|
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Commands
|
||||||
|
{
|
||||||
|
public record RebuildSearchIndex : MediatR.IRequest<Unit>, IBackgroundServiceRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Commands
|
||||||
|
{
|
||||||
|
public class RebuildSearchIndexHandler : MediatR.IRequestHandler<RebuildSearchIndex, Unit>
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
private readonly ISearchRepository _searchRepository;
|
||||||
|
|
||||||
|
public RebuildSearchIndexHandler(
|
||||||
|
ISearchIndex searchIndex,
|
||||||
|
ISearchRepository searchRepository,
|
||||||
|
IConfigElementRepository configElementRepository,
|
||||||
|
ILogger<RebuildSearchIndexHandler> logger)
|
||||||
|
{
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
_logger = logger;
|
||||||
|
_searchRepository = searchRepository;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder);
|
||||||
|
|
||||||
|
if (!indexFolderExists ||
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
||||||
|
_searchIndex.Version)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Migrating search index to version {Version}", _searchIndex.Version);
|
||||||
|
|
||||||
|
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
|
||||||
|
await _searchIndex.Rebuild(itemIds);
|
||||||
|
|
||||||
|
Option<ConfigElement> maybeVersion =
|
||||||
|
await _configElementRepository.Get(ConfigElementKey.SearchIndexVersion);
|
||||||
|
await maybeVersion.Match(
|
||||||
|
version =>
|
||||||
|
{
|
||||||
|
version.Value = _searchIndex.Version.ToString();
|
||||||
|
return _configElementRepository.Update(version);
|
||||||
|
},
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var configElement = new ConfigElement
|
||||||
|
{
|
||||||
|
Key = ConfigElementKey.SearchIndexVersion.Key,
|
||||||
|
Value = _searchIndex.Version.ToString()
|
||||||
|
};
|
||||||
|
return _configElementRepository.Add(configElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogDebug("Done migrating search index");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Search index is already version {Version}", _searchIndex.Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public record QuerySearchIndex(string Query) : IRequest<SearchResult>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public class QuerySearchIndexHandler : IRequestHandler<QuerySearchIndex, SearchResult>
|
||||||
|
{
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public QuerySearchIndexHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
|
||||||
|
|
||||||
|
public Task<SearchResult> Handle(QuerySearchIndex request, CancellationToken cancellationToken) =>
|
||||||
|
_searchIndex.Search(request.Query, 0, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public record QuerySearchIndexMovies
|
||||||
|
(string Query, int PageNumber, int PageSize) : IRequest<MovieCardResultsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public class QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMovies, MovieCardResultsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IMovieRepository _movieRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public QuerySearchIndexMoviesHandler(ISearchIndex searchIndex, IMovieRepository movieRepository)
|
||||||
|
{
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
_movieRepository = movieRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MovieCardResultsViewModel> Handle(
|
||||||
|
QuerySearchIndexMovies request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SearchResult searchResult = await _searchIndex.Search(
|
||||||
|
request.Query,
|
||||||
|
(request.PageNumber - 1) * request.PageSize,
|
||||||
|
request.PageSize);
|
||||||
|
|
||||||
|
List<MovieCardViewModel> items = await _movieRepository
|
||||||
|
.GetMoviesForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
|
||||||
|
return new MovieCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public record QuerySearchIndexShows
|
||||||
|
(string Query, int PageNumber, int PageSize) : IRequest<TelevisionShowCardResultsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search.Queries
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
QuerySearchIndexShowsHandler : IRequestHandler<QuerySearchIndexShows, TelevisionShowCardResultsViewModel>
|
||||||
|
{
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
private readonly ITelevisionRepository _televisionRepository;
|
||||||
|
|
||||||
|
public QuerySearchIndexShowsHandler(ISearchIndex searchIndex, ITelevisionRepository televisionRepository)
|
||||||
|
{
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
_televisionRepository = televisionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TelevisionShowCardResultsViewModel> Handle(
|
||||||
|
QuerySearchIndexShows request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SearchResult searchResult = await _searchIndex.Search(
|
||||||
|
request.Query,
|
||||||
|
(request.PageNumber - 1) * request.PageSize,
|
||||||
|
request.PageSize);
|
||||||
|
|
||||||
|
List<TelevisionShowCardViewModel> items = await _televisionRepository
|
||||||
|
.GetShowsForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
|
||||||
|
return new TelevisionShowCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Search
|
||||||
|
{
|
||||||
|
public class SearchResultViewModel<T>
|
||||||
|
{
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
public List<T> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+122
-56
@@ -1,39 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.FFmpeg;
|
using ErsatzTV.Core.FFmpeg;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using Microsoft.Extensions.Logging;
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Streaming.Queries
|
namespace ErsatzTV.Application.Streaming.Queries
|
||||||
{
|
{
|
||||||
public class
|
public class
|
||||||
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
|
GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<GetPlayoutItemProcessByChannelNumber>
|
||||||
{
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly FFmpegProcessService _ffmpegProcessService;
|
private readonly FFmpegProcessService _ffmpegProcessService;
|
||||||
private readonly ILogger<GetPlayoutItemProcessByChannelNumberHandler> _logger;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
|
||||||
private readonly IPlayoutRepository _playoutRepository;
|
private readonly IPlayoutRepository _playoutRepository;
|
||||||
|
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||||
|
|
||||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||||
IChannelRepository channelRepository,
|
IChannelRepository channelRepository,
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
IPlayoutRepository playoutRepository,
|
IPlayoutRepository playoutRepository,
|
||||||
IMediaSourceRepository mediaSourceRepository,
|
|
||||||
FFmpegProcessService ffmpegProcessService,
|
FFmpegProcessService ffmpegProcessService,
|
||||||
ILogger<GetPlayoutItemProcessByChannelNumberHandler> logger)
|
ILocalFileSystem localFileSystem,
|
||||||
|
IPlexPathReplacementService plexPathReplacementService)
|
||||||
: base(channelRepository, configElementRepository)
|
: base(channelRepository, configElementRepository)
|
||||||
{
|
{
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
_playoutRepository = playoutRepository;
|
_playoutRepository = playoutRepository;
|
||||||
_mediaSourceRepository = mediaSourceRepository;
|
|
||||||
_ffmpegProcessService = ffmpegProcessService;
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
_logger = logger;
|
_localFileSystem = localFileSystem;
|
||||||
|
_plexPathReplacementService = plexPathReplacementService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<Either<BaseError, Process>> GetProcess(
|
protected override async Task<Either<BaseError, Process>> GetProcess(
|
||||||
@@ -42,68 +44,132 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
string ffmpegPath)
|
string ffmpegPath)
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.Now;
|
DateTimeOffset now = DateTimeOffset.Now;
|
||||||
Option<PlayoutItem> maybePlayoutItem = await _playoutRepository.GetPlayoutItem(channel.Id, now);
|
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await _playoutRepository
|
||||||
return await maybePlayoutItem.Match<Task<Either<BaseError, Process>>>(
|
.GetPlayoutItem(channel.Id, now)
|
||||||
async playoutItem =>
|
.Map(o => o.ToEither<BaseError>(new UnableToLocatePlayoutItem()))
|
||||||
|
.BindT(ValidatePlayoutItemPath);
|
||||||
|
|
||||||
|
return await maybePlayoutItem.Match(
|
||||||
|
async playoutItemWithPath =>
|
||||||
{
|
{
|
||||||
MediaVersion version = playoutItem.MediaItem switch
|
MediaVersion version = playoutItemWithPath.PlayoutItem.MediaItem switch
|
||||||
{
|
{
|
||||||
Movie m => m.MediaVersions.Head(),
|
Movie m => m.MediaVersions.Head(),
|
||||||
Episode e => e.MediaVersions.Head(),
|
Episode e => e.MediaVersions.Head(),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem))
|
_ => throw new ArgumentOutOfRangeException(nameof(playoutItemWithPath))
|
||||||
};
|
};
|
||||||
|
|
||||||
MediaFile file = version.MediaFiles.Head();
|
bool saveReports = await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||||
string path = file.Path;
|
.Map(result => result.IfNone(false));
|
||||||
if (playoutItem.MediaItem is PlexMovie plexMovie)
|
|
||||||
{
|
|
||||||
path = await GetReplacementPlexPath(plexMovie.LibraryPathId, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _ffmpegProcessService.ForPlayoutItem(
|
return Right<BaseError, Process>(
|
||||||
ffmpegPath,
|
await _ffmpegProcessService.ForPlayoutItem(
|
||||||
channel,
|
ffmpegPath,
|
||||||
version,
|
saveReports,
|
||||||
path,
|
channel,
|
||||||
playoutItem.StartOffset,
|
version,
|
||||||
now);
|
playoutItemWithPath.Path,
|
||||||
|
playoutItemWithPath.PlayoutItem.StartOffset,
|
||||||
|
now));
|
||||||
},
|
},
|
||||||
async () =>
|
async error =>
|
||||||
{
|
{
|
||||||
if (channel.FFmpegProfile.Transcode)
|
var offlineTranscodeMessage =
|
||||||
|
$"offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'";
|
||||||
|
|
||||||
|
Option<TimeSpan> maybeDuration = await Optional(channel.FFmpegProfile.Transcode)
|
||||||
|
.Filter(transcode => transcode)
|
||||||
|
.Match(
|
||||||
|
_ => _playoutRepository.GetNextItemStart(channel.Id, now)
|
||||||
|
.MapT(nextStart => nextStart - now),
|
||||||
|
() => Option<TimeSpan>.None.AsTask());
|
||||||
|
|
||||||
|
switch (error)
|
||||||
{
|
{
|
||||||
Option<TimeSpan> maybeDuration = await _playoutRepository.GetNextItemStart(channel.Id, now)
|
case UnableToLocatePlayoutItem:
|
||||||
.MapT(nextStart => nextStart - now);
|
if (channel.FFmpegProfile.Transcode)
|
||||||
|
{
|
||||||
|
return _ffmpegProcessService.ForError(
|
||||||
|
ffmpegPath,
|
||||||
|
channel,
|
||||||
|
maybeDuration,
|
||||||
|
"Channel is Offline");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message =
|
||||||
|
$"Unable to locate playout item for channel {channel.Number}; {offlineTranscodeMessage}";
|
||||||
|
|
||||||
return _ffmpegProcessService.ForOfflineImage(ffmpegPath, channel, maybeDuration);
|
return BaseError.New(message);
|
||||||
|
}
|
||||||
|
case PlayoutItemDoesNotExistOnDisk:
|
||||||
|
if (channel.FFmpegProfile.Transcode)
|
||||||
|
{
|
||||||
|
return _ffmpegProcessService.ForError(ffmpegPath, channel, maybeDuration, error.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message =
|
||||||
|
$"Playout item does not exist on disk for channel {channel.Number}; {offlineTranscodeMessage}";
|
||||||
|
|
||||||
|
return BaseError.New(message);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (channel.FFmpegProfile.Transcode)
|
||||||
|
{
|
||||||
|
return _ffmpegProcessService.ForError(
|
||||||
|
ffmpegPath,
|
||||||
|
channel,
|
||||||
|
maybeDuration,
|
||||||
|
"Channel is Offline");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message =
|
||||||
|
$"Unexpected error locating playout item for channel {channel.Number}; {offlineTranscodeMessage}";
|
||||||
|
|
||||||
|
return BaseError.New(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var message =
|
|
||||||
$"Unable to locate playout item for channel {channel.Number}; offline image is unavailable because transcoding is disabled in ffmpeg profile '{channel.FFmpegProfile.Name}'";
|
|
||||||
|
|
||||||
return BaseError.New(message);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetReplacementPlexPath(int libraryPathId, string path)
|
private async Task<Either<BaseError, PlayoutItemWithPath>> ValidatePlayoutItemPath(PlayoutItem playoutItem)
|
||||||
{
|
{
|
||||||
List<PlexPathReplacement> replacements =
|
string path = await GetPlayoutItemPath(playoutItem);
|
||||||
await _mediaSourceRepository.GetPlexPathReplacementsByLibraryId(libraryPathId);
|
|
||||||
// TODO: this might barf mixing platforms (i.e. plex on linux, etv on windows)
|
// TODO: this won't work with url streaming from plex
|
||||||
Option<PlexPathReplacement> maybeReplacement = replacements
|
if (_localFileSystem.FileExists(path))
|
||||||
.SingleOrDefault(r => path.StartsWith(r.PlexPath + Path.DirectorySeparatorChar));
|
{
|
||||||
return maybeReplacement.Match(
|
return new PlayoutItemWithPath(playoutItem, path);
|
||||||
replacement =>
|
}
|
||||||
{
|
|
||||||
string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath);
|
return new PlayoutItemDoesNotExistOnDisk(path);
|
||||||
_logger.LogInformation(
|
|
||||||
"Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}",
|
|
||||||
replacement.PlexPath,
|
|
||||||
replacement.LocalPath,
|
|
||||||
finalPath);
|
|
||||||
return finalPath;
|
|
||||||
},
|
|
||||||
() => path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetPlayoutItemPath(PlayoutItem playoutItem)
|
||||||
|
{
|
||||||
|
MediaVersion version = playoutItem.MediaItem switch
|
||||||
|
{
|
||||||
|
Movie m => m.MediaVersions.Head(),
|
||||||
|
Episode e => e.MediaVersions.Head(),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(playoutItem))
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaFile file = version.MediaFiles.Head();
|
||||||
|
string path = file.Path;
|
||||||
|
return playoutItem.MediaItem switch
|
||||||
|
{
|
||||||
|
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||||
|
plexMovie.LibraryPathId,
|
||||||
|
path),
|
||||||
|
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath(
|
||||||
|
plexEpisode.LibraryPathId,
|
||||||
|
path),
|
||||||
|
_ => path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PlayoutItemWithPath(PlayoutItem PlayoutItem, string Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ namespace ErsatzTV.Application.Television
|
|||||||
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
|
show.ShowMetadata.HeadOrNone().Map(GetPoster).IfNone(string.Empty),
|
||||||
show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty),
|
show.ShowMetadata.HeadOrNone().Map(GetFanArt).IfNone(string.Empty),
|
||||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()));
|
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||||
|
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList())
|
||||||
|
.IfNone(new List<string>()));
|
||||||
|
|
||||||
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
|
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Application.Television
|
|||||||
string Poster,
|
string Poster,
|
||||||
string FanArt,
|
string FanArt,
|
||||||
List<string> Genres,
|
List<string> Genres,
|
||||||
List<string> Tags);
|
List<string> Tags,
|
||||||
|
List<string> Studios);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
|
||||||
<PackageReference Include="RestSharp" Version="106.11.7" />
|
<PackageReference Include="RestSharp" Version="106.11.7" />
|
||||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||||
<PackageReference Include="Moq" Version="4.16.0" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="1.0.1" />
|
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
{
|
{
|
||||||
var builder = new FFmpegComplexFilterBuilder();
|
var builder = new FFmpegComplexFilterBuilder();
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsNone.Should().BeTrue();
|
result.IsNone.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -30,15 +30,15 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
||||||
.WithAlignedAudio(duration);
|
.WithAlignedAudio(duration);
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be($"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a]");
|
filter.ComplexFilter.Should().Be($"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a]");
|
||||||
filter.AudioLabel.Should().Be("[a]");
|
filter.AudioLabel.Should().Be("[a]");
|
||||||
filter.VideoLabel.Should().Be("0:v");
|
filter.VideoLabel.Should().Be("0:0");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,36 +50,36 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
.WithAlignedAudio(duration)
|
.WithAlignedAudio(duration)
|
||||||
.WithDeinterlace(true);
|
.WithDeinterlace(true);
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be(
|
filter.ComplexFilter.Should().Be(
|
||||||
$"[0:a]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:v]yadif=1[v]");
|
$"[0:1]apad=whole_dur={duration.TotalMilliseconds}ms[a];[0:0]yadif=1[v]");
|
||||||
filter.AudioLabel.Should().Be("[a]");
|
filter.AudioLabel.Should().Be("[a]");
|
||||||
filter.VideoLabel.Should().Be("[v]");
|
filter.VideoLabel.Should().Be("[v]");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase(true, false, false, "[0:v]yadif=1[v]", "[v]")]
|
[TestCase(true, false, false, "[0:0]yadif=1[v]", "[v]")]
|
||||||
[TestCase(true, true, false, "[0:v]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
[TestCase(true, true, false, "[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||||
[TestCase(true, false, true, "[0:v]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
[TestCase(true, false, true, "[0:0]yadif=1,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
"[0:0]yadif=1,scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(false, true, false, "[0:v]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
[TestCase(false, true, false, "[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1[v]", "[v]")]
|
||||||
[TestCase(false, false, true, "[0:v]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
[TestCase(false, false, true, "[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
"[0:0]scale=1920:1000:flags=fast_bilinear,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_Software_Video_Filter(
|
public void Should_Return_Software_Video_Filter(
|
||||||
bool deinterlace,
|
bool deinterlace,
|
||||||
@@ -101,55 +101,55 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||||
filter.AudioLabel.Should().Be("0:a");
|
filter.AudioLabel.Should().Be("0:1");
|
||||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase(true, false, false, "[0:v]deinterlace_qsv[v]", "[v]")]
|
[TestCase(true, false, false, "[0:0]deinterlace_qsv[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_QSV_Video_Filter(
|
public void Should_Return_QSV_Video_Filter(
|
||||||
bool deinterlace,
|
bool deinterlace,
|
||||||
@@ -172,74 +172,74 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||||
filter.AudioLabel.Should().Be("0:a");
|
filter.AudioLabel.Should().Be("0:1");
|
||||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
// TODO: get yadif_cuda working in docker
|
// TODO: get yadif_cuda working in docker
|
||||||
// [TestCase(true, false, false, "[0:v]yadif_cuda[v]", "[v]")]
|
// [TestCase(true, false, false, "[0:V]yadif_cuda[v]", "[v]")]
|
||||||
// [TestCase(
|
// [TestCase(
|
||||||
// true,
|
// true,
|
||||||
// true,
|
// true,
|
||||||
// false,
|
// false,
|
||||||
// "[0:v]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]",
|
// "[0:V]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,hwupload[v]",
|
||||||
// "[v]")]
|
// "[v]")]
|
||||||
// [TestCase(
|
// [TestCase(
|
||||||
// true,
|
// true,
|
||||||
// false,
|
// false,
|
||||||
// true,
|
// true,
|
||||||
// "[0:v]yadif_cuda,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
// "[0:V]yadif_cuda,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
// "[v]")]
|
// "[v]")]
|
||||||
// [TestCase(
|
// [TestCase(
|
||||||
// true,
|
// true,
|
||||||
// true,
|
// true,
|
||||||
// true,
|
// true,
|
||||||
// "[0:v]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
// "[0:V]yadif_cuda,scale_npp=1920:1000:format=yuv420p,hwdownload,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
// "[v]")]
|
// "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]scale_npp=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_NVENC_Video_Filter(
|
public void Should_Return_NVENC_Video_Filter(
|
||||||
bool deinterlace,
|
bool deinterlace,
|
||||||
@@ -262,104 +262,104 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||||
filter.AudioLabel.Should().Be("0:a");
|
filter.AudioLabel.Should().Be("0:1");
|
||||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase("h264", true, false, false, "[0:v]deinterlace_vaapi[v]", "[v]")]
|
[TestCase("h264", true, false, false, "[0:0]deinterlace_vaapi[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase("mpeg4", true, false, false, "[0:v]hwupload,deinterlace_vaapi[v]", "[v]")]
|
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwupload,deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:v]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:v]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:v]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,scale_vaapi=w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_VAAPI_Video_Filter(
|
public void Should_Return_VAAPI_Video_Filter(
|
||||||
string codec,
|
string codec,
|
||||||
@@ -384,14 +384,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
builder = builder.WithBlackBars(new Resolution { Width = 1920, Height = 1080 });
|
||||||
}
|
}
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> result = builder.Build();
|
Option<FFmpegComplexFilter> result = builder.Build(0, 1);
|
||||||
|
|
||||||
result.IsSome.Should().BeTrue();
|
result.IsSome.Should().BeTrue();
|
||||||
result.IfSome(
|
result.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
filter.ComplexFilter.Should().Be(expectedVideoFilter);
|
||||||
filter.AudioLabel.Should().Be("0:a");
|
filter.AudioLabel.Should().Be("0:1");
|
||||||
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
filter.VideoLabel.Should().Be(expectedVideoLabel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -40,6 +42,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -55,6 +59,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -72,6 +78,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -89,6 +97,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -104,6 +114,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -121,6 +133,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
now,
|
now,
|
||||||
now.AddMinutes(5));
|
now.AddMinutes(5));
|
||||||
|
|
||||||
@@ -139,6 +153,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
now,
|
now,
|
||||||
now.AddMinutes(5));
|
now.AddMinutes(5));
|
||||||
|
|
||||||
@@ -155,6 +171,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -177,6 +195,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -199,6 +219,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -221,6 +243,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -244,6 +268,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -267,6 +293,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -290,6 +318,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -315,6 +345,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -337,12 +369,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "mpeg2video" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -365,12 +399,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "mpeg2video" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -392,12 +428,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "libx264" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "libx264" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -420,12 +458,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "mpeg2video" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -452,6 +492,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -473,12 +515,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "mpeg2video" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -505,6 +549,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -527,12 +573,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
// not anamorphic
|
// not anamorphic
|
||||||
var version = new MediaVersion
|
var version = new MediaVersion
|
||||||
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1", VideoCodec = "mpeg2video" };
|
{ Width = 1920, Height = 1080, SampleAspectRatio = "1:1" };
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream { Codec = "mpeg2video" },
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -550,12 +598,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioCodec = "aac"
|
AudioCodec = "aac"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "aac" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "aac" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -571,12 +621,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioCodec = "aac"
|
AudioCodec = "aac"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -592,12 +644,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioCodec = "aac"
|
AudioCodec = "aac"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -613,12 +667,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioCodec = "aac"
|
AudioCodec = "aac"
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.HttpLiveStreaming,
|
StreamingMode.HttpLiveStreaming,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -634,12 +690,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioBitrate = 2424
|
AudioBitrate = 2424
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -655,12 +713,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioBufferSize = 2424
|
AudioBufferSize = 2424
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -678,12 +738,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioChannels = 6
|
AudioChannels = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -701,12 +763,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioSampleRate = 48
|
AudioSampleRate = 48
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -723,12 +787,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioChannels = 6
|
AudioChannels = 6
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -745,12 +811,14 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
AudioSampleRate = 48
|
AudioSampleRate = 48
|
||||||
};
|
};
|
||||||
|
|
||||||
var version = new MediaVersion { AudioCodec = "ac3" };
|
var version = new MediaVersion();
|
||||||
|
|
||||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream { Codec = "ac3" },
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
@@ -775,6 +843,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
StreamingMode.TransportStream,
|
StreamingMode.TransportStream,
|
||||||
ffmpegProfile,
|
ffmpegProfile,
|
||||||
new MediaVersion(),
|
new MediaVersion(),
|
||||||
|
new MediaStream(),
|
||||||
|
new MediaStream(),
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
DateTimeOffset.Now);
|
DateTimeOffset.Now);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
_folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList();
|
_folders = allFolders.Distinct().Map(f => new FakeFolderEntry(f)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Unit EnsureFolderExists(string folder) => Unit.Default;
|
||||||
|
|
||||||
public DateTime GetLastWriteTime(string path) =>
|
public DateTime GetLastWriteTime(string path) =>
|
||||||
Optional(_files.SingleOrDefault(f => f.Path == path))
|
Optional(_files.SingleOrDefault(f => f.Path == path))
|
||||||
.Map(f => f.LastWriteTime)
|
.Map(f => f.LastWriteTime)
|
||||||
|
|||||||
@@ -14,17 +14,22 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
private readonly Map<int, List<MediaItem>> _data;
|
private readonly Map<int, List<MediaItem>> _data;
|
||||||
|
|
||||||
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
|
public FakeMediaCollectionRepository(Map<int, List<MediaItem>> data) => _data = data;
|
||||||
|
|
||||||
public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
|
public Task<Collection> Add(Collection collection) => throw new NotSupportedException();
|
||||||
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
|
public Task<bool> AddMediaItem(int collectionId, int mediaItemId) => throw new NotSupportedException();
|
||||||
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
|
public Task<bool> AddMediaItems(int collectionId, List<int> mediaItemIds) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> Get(int id) => throw new NotSupportedException();
|
||||||
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> GetCollectionWithItems(int id) => throw new NotSupportedException();
|
||||||
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
|
public Task<Option<Collection>> GetCollectionWithItemsUntracked(int id) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
|
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
|
||||||
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
|
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
|
||||||
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
|
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
|
||||||
public Task Delete(int collectionId) => throw new NotSupportedException();
|
public Task Delete(int collectionId) => throw new NotSupportedException();
|
||||||
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
|
public Task<List<int>> PlayoutIdsUsingCollection(int collectionId) => throw new NotSupportedException();
|
||||||
|
public Task<bool> IsCustomPlaybackOrder(int collectionId) => false.AsTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Metadata;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Fakes
|
namespace ErsatzTV.Core.Tests.Fakes
|
||||||
{
|
{
|
||||||
public class FakeMovieWithPath : Movie
|
public class FakeMovieWithPath : MediaItemScanResult<Movie>
|
||||||
{
|
{
|
||||||
public FakeMovieWithPath(string path)
|
public FakeMovieWithPath(string path)
|
||||||
{
|
: base(
|
||||||
Path = path;
|
new Movie
|
||||||
|
|
||||||
MediaVersions = new List<MediaVersion>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
{
|
||||||
MediaFiles = new List<MediaFile>
|
MediaVersions = new List<MediaVersion>
|
||||||
{
|
{
|
||||||
new() { Path = path }
|
new()
|
||||||
|
{
|
||||||
|
MediaFiles = new List<MediaFile>
|
||||||
|
{
|
||||||
|
new() { Path = path }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}) =>
|
||||||
};
|
IsAdded = true;
|
||||||
}
|
|
||||||
|
|
||||||
public string Path { get; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Metadata;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Tests.Fakes
|
namespace ErsatzTV.Core.Tests.Fakes
|
||||||
@@ -11,12 +12,6 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
{
|
{
|
||||||
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
|
public Task<bool> AllShowsExist(List<int> showIds) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<bool> Update(Season season) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
|
|
||||||
|
|
||||||
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
|
public Task<List<Show>> GetAllShows() => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
|
public Task<Option<Show>> GetShow(int showId) => throw new NotSupportedException();
|
||||||
@@ -26,6 +21,8 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
public Task<List<ShowMetadata>> GetPagedShows(int pageNumber, int pageSize) =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<List<ShowMetadata>> GetShowsForCards(List<int> ids) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<List<Season>> GetAllSeasons() => throw new NotSupportedException();
|
public Task<List<Season>> GetAllSeasons() => throw new NotSupportedException();
|
||||||
@@ -49,7 +46,7 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
public Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) =>
|
public Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata) =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Either<BaseError, Show>>
|
public Task<Either<BaseError, MediaItemScanResult<Show>>>
|
||||||
AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) =>
|
AddShow(int libraryPathId, string showFolder, ShowMetadata metadata) =>
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
|
|
||||||
@@ -65,6 +62,39 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
|
|
||||||
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException();
|
public Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath) => throw new NotSupportedException();
|
||||||
|
|
||||||
public Task<Unit> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
|
public Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Either<BaseError, MediaItemScanResult<PlexShow>>> GetOrAddPlexShow(
|
||||||
|
PlexLibrary library,
|
||||||
|
PlexShow item) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> AddGenre(ShowMetadata metadata, Genre genre) => throw new NotSupportedException();
|
||||||
|
public Task<bool> AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys) =>
|
||||||
|
throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<Unit> SetEpisodeNumber(Episode episode, int episodeNumber) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> Update(Show show) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> Update(Season season) => throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<bool> Update(Episode episode) => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using ErsatzTV.Core.Errors;
|
|||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Core.Metadata;
|
using ErsatzTV.Core.Metadata;
|
||||||
using ErsatzTV.Core.Tests.Fakes;
|
using ErsatzTV.Core.Tests.Fakes;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
@@ -44,20 +45,24 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository = new Mock<IMovieRepository>();
|
_movieRepository = new Mock<IMovieRepository>();
|
||||||
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
|
_movieRepository.Setup(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()))
|
||||||
.Returns(
|
.Returns(
|
||||||
(LibraryPath _, string path) => Right<BaseError, Movie>(new FakeMovieWithPath(path)).AsTask());
|
(LibraryPath _, string path) =>
|
||||||
|
Right<BaseError, MediaItemScanResult<Movie>>(new FakeMovieWithPath(path)).AsTask());
|
||||||
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
||||||
.Returns(new List<string>().AsEnumerable().AsTask());
|
.Returns(new List<string>().AsEnumerable().AsTask());
|
||||||
|
|
||||||
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
|
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
|
||||||
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
|
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
|
||||||
|
|
||||||
|
_localStatisticsProvider.Setup(x => x.RefreshStatistics(It.IsAny<string>(), It.IsAny<MediaItem>()))
|
||||||
|
.Returns<string, MediaItem>((_, _) => Right<BaseError, bool>(true).AsTask());
|
||||||
|
|
||||||
// fallback metadata adds metadata to a movie, so we need to replicate that here
|
// fallback metadata adds metadata to a movie, so we need to replicate that here
|
||||||
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<MediaItem>()))
|
_localMetadataProvider.Setup(x => x.RefreshFallbackMetadata(It.IsAny<MediaItem>()))
|
||||||
.Returns(
|
.Returns(
|
||||||
(MediaItem mediaItem) =>
|
(MediaItem mediaItem) =>
|
||||||
{
|
{
|
||||||
((Movie) mediaItem).MovieMetadata = new List<MovieMetadata> { new() };
|
((Movie) mediaItem).MovieMetadata = new List<MovieMetadata> { new() };
|
||||||
return Unit.Default.AsTask();
|
return Task.FromResult(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
_imageCache = new Mock<IImageCache>();
|
_imageCache = new Mock<IImageCache>();
|
||||||
@@ -76,7 +81,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Path = BadFakeRoot };
|
var libraryPath = new LibraryPath { Path = BadFakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsLeft.Should().BeTrue();
|
result.IsLeft.Should().BeTrue();
|
||||||
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
|
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
|
||||||
@@ -96,7 +104,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -104,11 +115,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +143,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -137,11 +154,15 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
|
x => x.RefreshSidecarMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath),
|
||||||
|
metadataPath),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +183,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -170,11 +194,15 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshSidecarMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath), metadataPath),
|
x => x.RefreshSidecarMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath),
|
||||||
|
metadataPath),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +227,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -207,11 +238,61 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_imageCache.Verify(
|
||||||
|
x => x.CopyArtworkToCache(posterPath, ArtworkKind.Poster),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task NewMovie_Statistics_And_FallbackMetadata_And_FolderPoster(
|
||||||
|
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))]
|
||||||
|
string videoExtension,
|
||||||
|
[ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))]
|
||||||
|
string imageExtension)
|
||||||
|
{
|
||||||
|
string moviePath = Path.Combine(
|
||||||
|
FakeRoot,
|
||||||
|
Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}"));
|
||||||
|
|
||||||
|
string posterPath = Path.Combine(
|
||||||
|
Path.GetDirectoryName(moviePath) ?? string.Empty,
|
||||||
|
$"folder.{imageExtension}");
|
||||||
|
|
||||||
|
MovieFolderScanner service = GetService(
|
||||||
|
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now },
|
||||||
|
new FakeFileEntry(posterPath) { LastWriteTime = DateTime.Now }
|
||||||
|
);
|
||||||
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
|
_movieRepository.Verify(x => x.GetOrAdd(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
|
||||||
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
|
_localStatisticsProvider.Verify(
|
||||||
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
_localMetadataProvider.Verify(
|
||||||
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_imageCache.Verify(
|
_imageCache.Verify(
|
||||||
@@ -240,7 +321,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -248,11 +332,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_imageCache.Verify(
|
_imageCache.Verify(
|
||||||
@@ -280,7 +367,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -288,11 +378,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +409,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -324,11 +420,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +445,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -354,11 +456,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
_movieRepository.Verify(x => x.GetOrAdd(libraryPath, moviePath), Times.Once);
|
||||||
|
|
||||||
_localStatisticsProvider.Verify(
|
_localStatisticsProvider.Verify(
|
||||||
x => x.RefreshStatistics(FFprobePath, It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshStatistics(
|
||||||
|
FFprobePath,
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
|
|
||||||
_localMetadataProvider.Verify(
|
_localMetadataProvider.Verify(
|
||||||
x => x.RefreshFallbackMetadata(It.Is<FakeMovieWithPath>(i => i.Path == moviePath)),
|
x => x.RefreshFallbackMetadata(
|
||||||
|
It.Is<Movie>(i => i.MediaVersions.Head().MediaFiles.Head().Path == moviePath)),
|
||||||
Times.Once);
|
Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +483,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -402,7 +510,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
var libraryPath = new LibraryPath { Id = 1, Path = FakeRoot };
|
||||||
|
|
||||||
Either<BaseError, Unit> result = await service.ScanFolder(libraryPath, FFprobePath);
|
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||||
|
libraryPath,
|
||||||
|
FFprobePath,
|
||||||
|
DateTimeOffset.MinValue);
|
||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
@@ -417,7 +528,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Object,
|
_movieRepository.Object,
|
||||||
_localStatisticsProvider.Object,
|
_localStatisticsProvider.Object,
|
||||||
_localMetadataProvider.Object,
|
_localMetadataProvider.Object,
|
||||||
|
new Mock<IMetadataRepository>().Object,
|
||||||
_imageCache.Object,
|
_imageCache.Object,
|
||||||
|
new Mock<ISearchIndex>().Object,
|
||||||
new Mock<ILogger<MovieFolderScanner>>().Object
|
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Runtime;
|
||||||
|
using ErsatzTV.Core.Plex;
|
||||||
|
using FluentAssertions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Plex
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class PlexPathReplacementServiceTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task PlexWindows_To_EtvWindows()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"C:\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PlexWindows_To_EtvLinux()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"C:\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PlexWindows_To_EtvLinux_UncPath()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"\\192.168.1.100\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PlexWindows_To_EtvLinux_UncPathWithTrailingSlash()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"\\192.168.1.100\Something\Some Shared Folder\",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder/",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PlexLinux_To_EtvWindows()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||||
|
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task PlexLinux_To_EtvLinux()
|
||||||
|
{
|
||||||
|
var replacements = new List<PlexPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
PlexPath = @"/mnt/something/Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
PlexMediaSource = new PlexMediaSource { Platform = "Linux" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new PlexPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<PlexPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementPlexPath(
|
||||||
|
0,
|
||||||
|
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Scheduling;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Scheduling
|
||||||
|
{
|
||||||
|
public class CustomOrderContentTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void MediaItems_Should_Sort_By_CustomOrder()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState();
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 10; i >= 1; i--)
|
||||||
|
{
|
||||||
|
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||||
|
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_Index_Should_Increment()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState();
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
customOrderContent.State.Index.Should().Be(i % 10);
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void State_Should_Impact_Iterator_Start()
|
||||||
|
{
|
||||||
|
Collection collection = CreateCollection(10);
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
var state = new CollectionEnumeratorState { Index = 5 };
|
||||||
|
|
||||||
|
var customOrderContent = new CustomOrderCollectionEnumerator(collection, contents, state);
|
||||||
|
|
||||||
|
for (var i = 5; i >= 1; i--)
|
||||||
|
{
|
||||||
|
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||||
|
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||||
|
customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10
|
||||||
|
customOrderContent.MoveNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection CreateCollection(int episodeCount)
|
||||||
|
{
|
||||||
|
var collection = new Collection { CollectionItems = new List<CollectionItem>() };
|
||||||
|
|
||||||
|
for (var i = 1; i <= episodeCount; i++)
|
||||||
|
{
|
||||||
|
collection.CollectionItems.Add(
|
||||||
|
new CollectionItem
|
||||||
|
{
|
||||||
|
MediaItemId = i,
|
||||||
|
// reverse order
|
||||||
|
CustomIndex = episodeCount - i
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static List<MediaItem> Episodes(int count) =>
|
||||||
|
Range(1, count).Map(
|
||||||
|
i => (MediaItem) new Episode
|
||||||
|
{
|
||||||
|
Id = i,
|
||||||
|
EpisodeMetadata = new List<EpisodeMetadata>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ReleaseDate = new DateTime(2020, 1, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Reverse()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,24 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
_logger = factory.CreateLogger<PlayoutBuilder>();
|
_logger = factory.CreateLogger<PlayoutBuilder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Timeout(2000)]
|
||||||
|
public async Task ZeroDurationItem_Should_Abort()
|
||||||
|
{
|
||||||
|
var mediaItems = new List<MediaItem>
|
||||||
|
{
|
||||||
|
TestMovie(1, TimeSpan.Zero, DateTime.Today)
|
||||||
|
};
|
||||||
|
|
||||||
|
(PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random);
|
||||||
|
DateTimeOffset start = HoursAfterMidnight(0);
|
||||||
|
DateTimeOffset finish = start + TimeSpan.FromHours(6);
|
||||||
|
|
||||||
|
Playout result = await builder.BuildPlayoutItems(playout, start, finish);
|
||||||
|
|
||||||
|
result.Items.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task InitialFlood_Should_StartAtMidnight()
|
public async Task InitialFlood_Should_StartAtMidnight()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,54 @@ namespace ErsatzTV.Core.Tests.Scheduling
|
|||||||
// this seed will produce (shuffle) 1-10 in order
|
// this seed will produce (shuffle) 1-10 in order
|
||||||
private const int MagicSeed = 670596;
|
private const int MagicSeed = 670596;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Episodes_Should_Not_Duplicate_When_Reshuffling()
|
||||||
|
{
|
||||||
|
List<MediaItem> contents = Episodes(10);
|
||||||
|
|
||||||
|
// normally returns 10 5 7 4 3 6 2 8 9 1 1 (note duplicate 1 at end)
|
||||||
|
var state = new CollectionEnumeratorState { Seed = 8 };
|
||||||
|
|
||||||
|
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||||
|
|
||||||
|
var list = new List<int>();
|
||||||
|
for (var i = 1; i <= 1000; i++)
|
||||||
|
{
|
||||||
|
shuffledContent.Current.IsSome.Should().BeTrue();
|
||||||
|
shuffledContent.Current.Do(x => list.Add(x.Id));
|
||||||
|
shuffledContent.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < list.Count - 1; i++)
|
||||||
|
{
|
||||||
|
if (list[i] == list[i + 1])
|
||||||
|
{
|
||||||
|
Assert.Fail("List contains duplicate items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Timeout(2000)]
|
||||||
|
public void Duplicate_Check_Should_Ignore_Single_Item()
|
||||||
|
{
|
||||||
|
List<MediaItem> contents = Episodes(1);
|
||||||
|
|
||||||
|
var state = new CollectionEnumeratorState();
|
||||||
|
|
||||||
|
var shuffledContent = new ShuffledMediaCollectionEnumerator(contents, state);
|
||||||
|
|
||||||
|
var list = new List<int>();
|
||||||
|
for (var i = 1; i <= 10; i++)
|
||||||
|
{
|
||||||
|
shuffledContent.Current.IsSome.Should().BeTrue();
|
||||||
|
shuffledContent.Current.Do(x => list.Add(x.Id));
|
||||||
|
shuffledContent.MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Should().Equal(1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Episodes_Should_Shuffle()
|
public void Episodes_Should_Shuffle()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ namespace ErsatzTV.Core.Domain
|
|||||||
public FFmpegProfile FFmpegProfile { get; set; }
|
public FFmpegProfile FFmpegProfile { get; set; }
|
||||||
public StreamingMode StreamingMode { get; set; }
|
public StreamingMode StreamingMode { get; set; }
|
||||||
public List<Playout> Playouts { get; set; }
|
public List<Playout> Playouts { get; set; }
|
||||||
|
|
||||||
public List<Artwork> Artwork { get; set; }
|
public List<Artwork> Artwork { get; set; }
|
||||||
// public SourceMode Mode { get; set; }
|
public string PreferredLanguageCode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Domain
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
public List<MediaItem> MediaItems { get; set; }
|
public List<MediaItem> MediaItems { get; set; }
|
||||||
public List<CollectionItem> CollectionItems { get; set; }
|
public List<CollectionItem> CollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
public Collection Collection { get; set; }
|
public Collection Collection { get; set; }
|
||||||
public int MediaItemId { get; set; }
|
public int MediaItemId { get; set; }
|
||||||
public MediaItem MediaItem { get; set; }
|
public MediaItem MediaItem { get; set; }
|
||||||
|
public int? CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,8 @@
|
|||||||
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
|
public static ConfigElementKey FFprobePath => new("ffmpeg.ffprobe_path");
|
||||||
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
|
public static ConfigElementKey FFmpegDefaultProfileId => new("ffmpeg.default_profile_id");
|
||||||
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
|
public static ConfigElementKey FFmpegDefaultResolutionId => new("ffmpeg.default_resolution_id");
|
||||||
|
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||||
|
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||||
|
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class MediaStream
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Index { get; set; }
|
||||||
|
public string Codec { get; set; }
|
||||||
|
public string Profile { get; set; }
|
||||||
|
public MediaStreamKind MediaStreamKind { get; set; }
|
||||||
|
public string Language { get; set; }
|
||||||
|
public int Channels { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public bool Default { get; set; }
|
||||||
|
public bool Forced { get; set; }
|
||||||
|
public int MediaVersionId { get; set; }
|
||||||
|
public MediaVersion MediaVersion { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public enum MediaStreamKind
|
||||||
|
{
|
||||||
|
Video = 1,
|
||||||
|
Audio = 2,
|
||||||
|
Subtitle = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,15 +8,21 @@ namespace ErsatzTV.Core.Domain
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
public List<MediaFile> MediaFiles { get; set; }
|
public List<MediaFile> MediaFiles { get; set; }
|
||||||
|
public List<MediaStream> Streams { get; set; }
|
||||||
public TimeSpan Duration { get; set; }
|
public TimeSpan Duration { get; set; }
|
||||||
public string SampleAspectRatio { get; set; }
|
public string SampleAspectRatio { get; set; }
|
||||||
public string DisplayAspectRatio { get; set; }
|
public string DisplayAspectRatio { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use MediaSource instead")]
|
||||||
public string VideoCodec { get; set; }
|
public string VideoCodec { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use MediaSource instead")]
|
||||||
public string VideoProfile { get; set; }
|
public string VideoProfile { get; set; }
|
||||||
|
|
||||||
|
[Obsolete("Use MediaSource instead")]
|
||||||
public string AudioCodec { get; set; }
|
public string AudioCodec { get; set; }
|
||||||
|
|
||||||
public VideoScanKind VideoScanKind { get; set; }
|
public VideoScanKind VideoScanKind { get; set; }
|
||||||
public DateTime DateAdded { get; set; }
|
public DateTime DateAdded { get; set; }
|
||||||
public DateTime DateUpdated { get; set; }
|
public DateTime DateUpdated { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class PlexEpisode : Episode
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class PlexSeason : Season
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class PlexShow : Show
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ namespace ErsatzTV.Core.Domain
|
|||||||
{
|
{
|
||||||
public string ServerName { get; set; }
|
public string ServerName { get; set; }
|
||||||
public string ProductVersion { get; set; }
|
public string ProductVersion { get; set; }
|
||||||
|
public string Platform { get; set; }
|
||||||
|
public string PlatformVersion { get; set; }
|
||||||
public string ClientIdentifier { get; set; }
|
public string ClientIdentifier { get; set; }
|
||||||
|
|
||||||
// public bool IsOwned { get; set; }
|
// public bool IsOwned { get; set; }
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ namespace ErsatzTV.Core.Domain
|
|||||||
public List<Artwork> Artwork { get; set; }
|
public List<Artwork> Artwork { get; set; }
|
||||||
public List<Genre> Genres { get; set; }
|
public List<Genre> Genres { get; set; }
|
||||||
public List<Tag> Tags { get; set; }
|
public List<Tag> Tags { get; set; }
|
||||||
|
public List<Studio> Studios { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain
|
||||||
|
{
|
||||||
|
public class Studio
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace ErsatzTV.Core.Domain
|
|
||||||
{
|
|
||||||
public enum SourceMode
|
|
||||||
{
|
|
||||||
Transcode,
|
|
||||||
DirectPlay,
|
|
||||||
DirectPaths
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Core.Errors
|
||||||
|
{
|
||||||
|
public class PlayoutItemDoesNotExistOnDisk : BaseError
|
||||||
|
{
|
||||||
|
public PlayoutItemDoesNotExistOnDisk(string path) : base($"Playout item does not exist on disk\n{path}")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ErsatzTV.Core.Errors
|
||||||
|
{
|
||||||
|
public class UnableToLocatePlayoutItem : BaseError
|
||||||
|
{
|
||||||
|
public UnableToLocatePlayoutItem() : base("Unable to locate playout item")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
public override string ToString() =>
|
public override string ToString() =>
|
||||||
$@"ffconcat version 1.0
|
$@"ffconcat version 1.0
|
||||||
file {Scheme}://{Host}/ffmpeg/stream/{ChannelNumber}
|
file http://localhost:8409/ffmpeg/stream/{ChannelNumber}
|
||||||
file {Scheme}://{Host}/ffmpeg/stream/{ChannelNumber}";
|
file http://localhost:8409/ffmpeg/stream/{ChannelNumber}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Option<FFmpegComplexFilter> Build()
|
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, int audioStreamIndex)
|
||||||
{
|
{
|
||||||
var complexFilter = new StringBuilder();
|
var complexFilter = new StringBuilder();
|
||||||
|
|
||||||
var videoLabel = "0:v";
|
var videoLabel = $"0:{videoStreamIndex}";
|
||||||
var audioLabel = "0:a";
|
var audioLabel = $"0:{audioStreamIndex}";
|
||||||
|
|
||||||
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
|
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
|
||||||
bool isHardwareDecode = acceleration switch
|
bool isHardwareDecode = acceleration switch
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
StreamingMode streamingMode,
|
StreamingMode streamingMode,
|
||||||
FFmpegProfile ffmpegProfile,
|
FFmpegProfile ffmpegProfile,
|
||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
|
MediaStream videoStream,
|
||||||
|
MediaStream audioStream,
|
||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
DateTimeOffset now)
|
DateTimeOffset now)
|
||||||
{
|
{
|
||||||
@@ -85,7 +87,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
|
if (result.ScaledSize.IsSome || result.PadToDesiredResolution ||
|
||||||
NeedToNormalizeVideoCodec(ffmpegProfile, version))
|
NeedToNormalizeVideoCodec(ffmpegProfile, videoStream))
|
||||||
{
|
{
|
||||||
result.VideoCodec = ffmpegProfile.VideoCodec;
|
result.VideoCodec = ffmpegProfile.VideoCodec;
|
||||||
result.VideoBitrate = ffmpegProfile.VideoBitrate;
|
result.VideoBitrate = ffmpegProfile.VideoBitrate;
|
||||||
@@ -96,7 +98,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
result.VideoCodec = "copy";
|
result.VideoCodec = "copy";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NeedToNormalizeAudioCodec(ffmpegProfile, version))
|
if (NeedToNormalizeAudioCodec(ffmpegProfile, audioStream))
|
||||||
{
|
{
|
||||||
result.AudioCodec = ffmpegProfile.AudioCodec;
|
result.AudioCodec = ffmpegProfile.AudioCodec;
|
||||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||||
@@ -104,7 +106,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
if (ffmpegProfile.NormalizeAudio)
|
if (ffmpegProfile.NormalizeAudio)
|
||||||
{
|
{
|
||||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
||||||
|
{
|
||||||
|
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||||
|
}
|
||||||
|
|
||||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||||
result.AudioDuration = version.Duration;
|
result.AudioDuration = version.Duration;
|
||||||
}
|
}
|
||||||
@@ -152,11 +158,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private static bool IsOddSize(MediaVersion version) =>
|
private static bool IsOddSize(MediaVersion version) =>
|
||||||
version.Height % 2 == 1 || version.Width % 2 == 1;
|
version.Height % 2 == 1 || version.Width % 2 == 1;
|
||||||
|
|
||||||
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
private static bool NeedToNormalizeVideoCodec(FFmpegProfile ffmpegProfile, MediaStream videoStream) =>
|
||||||
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != version.VideoCodec;
|
ffmpegProfile.NormalizeVideoCodec && ffmpegProfile.VideoCodec != videoStream.Codec;
|
||||||
|
|
||||||
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaVersion version) =>
|
private static bool NeedToNormalizeAudioCodec(FFmpegProfile ffmpegProfile, MediaStream audioStream) =>
|
||||||
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != version.AudioCodec;
|
ffmpegProfile.NormalizeAudioCodec && ffmpegProfile.AudioCodec != audioStream.Codec;
|
||||||
|
|
||||||
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
private static IDisplaySize CalculateScaledSize(FFmpegProfile ffmpegProfile, MediaVersion version)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
@@ -39,9 +40,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
|
|
||||||
private readonly List<string> _arguments = new();
|
private readonly List<string> _arguments = new();
|
||||||
private readonly string _ffmpegPath;
|
private readonly string _ffmpegPath;
|
||||||
|
private readonly bool _saveReports;
|
||||||
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
|
private FFmpegComplexFilterBuilder _complexFilterBuilder = new();
|
||||||
|
|
||||||
public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath;
|
public FFmpegProcessBuilder(string ffmpegPath, bool saveReports)
|
||||||
|
{
|
||||||
|
_ffmpegPath = ffmpegPath;
|
||||||
|
_saveReports = saveReports;
|
||||||
|
}
|
||||||
|
|
||||||
public FFmpegProcessBuilder WithThreads(int threads)
|
public FFmpegProcessBuilder WithThreads(int threads)
|
||||||
{
|
{
|
||||||
@@ -218,13 +224,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
public FFmpegProcessBuilder WithErrorText(IDisplaySize desiredResolution, string text)
|
public FFmpegProcessBuilder WithErrorText(IDisplaySize desiredResolution, string text)
|
||||||
{
|
{
|
||||||
const string FONT_FILE = "fontfile=Resources/Roboto-Regular.ttf";
|
const string FONT_FILE = "fontfile=Resources/Roboto-Regular.ttf";
|
||||||
const string FONT_SIZE = "fontsize=60";
|
|
||||||
const string FONT_COLOR = "fontcolor=white";
|
const string FONT_COLOR = "fontcolor=white";
|
||||||
const string X = "x=(w-text_w)/2";
|
const string X = "x=(w-text_w)/2";
|
||||||
const string Y = "y=(h-text_h)/3*2";
|
const string Y = "y=(h-text_h)/3*2";
|
||||||
|
|
||||||
|
string fontSize = text.Length > 60 ? "fontsize=40" : "fontsize=60";
|
||||||
|
|
||||||
return WithFilterComplex(
|
return WithFilterComplex(
|
||||||
$"[0:0]scale={desiredResolution.Width}:{desiredResolution.Height},drawtext={FONT_FILE}:{FONT_SIZE}:{FONT_COLOR}:{X}:{Y}:text='{text}'[v]",
|
$"[0:0]scale={desiredResolution.Width}:{desiredResolution.Height},drawtext={FONT_FILE}:{fontSize}:{FONT_COLOR}:{X}:{Y}:text='{text}'[v]",
|
||||||
"[v]",
|
"[v]",
|
||||||
"1:a");
|
"1:a");
|
||||||
}
|
}
|
||||||
@@ -322,12 +329,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FFmpegProcessBuilder WithFilterComplex()
|
public FFmpegProcessBuilder WithFilterComplex(int videoStreamIndex, int audioStreamIndex)
|
||||||
{
|
{
|
||||||
var videoLabel = "0:v";
|
var videoLabel = $"0:v:{videoStreamIndex}";
|
||||||
var audioLabel = "0:a";
|
var audioLabel = $"0:a:{audioStreamIndex}";
|
||||||
|
|
||||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build();
|
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, audioStreamIndex);
|
||||||
maybeFilter.IfSome(
|
maybeFilter.IfSome(
|
||||||
filter =>
|
filter =>
|
||||||
{
|
{
|
||||||
@@ -364,6 +371,12 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
StandardOutputEncoding = Encoding.UTF8
|
StandardOutputEncoding = Encoding.UTF8
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (_saveReports)
|
||||||
|
{
|
||||||
|
string fileName = Path.Combine(FileSystemLayout.FFmpegReportsFolder, "%p-%t.log");
|
||||||
|
startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string argument in _arguments)
|
foreach (string argument in _arguments)
|
||||||
{
|
{
|
||||||
startInfo.ArgumentList.Add(argument);
|
startInfo.ArgumentList.Add(argument);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -8,34 +9,46 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
{
|
{
|
||||||
public class FFmpegProcessService
|
public class FFmpegProcessService
|
||||||
{
|
{
|
||||||
|
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
||||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
||||||
|
|
||||||
public FFmpegProcessService(FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService) =>
|
public FFmpegProcessService(
|
||||||
|
FFmpegPlaybackSettingsCalculator ffmpegPlaybackSettingsService,
|
||||||
|
IFFmpegStreamSelector ffmpegStreamSelector)
|
||||||
|
{
|
||||||
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
|
_playbackSettingsCalculator = ffmpegPlaybackSettingsService;
|
||||||
|
_ffmpegStreamSelector = ffmpegStreamSelector;
|
||||||
|
}
|
||||||
|
|
||||||
public Process ForPlayoutItem(
|
public async Task<Process> ForPlayoutItem(
|
||||||
string ffmpegPath,
|
string ffmpegPath,
|
||||||
|
bool saveReports,
|
||||||
Channel channel,
|
Channel channel,
|
||||||
MediaVersion version,
|
MediaVersion version,
|
||||||
string path,
|
string path,
|
||||||
DateTimeOffset start,
|
DateTimeOffset start,
|
||||||
DateTimeOffset now)
|
DateTimeOffset now)
|
||||||
{
|
{
|
||||||
|
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
|
||||||
|
MediaStream audioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
|
||||||
|
|
||||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.FFmpegProfile,
|
channel.FFmpegProfile,
|
||||||
version,
|
version,
|
||||||
|
videoStream,
|
||||||
|
audioStream,
|
||||||
start,
|
start,
|
||||||
now);
|
now);
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports)
|
||||||
.WithThreads(playbackSettings.ThreadCount)
|
.WithThreads(playbackSettings.ThreadCount)
|
||||||
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
|
.WithHardwareAcceleration(playbackSettings.HardwareAcceleration)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||||
.WithSeek(playbackSettings.StreamSeek)
|
.WithSeek(playbackSettings.StreamSeek)
|
||||||
.WithInputCodec(path, playbackSettings.HardwareAcceleration, version.VideoCodec);
|
.WithInputCodec(path, playbackSettings.HardwareAcceleration, videoStream.Codec);
|
||||||
|
|
||||||
playbackSettings.ScaledSize.Match(
|
playbackSettings.ScaledSize.Match(
|
||||||
scaledSize =>
|
scaledSize =>
|
||||||
@@ -50,7 +63,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
builder = builder
|
builder = builder
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration).WithFilterComplex();
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
},
|
},
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
@@ -60,19 +74,19 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
.WithFilterComplex();
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
}
|
}
|
||||||
else if (playbackSettings.Deinterlace)
|
else if (playbackSettings.Deinterlace)
|
||||||
{
|
{
|
||||||
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
.WithFilterComplex();
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder = builder
|
builder = builder
|
||||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||||
.WithFilterComplex();
|
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,14 +98,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Process ForOfflineImage(string ffmpegPath, Channel channel, Option<TimeSpan> duration)
|
public Process ForError(string ffmpegPath, Channel channel, Option<TimeSpan> duration, string errorMessage)
|
||||||
{
|
{
|
||||||
FFmpegPlaybackSettings playbackSettings =
|
FFmpegPlaybackSettings playbackSettings =
|
||||||
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
||||||
|
|
||||||
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
|
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false)
|
||||||
.WithThreads(1)
|
.WithThreads(1)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
@@ -99,7 +113,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.WithLoopedImage("Resources/background.png")
|
.WithLoopedImage("Resources/background.png")
|
||||||
.WithLibavfilter()
|
.WithLibavfilter()
|
||||||
.WithInput("anullsrc")
|
.WithInput("anullsrc")
|
||||||
.WithErrorText(desiredResolution, "Channel is Offline")
|
.WithErrorText(desiredResolution, errorMessage)
|
||||||
.WithPixfmt("yuv420p")
|
.WithPixfmt("yuv420p")
|
||||||
.WithPlaybackArgs(playbackSettings)
|
.WithPlaybackArgs(playbackSettings)
|
||||||
.WithMetadata(channel)
|
.WithMetadata(channel)
|
||||||
@@ -114,13 +128,13 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
{
|
{
|
||||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||||
|
|
||||||
return new FFmpegProcessBuilder(ffmpegPath)
|
return new FFmpegProcessBuilder(ffmpegPath, false)
|
||||||
.WithThreads(1)
|
.WithThreads(1)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||||
.WithInfiniteLoop()
|
.WithInfiniteLoop()
|
||||||
.WithConcat($"{scheme}://{host}/ffmpeg/concat/{channel.Number}")
|
.WithConcat($"http://localhost:8409/ffmpeg/concat/{channel.Number}")
|
||||||
.WithMetadata(channel)
|
.WithMetadata(channel)
|
||||||
.WithFormat("mpegts")
|
.WithFormat("mpegts")
|
||||||
.WithPipe()
|
.WithPipe()
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.FFmpeg
|
||||||
|
{
|
||||||
|
public class FFmpegStreamSelector : IFFmpegStreamSelector
|
||||||
|
{
|
||||||
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
private readonly ILogger<FFmpegStreamSelector> _logger;
|
||||||
|
|
||||||
|
public FFmpegStreamSelector(
|
||||||
|
ILogger<FFmpegStreamSelector> logger,
|
||||||
|
IConfigElementRepository configElementRepository)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configElementRepository = configElementRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) =>
|
||||||
|
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
|
||||||
|
|
||||||
|
public async Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version)
|
||||||
|
{
|
||||||
|
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
|
||||||
|
|
||||||
|
string language = (channel.PreferredLanguageCode ?? string.Empty).ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(language))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Channel {Number} has no preferred language code", channel.Number);
|
||||||
|
Option<string> maybeDefaultLanguage = await _configElementRepository.GetValue<string>(
|
||||||
|
ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||||
|
maybeDefaultLanguage.Match(
|
||||||
|
lang => language = lang.ToLowerInvariant(),
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
_logger.LogDebug("FFmpeg has no preferred language code; falling back to {Code}", "eng");
|
||||||
|
language = "eng";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var correctLanguage = audioStreams.Filter(
|
||||||
|
s => string.Equals(
|
||||||
|
s.Language,
|
||||||
|
language,
|
||||||
|
StringComparison.InvariantCultureIgnoreCase)).ToList();
|
||||||
|
if (correctLanguage.Any())
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Found {Count} audio streams with preferred language code {Code}; selecting stream with most channels",
|
||||||
|
correctLanguage.Count,
|
||||||
|
language);
|
||||||
|
|
||||||
|
return correctLanguage.OrderByDescending(s => s.Channels).Head();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Unable to find audio stream with preferred language code {Code}; selecting stream with most channels",
|
||||||
|
language);
|
||||||
|
|
||||||
|
return audioStreams.OrderByDescending(s => s.Channels).Head();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@ namespace ErsatzTV.Core
|
|||||||
|
|
||||||
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
|
public static readonly string PlexSecretsPath = Path.Combine(AppDataFolder, "plex-secrets.json");
|
||||||
|
|
||||||
|
public static readonly string FFmpegReportsFolder = Path.Combine(AppDataFolder, "ffmpeg-reports");
|
||||||
|
public static readonly string SearchIndexFolder = Path.Combine(AppDataFolder, "search-index");
|
||||||
|
|
||||||
public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
|
public static readonly string ArtworkCacheFolder = Path.Combine(AppDataFolder, "cache", "artwork");
|
||||||
|
|
||||||
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
|
public static readonly string PosterCacheFolder = Path.Combine(ArtworkCacheFolder, "posters");
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||||
|
{
|
||||||
|
public interface IFFmpegStreamSelector
|
||||||
|
{
|
||||||
|
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version);
|
||||||
|
Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,12 @@ namespace ErsatzTV.Core.Interfaces.Locking
|
|||||||
public interface IEntityLocker
|
public interface IEntityLocker
|
||||||
{
|
{
|
||||||
event EventHandler OnLibraryChanged;
|
event EventHandler OnLibraryChanged;
|
||||||
|
event EventHandler OnPlexChanged;
|
||||||
bool LockLibrary(int libraryId);
|
bool LockLibrary(int libraryId);
|
||||||
bool UnlockLibrary(int libraryId);
|
bool UnlockLibrary(int libraryId);
|
||||||
bool IsLibraryLocked(int libraryId);
|
bool IsLibraryLocked(int libraryId);
|
||||||
|
bool LockPlex();
|
||||||
|
bool UnlockPlex();
|
||||||
|
bool IsPlexLocked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
|||||||
{
|
{
|
||||||
public interface ILocalFileSystem
|
public interface ILocalFileSystem
|
||||||
{
|
{
|
||||||
|
Unit EnsureFolderExists(string folder);
|
||||||
DateTime GetLastWriteTime(string path);
|
DateTime GetLastWriteTime(string path);
|
||||||
bool IsLibraryPathAccessible(LibraryPath libraryPath);
|
bool IsLibraryPathAccessible(LibraryPath libraryPath);
|
||||||
IEnumerable<string> ListSubdirectories(string folder);
|
IEnumerable<string> ListSubdirectories(string folder);
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using LanguageExt;
|
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||||
{
|
{
|
||||||
public interface ILocalMetadataProvider
|
public interface ILocalMetadataProvider
|
||||||
{
|
{
|
||||||
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
||||||
Task<Unit> RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
Task<bool> RefreshSidecarMetadata(MediaItem mediaItem, string path);
|
||||||
Task<Unit> RefreshSidecarMetadata(Show televisionShow, string showFolder);
|
Task<bool> RefreshSidecarMetadata(Show televisionShow, string showFolder);
|
||||||
Task<Unit> RefreshFallbackMetadata(MediaItem mediaItem);
|
Task<bool> RefreshFallbackMetadata(MediaItem mediaItem);
|
||||||
Task<Unit> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user