Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
050aaaa288 | ||
|
|
7c07c5f522 | ||
|
|
d8d21996b4 | ||
|
|
e368d4a075 | ||
|
|
466059e2aa | ||
|
|
e951ecb650 | ||
|
|
1d1f53da01 | ||
|
|
a854294cb6 | ||
|
|
f89f3d2225 | ||
|
|
a2700e087c | ||
|
|
34fbfce0a5 | ||
|
|
993293c104 | ||
|
|
ececa62446 | ||
|
|
237729e79d | ||
|
|
9c0ada2df5 | ||
|
|
dee264597b | ||
|
|
a8db294043 | ||
|
|
a2a63e0120 | ||
|
|
c7881aec14 | ||
|
|
558bdcb6b0 | ||
|
|
24f2b4b727 | ||
|
|
667887f387 | ||
|
|
98eb72fcfe | ||
|
|
c2f92fd054 | ||
|
|
f04ddd3a40 | ||
|
|
aa0942384d | ||
|
|
cd100be3a2 | ||
|
|
2b26a5411c | ||
|
|
baf81f31cd | ||
|
|
bfa290790b | ||
|
|
b975922a77 | ||
|
|
1a39978a77 | ||
|
|
436c9119fa | ||
|
|
33642a13ce | ||
|
|
09b349d1cb | ||
|
|
2be729c10e | ||
|
|
0aac702853 | ||
|
|
3f406ac556 | ||
|
|
454e2edf7c | ||
|
|
b3f4fa8c23 | ||
|
|
a6496db58d | ||
|
|
3eed79b5e1 | ||
|
|
79bfba6428 | ||
|
|
9f6d4114a6 | ||
|
|
9809c60924 | ||
|
|
16072fed1c | ||
|
|
3fb6da0754 | ||
|
|
24cdf6295f | ||
|
|
c1b41e2865 | ||
|
|
d249e95f12 | ||
|
|
efae005447 | ||
|
|
cead787c55 | ||
|
|
77a69af1a8 | ||
|
|
8fea24a3a5 | ||
|
|
6b44873474 | ||
|
|
c5ee5903b2 | ||
|
|
526eada48b | ||
|
|
7a0d65a433 | ||
|
|
74c95249c3 | ||
|
|
d4a2197dfa | ||
|
|
633586ddba | ||
|
|
da3e05b231 | ||
|
|
9e6de7e2eb | ||
|
|
4097288fed | ||
|
|
90f775aab4 | ||
|
|
fc33c5cd05 | ||
|
|
37eee73ab7 | ||
|
|
e7ebb32a1d | ||
|
|
9ea4459988 | ||
|
|
745b03af73 | ||
|
|
a62c4ecfcf | ||
|
|
c48f0a7d51 | ||
|
|
f2c105174b | ||
|
|
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 |
@@ -79,3 +79,7 @@ indent_size=2
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
tab_width=4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
+14
-22
@@ -1,5 +1,6 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
name: Build & Publish to Docker Hub
|
||||
needs: build_and_test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no ci]')
|
||||
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -51,16 +52,17 @@ jobs:
|
||||
final="${tag2/prealpha/$short}"
|
||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
- name: Set up Docker Buildx Base
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-base
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Set up Docker Buildx NVIDIA
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-nvidia
|
||||
|
||||
- name: Set up Docker Buildx VAAPI
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-vaapi
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
@@ -71,6 +73,7 @@ jobs:
|
||||
- name: Build and push base
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-base.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
@@ -79,19 +82,11 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:develop
|
||||
jasongdove/ersatztv:${{ github.sha }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
- # Temporary fix
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
- name: Build and push nvidia
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/nvidia/Dockerfile
|
||||
push: true
|
||||
@@ -100,12 +95,11 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:develop-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
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/vaapi/Dockerfile
|
||||
push: true
|
||||
@@ -114,5 +108,3 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:develop-vaapi
|
||||
jasongdove/ersatztv:${{ github.sha }}-vaapi
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Publish docs via GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: ersatztv.org
|
||||
@@ -83,16 +83,17 @@ jobs:
|
||||
echo "GIT_TAG=${tag:1}" >> $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
|
||||
id: builder-base
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
- name: Set up Docker Buildx NVIDIA
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-nvidia
|
||||
|
||||
- name: Set up Docker Buildx VAAPI
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-vaapi
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
@@ -103,6 +104,7 @@ jobs:
|
||||
- name: Build and push base
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-base.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
@@ -111,12 +113,11 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:latest
|
||||
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
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/nvidia/Dockerfile
|
||||
push: true
|
||||
@@ -125,12 +126,11 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:latest-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
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/vaapi/Dockerfile
|
||||
push: true
|
||||
@@ -139,5 +139,3 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:latest-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
|
||||
|
||||
@@ -40,3 +40,6 @@ msbuild.wrn
|
||||
core
|
||||
|
||||
scripts/generate-api-sdk/swagger.json
|
||||
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace ErsatzTV.Application.Artists
|
||||
{
|
||||
public record ArtistViewModel(
|
||||
string Name,
|
||||
string Disambiguation,
|
||||
string Biography,
|
||||
string Thumbnail,
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Styles,
|
||||
List<string> Moods,
|
||||
List<CultureInfo> Languages);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Artists
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
|
||||
{
|
||||
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
|
||||
return new ArtistViewModel(
|
||||
metadata.Title,
|
||||
metadata.Disambiguation,
|
||||
metadata.Biography,
|
||||
Artwork(metadata, ArtworkKind.Thumbnail),
|
||||
Artwork(metadata, ArtworkKind.FanArt),
|
||||
metadata.Genres.Map(g => g.Name).ToList(),
|
||||
metadata.Styles.Map(s => s.Name).ToList(),
|
||||
metadata.Moods.Map(m => m.Name).ToList(),
|
||||
LanguagesForArtist(languages));
|
||||
}
|
||||
|
||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return languages
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Sequence()
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Artists.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
|
||||
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
|
||||
{
|
||||
_artistRepository = artistRepository;
|
||||
_searchRepository = searchRepository;
|
||||
}
|
||||
|
||||
public async Task<Option<ArtistViewModel>> Handle(
|
||||
GetArtistById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
|
||||
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
|
||||
async artist =>
|
||||
{
|
||||
List<string> languages = await _searchRepository.GetLanguagesForArtist(artist);
|
||||
return ProjectToViewModel(artist, languages);
|
||||
},
|
||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@ namespace ErsatzTV.Application.Channels
|
||||
string Name,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Number,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,6 +11,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Channels.Commands
|
||||
{
|
||||
@@ -36,9 +39,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
_channelRepository.Add(c).Map(ProjectToViewModel);
|
||||
|
||||
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(
|
||||
(name, number, ffmpegProfileId) =>
|
||||
(name, number, ffmpegProfileId, preferredLanguageCode) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
@@ -59,7 +63,8 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
Number = number,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork
|
||||
Artwork = artwork,
|
||||
PreferredLanguageCode = preferredLanguageCode
|
||||
};
|
||||
});
|
||||
|
||||
@@ -67,6 +72,13 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
createChannel.NotEmpty(c => c.Name)
|
||||
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
|
||||
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.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)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
|
||||
|
||||
@@ -12,5 +12,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
string Number,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredLanguageCode,
|
||||
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@@ -32,6 +33,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
c.Name = update.Name;
|
||||
c.Number = update.Number;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.PreferredLanguageCode = update.PreferredLanguageCode;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
{
|
||||
@@ -65,8 +67,9 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
|
||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request))
|
||||
.Apply((channelToUpdate, _, _) => channelToUpdate);
|
||||
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
|
||||
ValidatePreferredLanguage(request))
|
||||
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
|
||||
_channelRepository.Get(updateChannel.ChannelId)
|
||||
@@ -79,7 +82,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
|
||||
{
|
||||
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
|
||||
int matchId = match.Map(c => c.Id).IfNone(updateChannel.ChannelId);
|
||||
int matchId = await match.Map(c => c.Id).IfNoneAsync(updateChannel.ChannelId);
|
||||
if (matchId == updateChannel.ChannelId)
|
||||
{
|
||||
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
|
||||
@@ -92,5 +95,12 @@ namespace ErsatzTV.Application.Channels.Commands
|
||||
|
||||
return BaseError.New("Channel number must be unique");
|
||||
}
|
||||
|
||||
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
|
||||
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
|
||||
.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.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.PreferredLanguageCode,
|
||||
channel.StreamingMode);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
|
||||
@@ -3,9 +3,9 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Channels.Queries
|
||||
{
|
||||
@@ -15,7 +15,7 @@ namespace ErsatzTV.Application.Channels.Queries
|
||||
|
||||
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||
|
||||
public Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll().Map(channels => channels.Map(ProjectToViewModel).ToList());
|
||||
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Commands
|
||||
{
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Commands
|
||||
{
|
||||
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(request.Key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = request.Key.Key, Value = request.Value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Configuration
|
||||
{
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
|
||||
new(element.Key, element.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Queries
|
||||
{
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Configuration.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Queries
|
||||
{
|
||||
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<Option<ConfigElementViewModel>> Handle(
|
||||
GetConfigElementByKey request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,20 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<NoWarn>VSTHRD200</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncFixer" Version="1.5.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -11,17 +11,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
}
|
||||
|
||||
@@ -43,19 +43,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
Transcode = request.Transcode,
|
||||
HardwareAcceleration = request.HardwareAcceleration,
|
||||
ResolutionId = resolutionId,
|
||||
NormalizeResolution = request.NormalizeResolution,
|
||||
NormalizeVideo = request.NormalizeVideo,
|
||||
VideoCodec = request.VideoCodec,
|
||||
NormalizeVideoCodec = request.NormalizeVideoCodec,
|
||||
VideoBitrate = request.VideoBitrate,
|
||||
VideoBufferSize = request.VideoBufferSize,
|
||||
AudioCodec = request.AudioCodec,
|
||||
NormalizeAudioCodec = request.NormalizeAudioCodec,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
AudioVolume = request.AudioVolume,
|
||||
NormalizeLoudness = request.NormalizeLoudness,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeAudio = request.NormalizeAudio
|
||||
NormalizeAudio = request.NormalizeAudio,
|
||||
FrameRate = request.FrameRate
|
||||
});
|
||||
|
||||
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
@@ -63,7 +62,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
createFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
|
||||
|
||||
@@ -12,17 +12,16 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
int ResolutionId,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
bool NormalizeAudio,
|
||||
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
}
|
||||
|
||||
@@ -37,19 +37,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
p.Transcode = update.Transcode;
|
||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||
p.ResolutionId = update.ResolutionId;
|
||||
p.NormalizeResolution = update.NormalizeResolution;
|
||||
p.NormalizeVideo = update.NormalizeVideo;
|
||||
p.VideoCodec = update.VideoCodec;
|
||||
p.NormalizeVideoCodec = update.NormalizeVideoCodec;
|
||||
p.VideoBitrate = update.VideoBitrate;
|
||||
p.VideoBufferSize = update.VideoBufferSize;
|
||||
p.AudioCodec = update.AudioCodec;
|
||||
p.NormalizeAudioCodec = update.NormalizeAudioCodec;
|
||||
p.AudioBitrate = update.AudioBitrate;
|
||||
p.AudioBufferSize = update.AudioBufferSize;
|
||||
p.AudioVolume = update.AudioVolume;
|
||||
p.NormalizeLoudness = update.NormalizeLoudness;
|
||||
p.AudioChannels = update.AudioChannels;
|
||||
p.AudioSampleRate = update.AudioSampleRate;
|
||||
p.NormalizeAudio = update.NormalizeAudio;
|
||||
p.FrameRate = update.FrameRate;
|
||||
await _ffmpegProfileRepository.Update(p);
|
||||
return ProjectToViewModel(p);
|
||||
}
|
||||
@@ -69,7 +68,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
|
||||
|
||||
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
updateFFmpegProfile.AtLeast(1)(p => p.ThreadCount);
|
||||
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
|
||||
|
||||
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
|
||||
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
@@ -70,54 +71,35 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
|
||||
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
|
||||
{
|
||||
Option<ConfigElement> ffmpegPath = await _configElementRepository.Get(ConfigElementKey.FFmpegPath);
|
||||
Option<ConfigElement> ffprobePath = await _configElementRepository.Get(ConfigElementKey.FFprobePath);
|
||||
Option<ConfigElement> defaultFFmpegProfileId =
|
||||
await _configElementRepository.Get(ConfigElementKey.FFmpegDefaultProfileId);
|
||||
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
|
||||
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
|
||||
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString());
|
||||
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString());
|
||||
|
||||
ffmpegPath.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Settings.FFmpegPath;
|
||||
_configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement
|
||||
{ Key = ConfigElementKey.FFmpegPath.Key, Value = request.Settings.FFmpegPath };
|
||||
_configElementRepository.Add(ce);
|
||||
});
|
||||
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
|
||||
}
|
||||
|
||||
ffprobePath.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Settings.FFprobePath;
|
||||
_configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement
|
||||
{ Key = ConfigElementKey.FFprobePath.Key, Value = request.Settings.FFprobePath };
|
||||
_configElementRepository.Add(ce);
|
||||
});
|
||||
|
||||
defaultFFmpegProfileId.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Settings.DefaultFFmpegProfileId.ToString();
|
||||
_configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement
|
||||
{
|
||||
Key = ConfigElementKey.FFmpegDefaultProfileId.Key,
|
||||
Value = request.Settings.DefaultFFmpegProfileId.ToString()
|
||||
};
|
||||
_configElementRepository.Add(ce);
|
||||
});
|
||||
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode);
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task Upsert(ConfigElementKey key, string value)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,16 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
bool Transcode,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
ResolutionViewModel Resolution,
|
||||
bool NormalizeResolution,
|
||||
bool NormalizeVideo,
|
||||
string VideoCodec,
|
||||
bool NormalizeVideoCodec,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
string AudioCodec,
|
||||
bool NormalizeAudioCodec,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
int AudioVolume,
|
||||
bool NormalizeLoudness,
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeAudio);
|
||||
bool NormalizeAudio,
|
||||
string FrameRate);
|
||||
}
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
public string FFmpegPath { get; set; }
|
||||
public string FFprobePath { get; set; }
|
||||
public int DefaultFFmpegProfileId { get; set; }
|
||||
public string PreferredLanguageCode { get; set; }
|
||||
public bool SaveReports { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,18 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
||||
profile.Transcode,
|
||||
profile.HardwareAcceleration,
|
||||
Project(profile.Resolution),
|
||||
profile.NormalizeResolution,
|
||||
profile.NormalizeVideo,
|
||||
profile.VideoCodec,
|
||||
profile.NormalizeVideoCodec,
|
||||
profile.VideoBitrate,
|
||||
profile.VideoBufferSize,
|
||||
profile.AudioCodec,
|
||||
profile.NormalizeAudioCodec,
|
||||
profile.AudioBitrate,
|
||||
profile.AudioBufferSize,
|
||||
profile.AudioVolume,
|
||||
profile.NormalizeLoudness,
|
||||
profile.AudioChannels,
|
||||
profile.AudioSampleRate,
|
||||
profile.NormalizeAudio);
|
||||
profile.NormalizeAudio,
|
||||
profile.FrameRate);
|
||||
|
||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||
|
||||
@@ -22,12 +22,18 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
||||
Option<string> ffprobePath = await _configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath);
|
||||
Option<int> defaultFFmpegProfileId =
|
||||
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
|
||||
{
|
||||
FFmpegPath = ffmpegPath.IfNone(string.Empty),
|
||||
FFprobePath = ffprobePath.IfNone(string.Empty),
|
||||
DefaultFFmpegProfileId = defaultFFmpegProfileId.IfNone(0)
|
||||
FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty),
|
||||
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
|
||||
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
||||
SaveReports = await saveReports.IfNoneAsync(false),
|
||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public record UpdateHDHRTunerCount(int TunerCount) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public class UpdateHDHRTunerCountHandler : MediatR.IRequestHandler<UpdateHDHRTunerCount, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public UpdateHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateHDHRTunerCount request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
Optional(request.TunerCount)
|
||||
.Filter(tc => tc > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
}).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public record GetHDHRTunerCount : IRequest<int>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public class GetHDHRTunerCountHandler : IRequestHandler<GetHDHRTunerCount, int>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<int> Handle(GetHDHRTunerCount request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.HDHRTunerCount)
|
||||
.Map(result => result.IfNone(2));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries.Commands
|
||||
@@ -11,9 +13,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
DeleteLocalLibraryPathHandler : MediatR.IRequestHandler<DeleteLocalLibraryPath, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
|
||||
public DeleteLocalLibraryPathHandler(ILibraryRepository libraryRepository, ISearchIndex searchIndex)
|
||||
{
|
||||
_libraryRepository = libraryRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteLocalLibraryPath request,
|
||||
@@ -22,8 +28,13 @@ namespace ErsatzTV.Application.Libraries.Commands
|
||||
.MapT(DoDeletion)
|
||||
.Bind(t => t.ToEitherAsync());
|
||||
|
||||
private Task<Unit> DoDeletion(LibraryPath libraryPath) =>
|
||||
_libraryRepository.DeleteLocalPath(libraryPath.Id).ToUnit();
|
||||
private async Task<Unit> DoDeletion(LibraryPath libraryPath)
|
||||
{
|
||||
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) =>
|
||||
(await _libraryRepository.GetPath(request.LocalLibraryPathId))
|
||||
|
||||
@@ -18,7 +18,11 @@ namespace ErsatzTV.Application.Libraries.Queries
|
||||
|
||||
public Task<List<LibraryViewModel>> Handle(GetAllLibraries request, CancellationToken cancellationToken) =>
|
||||
_libraryRepository.GetAll()
|
||||
.Map(list => list.Filter(ShouldIncludeLibrary).Map(ProjectToViewModel).ToList());
|
||||
.Map(
|
||||
list => list.Filter(ShouldIncludeLibrary)
|
||||
.OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1)
|
||||
.ThenBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel).ToList());
|
||||
|
||||
private static bool ShouldIncludeLibrary(Library library) =>
|
||||
library switch
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
public record LogEntryViewModel(
|
||||
int Id,
|
||||
DateTime Timestamp,
|
||||
string Level,
|
||||
LogEventLevel Level,
|
||||
string Exception,
|
||||
string RenderedMessage,
|
||||
string Properties);
|
||||
string Message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) =>
|
||||
new(
|
||||
internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry)
|
||||
{
|
||||
string message = logEntry.RenderedMessage;
|
||||
if (!string.IsNullOrWhiteSpace(logEntry.Properties))
|
||||
{
|
||||
foreach (KeyValuePair<string, JToken> property in JObject.Parse(logEntry.Properties))
|
||||
{
|
||||
var token = $"{{{property.Key}}}";
|
||||
if (message.Contains(token))
|
||||
{
|
||||
message = message.Replace(token, property.Value.ToString());
|
||||
}
|
||||
|
||||
var destructureToken = $"{{@{property.Key}}}";
|
||||
if (message.Contains(destructureToken))
|
||||
{
|
||||
message = message.Replace(destructureToken, property.Value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(logEntry.Level, out LogEventLevel level))
|
||||
{
|
||||
level = LogEventLevel.Debug;
|
||||
}
|
||||
|
||||
return new LogEntryViewModel(
|
||||
logEntry.Id,
|
||||
logEntry.Timestamp,
|
||||
logEntry.Level,
|
||||
level,
|
||||
logEntry.Exception,
|
||||
logEntry.RenderedMessage,
|
||||
logEntry.Properties);
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) :
|
||||
MediaCardViewModel(Id, Name, Role, Name, Thumb);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ArtistCardResultsViewModel(
|
||||
int Count,
|
||||
List<ArtistCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ArtistCardViewModel
|
||||
(int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||
ArtistId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster);
|
||||
}
|
||||
@@ -7,7 +7,9 @@ namespace ErsatzTV.Application.MediaCards
|
||||
List<MovieCardViewModel> MovieCards,
|
||||
List<TelevisionShowCardViewModel> ShowCards,
|
||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards)
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||
List<ArtistCardViewModel> ArtistCards,
|
||||
List<MusicVideoCardViewModel> MusicVideoCards)
|
||||
{
|
||||
public bool UseCustomPlaybackOrder { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using static LanguageExt.Prelude;
|
||||
@@ -52,6 +51,23 @@ namespace ErsatzTV.Application.MediaCards
|
||||
movieMetadata.SortTitle,
|
||||
GetPoster(movieMetadata));
|
||||
|
||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||
new(
|
||||
musicVideoMetadata.MusicVideoId,
|
||||
musicVideoMetadata.Title,
|
||||
musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title,
|
||||
musicVideoMetadata.SortTitle,
|
||||
musicVideoMetadata.Plot,
|
||||
GetThumbnail(musicVideoMetadata));
|
||||
|
||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||
new(
|
||||
artistMetadata.ArtistId,
|
||||
artistMetadata.Title,
|
||||
artistMetadata.Disambiguation,
|
||||
artistMetadata.SortTitle,
|
||||
GetThumbnail(artistMetadata));
|
||||
|
||||
internal static CollectionCardResultsViewModel
|
||||
ProjectToViewModel(Collection collection) =>
|
||||
new(
|
||||
@@ -64,18 +80,19 @@ namespace ErsatzTV.Application.MediaCards
|
||||
collection.MediaItems.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
||||
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
internal static ActorCardViewModel ProjectToViewModel(Actor actor) =>
|
||||
new(actor.Id, actor.Name, actor.Role, actor.Artwork?.Path);
|
||||
|
||||
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) =>
|
||||
new(
|
||||
items.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(),
|
||||
items.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList());
|
||||
|
||||
private static string GetSeasonName(int number) =>
|
||||
number == 0 ? "Specials" : $"Season {number}";
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards);
|
||||
public record MovieCardResultsViewModel(int Count, List<MovieCardViewModel> Cards, Option<SearchPageMap> PageMap);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MusicVideoCardResultsViewModel(
|
||||
int Count,
|
||||
List<MusicVideoCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MusicVideoCardViewModel
|
||||
(
|
||||
int MusicVideoId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string SortTitle,
|
||||
string Plot,
|
||||
string Poster) : MediaCardViewModel(
|
||||
MusicVideoId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public record GetMusicVideoCards
|
||||
(int ArtistId, int PageNumber, int PageSize) : IRequest<MusicVideoCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class GetMusicVideoCardsHandler : IRequestHandler<GetMusicVideoCards, MusicVideoCardResultsViewModel>
|
||||
{
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
|
||||
public GetMusicVideoCardsHandler(IMusicVideoRepository musicVideoRepository) =>
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
|
||||
public async Task<MusicVideoCardResultsViewModel> Handle(
|
||||
GetMusicVideoCards request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _musicVideoRepository.GetMusicVideoCount(request.ArtistId);
|
||||
|
||||
List<MusicVideoCardViewModel> results = await _musicVideoRepository
|
||||
.GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new MusicVideoCardResultsViewModel(count, results, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record TelevisionShowCardResultsViewModel(int Count, List<TelevisionShowCardViewModel> Cards);
|
||||
public record TelevisionShowCardResultsViewModel(
|
||||
int Count,
|
||||
List<TelevisionShowCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddArtistToCollection
|
||||
(int CollectionId, int ArtistId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
AddArtistToCollectionHandler : MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public AddArtistToCollectionHandler(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_artistRepository = artistRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
AddArtistToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddArtistRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddArtistRequest(AddArtistToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ArtistId))
|
||||
{
|
||||
// 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 async Task<Validation<BaseError, Unit>> Validate(AddArtistToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateArtist(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddArtistToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateArtist(AddArtistToCollection request) =>
|
||||
LoadArtist(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
|
||||
private Task<Option<Artist>> LoadArtist(AddArtistToCollection request) =>
|
||||
_artistRepository.GetArtist(request.ArtistId);
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,10 @@ using LanguageExt;
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddItemsToCollection
|
||||
(int CollectionId, List<int> MovieIds, List<int> ShowIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
(
|
||||
int CollectionId,
|
||||
List<int> MovieIds,
|
||||
List<int> ShowIds,
|
||||
List<int> ArtistIds,
|
||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
|
||||
private async Task<Unit> ApplyAddItemsRequest(AddItemsToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItems(
|
||||
request.CollectionId,
|
||||
request.MovieIds.Append(request.ShowIds).ToList()))
|
||||
var allItems = request.MovieIds
|
||||
.Append(request.ShowIds)
|
||||
.Append(request.ArtistIds)
|
||||
.Append(request.MusicVideoIds)
|
||||
.ToList();
|
||||
|
||||
if (await _mediaCollectionRepository.AddMediaItems(request.CollectionId, allItems))
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddMusicVideoToCollection
|
||||
(int CollectionId, int MusicVideoId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
AddMusicVideoToCollectionHandler : MediatR.IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
|
||||
public AddMusicVideoToCollectionHandler(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMusicVideoRepository musicVideoRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
AddMusicVideoToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddMusicVideoRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddMusicVideoRequest(AddMusicVideoToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.MusicVideoId))
|
||||
{
|
||||
// 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 async Task<Validation<BaseError, Unit>> Validate(AddMusicVideoToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateMusicVideo(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddMusicVideoToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateMusicVideo(AddMusicVideoToCollection request) =>
|
||||
LoadMusicVideo(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
|
||||
private Task<Option<MusicVideo>> LoadMusicVideo(AddMusicVideoToCollection request) =>
|
||||
_musicVideoRepository.GetMusicVideo(request.MusicVideoId);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -39,7 +39,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
Option<CollectionItem> maybeCollectionItem =
|
||||
c.CollectionItems.FirstOrDefault(ci => ci.MediaItemId == updateItem.MediaItemId);
|
||||
|
||||
maybeCollectionItem.IfSome(ci => ci.CustomIndex = updateItem.CustomIndex);
|
||||
await maybeCollectionItem.IfSomeAsync(ci => ci.CustomIndex = updateItem.CustomIndex);
|
||||
}
|
||||
|
||||
if (await _mediaCollectionRepository.Update(c))
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
private async Task<Unit> ApplyUpdateRequest(Collection c, UpdateCollection request)
|
||||
{
|
||||
c.Name = request.Name;
|
||||
request.UseCustomPlaybackOrder.IfSome(
|
||||
await request.UseCustomPlaybackOrder.IfSomeAsync(
|
||||
useCustomPlaybackOrder => c.UseCustomPlaybackOrder = useCustomPlaybackOrder);
|
||||
if (await _mediaCollectionRepository.Update(c) && request.UseCustomPlaybackOrder.IsSome)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections
|
||||
{
|
||||
public record PagedMediaCollectionsViewModel(int TotalCount, List<MediaCollectionViewModel> Page);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public record GetPagedCollections(int PageNum, int PageSize) : IRequest<PagedMediaCollectionsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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.MediaCollections.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, PagedMediaCollectionsViewModel>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public GetPagedCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
|
||||
public async Task<PagedMediaCollectionsViewModel> Handle(
|
||||
GetPagedCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _mediaCollectionRepository.CountAllCollections();
|
||||
|
||||
List<MediaCollectionViewModel> page = await _mediaCollectionRepository
|
||||
.GetPagedCollections(request.PageNum, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new PagedMediaCollectionsViewModel(count, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems
|
||||
{
|
||||
@@ -8,59 +7,6 @@ namespace ErsatzTV.Application.MediaItems
|
||||
internal static MediaItemViewModel ProjectToViewModel(MediaItem mediaItem) =>
|
||||
new(mediaItem.Id, mediaItem.LibraryPathId);
|
||||
|
||||
internal static MediaItemSearchResultViewModel ProjectToSearchViewModel(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
Episode e => ProjectToSearchViewModel(e),
|
||||
Movie m => ProjectToSearchViewModel(m),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Episode mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
GetLibraryName(mediaItem),
|
||||
"TV Show",
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
|
||||
private static MediaItemSearchResultViewModel ProjectToSearchViewModel(Movie mediaItem) =>
|
||||
new(
|
||||
mediaItem.Id,
|
||||
GetLibraryName(mediaItem),
|
||||
"Movie",
|
||||
GetDisplayTitle(mediaItem),
|
||||
GetDisplayDuration(mediaItem));
|
||||
|
||||
|
||||
private static string GetDisplayTitle(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
|
||||
.IfNone("[unknown episode]"),
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
private static string GetDisplayDuration(MediaItem mediaItem)
|
||||
{
|
||||
MediaVersion version = mediaItem switch
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
return string.Format(
|
||||
version.Duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
version.Duration);
|
||||
}
|
||||
|
||||
// TODO: fix this when search is reimplemented
|
||||
private static string GetLibraryName(MediaItem item) =>
|
||||
"Library Name";
|
||||
|
||||
public static NamedMediaItemViewModel ProjectToViewModel(Show show) =>
|
||||
new(show.Id, show.ShowMetadata.HeadOrNone().Map(sm => $"{sm?.Title} ({sm?.Year})").IfNone("???"));
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<CultureInfo>();
|
||||
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
List<string> allLanguageCodes = await _mediaItemRepository.GetAllLanguageCodes();
|
||||
foreach (string code in allLanguageCodes)
|
||||
{
|
||||
Option<CultureInfo> maybeCulture = allCultures.Find(
|
||||
ci => string.Equals(code, ci.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||
await maybeCulture.IfSomeAsync(cultureInfo => result.Add(cultureInfo));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -21,7 +23,9 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<ScanLocalLibraryHandler> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IMovieFolderScanner _movieFolderScanner;
|
||||
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
||||
|
||||
public ScanLocalLibraryHandler(
|
||||
@@ -29,14 +33,18 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
IConfigElementRepository configElementRepository,
|
||||
IMovieFolderScanner movieFolderScanner,
|
||||
ITelevisionFolderScanner televisionFolderScanner,
|
||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||
IEntityLocker entityLocker,
|
||||
IMediator mediator,
|
||||
ILogger<ScanLocalLibraryHandler> logger)
|
||||
{
|
||||
_libraryRepository = libraryRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_movieFolderScanner = movieFolderScanner;
|
||||
_televisionFolderScanner = televisionFolderScanner;
|
||||
_musicVideoFolderScanner = musicVideoFolderScanner;
|
||||
_entityLocker = entityLocker;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@@ -58,32 +66,62 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
|
||||
|
||||
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
for (var i = 0; i < localLibrary.Paths.Count; i++)
|
||||
{
|
||||
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
||||
LibraryPath libraryPath = localLibrary.Paths[i];
|
||||
|
||||
decimal progressMin = (decimal) i / localLibrary.Paths.Count;
|
||||
decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count;
|
||||
|
||||
var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
{
|
||||
switch (localLibrary.MediaKind)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
||||
await _movieFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath);
|
||||
await _televisionFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
case LibraryMediaKind.MusicVideos:
|
||||
await _musicVideoFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
}
|
||||
|
||||
libraryPath.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(libraryPath);
|
||||
}
|
||||
|
||||
localLibrary.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(localLibrary);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping unforced scan of library {Name}",
|
||||
localLibrary.Name);
|
||||
await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax));
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"Scan of library {Name} completed in {Duration}",
|
||||
localLibrary.Name,
|
||||
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||
|
||||
await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0));
|
||||
|
||||
_entityLocker.UnlockLibrary(localLibrary.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Movies
|
||||
@@ -16,7 +20,27 @@ namespace ErsatzTV.Application.Movies
|
||||
Artwork(metadata, ArtworkKind.Poster),
|
||||
Artwork(metadata, ArtworkKind.FanArt),
|
||||
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(),
|
||||
LanguagesForMovie(movie),
|
||||
metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id).Map(MediaCards.Mapper.ProjectToViewModel)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static List<CultureInfo> LanguagesForMovie(Movie movie)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return movie.MediaVersions
|
||||
.Map(mv => mv.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).Map(s => s.Language))
|
||||
.Flatten()
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Sequence()
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
|
||||
namespace ErsatzTV.Application.Movies
|
||||
{
|
||||
@@ -9,5 +11,8 @@ namespace ErsatzTV.Application.Movies
|
||||
string Poster,
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Tags);
|
||||
List<string> Tags,
|
||||
List<string> Studios,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(CreatePlayout request) =>
|
||||
(await ChannelMustExist(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
|
||||
(await ValidateChannel(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
|
||||
.Apply(
|
||||
(channel, programSchedule, playoutType) => new Playout
|
||||
{
|
||||
@@ -57,10 +57,19 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
ProgramSchedulePlayoutType = playoutType
|
||||
});
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ValidateChannel(CreatePlayout createPlayout) =>
|
||||
ChannelMustExist(createPlayout).BindT(ChannelMustNotHavePlayouts);
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustExist(CreatePlayout createPlayout) =>
|
||||
(await _channelRepository.Get(createPlayout.ChannelId))
|
||||
.ToValidation<BaseError>("Channel does not exist.");
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustNotHavePlayouts(Channel channel) =>
|
||||
Optional(await _channelRepository.CountPlayouts(channel.Id))
|
||||
.Filter(count => count == 0)
|
||||
.Map(_ => channel)
|
||||
.ToValidation<BaseError>("Channel already has one playout.");
|
||||
|
||||
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
CreatePlayout createPlayout) =>
|
||||
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId))
|
||||
|
||||
@@ -24,15 +24,28 @@ namespace ErsatzTV.Application.Playouts
|
||||
private static PlayoutProgramScheduleViewModel Project(ProgramSchedule programSchedule) =>
|
||||
new(programSchedule.Id, programSchedule.Name);
|
||||
|
||||
private static string GetDisplayTitle(MediaItem mediaItem) =>
|
||||
mediaItem switch
|
||||
private static string GetDisplayTitle(MediaItem mediaItem)
|
||||
{
|
||||
switch (mediaItem)
|
||||
{
|
||||
Episode e => e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{em.Title} - s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00}")
|
||||
.IfNone("[unknown episode]"),
|
||||
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]"),
|
||||
_ => string.Empty
|
||||
};
|
||||
case Episode e:
|
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
|
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
|
||||
return e.EpisodeMetadata.HeadOrNone()
|
||||
.Map(em => $"{showTitle}s{e.Season.SeasonNumber:00}e{e.EpisodeNumber:00} - {em.Title}")
|
||||
.IfNone("[unknown episode]");
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||
case MusicVideo mv:
|
||||
string artistName = mv.Artist.ArtistMetadata.HeadOrNone()
|
||||
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
|
||||
return mv.MusicVideoMetadata.HeadOrNone()
|
||||
.Map(mvm => $"{artistName}{mvm.Title}")
|
||||
.IfNone("[unknown music video]");
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDisplayDuration(MediaItem mediaItem)
|
||||
{
|
||||
@@ -40,6 +53,7 @@ namespace ErsatzTV.Application.Playouts
|
||||
{
|
||||
Movie m => m.MediaVersions.Head(),
|
||||
Episode e => e.MediaVersions.Head(),
|
||||
MusicVideo mv => mv.MediaVersions.Head(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mediaItem))
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Threading;
|
||||
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
|
||||
@@ -13,20 +15,24 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public SignOutOfPlexHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexSecretStore plexSecretStore,
|
||||
IEntityLocker entityLocker)
|
||||
IEntityLocker entityLocker,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_entityLocker = entityLocker;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(SignOutOfPlex request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _mediaSourceRepository.DeleteAllPlex();
|
||||
List<int> ids = await _mediaSourceRepository.DeleteAllPlex();
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
await _plexSecretStore.DeleteAll();
|
||||
_entityLocker.UnlockPlex();
|
||||
|
||||
|
||||
@@ -78,10 +78,10 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
var existing = connectionParameters.PlexMediaSource.Libraries.OfType<PlexLibrary>().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();
|
||||
connectionParameters.PlexMediaSource.Libraries.AddRange(toAdd);
|
||||
toRemove.ForEach(c => connectionParameters.PlexMediaSource.Libraries.Remove(c));
|
||||
|
||||
return _mediaSourceRepository.Update(connectionParameters.PlexMediaSource);
|
||||
return _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.PlexMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove);
|
||||
},
|
||||
error =>
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
|
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IPlexBackgroundServiceRequest
|
||||
{
|
||||
int PlexLibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
|
||||
@@ -31,6 +32,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IPlexSecretStore plexSecretStore,
|
||||
IPlexMovieLibraryScanner plexMovieLibraryScanner,
|
||||
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
|
||||
ILibraryRepository libraryRepository,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexLibraryByIdHandler> logger)
|
||||
{
|
||||
@@ -38,6 +40,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_plexMovieLibraryScanner = plexMovieLibraryScanner;
|
||||
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner;
|
||||
_libraryRepository = libraryRepository;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -78,7 +81,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
}
|
||||
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _mediaSourceRepository.Update(parameters.Library);
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
@@ -19,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _channel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||
|
||||
@@ -26,12 +28,14 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexTvApiClient plexTvApiClient,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker)
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, List<PlexMediaSource>>> Handle(
|
||||
@@ -47,6 +51,14 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
await SynchronizeServer(allExisting, server);
|
||||
}
|
||||
|
||||
// delete removed servers
|
||||
foreach (PlexMediaSource removed in allExisting.Filter(
|
||||
s => servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier)))
|
||||
{
|
||||
_logger.LogWarning("Deleting removed Plex server {ServerName}!", removed.Id.ToString());
|
||||
await _mediaSourceRepository.DeletePlex(removed);
|
||||
}
|
||||
|
||||
foreach (PlexMediaSource mediaSource in await _mediaSourceRepository.GetAllPlex())
|
||||
{
|
||||
await _channel.WriteAsync(new SynchronizePlexLibraries(mediaSource.Id));
|
||||
@@ -57,43 +69,32 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
return allExisting;
|
||||
}
|
||||
|
||||
private async Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
private Task SynchronizeServer(List<PlexMediaSource> allExisting, PlexMediaSource server)
|
||||
{
|
||||
Option<PlexMediaSource> maybeExisting =
|
||||
allExisting.Find(s => s.ClientIdentifier == server.ClientIdentifier);
|
||||
await maybeExisting.Match(
|
||||
return maybeExisting.Match(
|
||||
existing =>
|
||||
{
|
||||
existing.Platform = server.Platform;
|
||||
existing.PlatformVersion = server.PlatformVersion;
|
||||
existing.ProductVersion = server.ProductVersion;
|
||||
existing.ServerName = server.ServerName;
|
||||
MergeConnections(existing.Connections, server.Connections);
|
||||
if (existing.Connections.Any() && existing.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
existing.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
return _mediaSourceRepository.Update(existing);
|
||||
var toAdd = server.Connections
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
if (server.Connections.Any())
|
||||
{
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server);
|
||||
await _mediaSourceRepository.Add(server);
|
||||
});
|
||||
}
|
||||
|
||||
private void MergeConnections(
|
||||
List<PlexConnection> existing,
|
||||
List<PlexConnection> incoming)
|
||||
{
|
||||
var toAdd = incoming.Filter(connection => existing.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Filter(connection => incoming.All(c => c.Uri != connection.Uri)).ToList();
|
||||
existing.AddRange(toAdd);
|
||||
toRemove.ForEach(c => existing.Remove(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
@@ -13,16 +14,23 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public UpdatePlexLibraryPreferencesHandler(IMediaSourceRepository mediaSourceRepository) =>
|
||||
public UpdatePlexLibraryPreferencesHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ISearchIndex searchIndex)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_searchIndex = searchIndex;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdatePlexLibraryPreferences request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
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);
|
||||
await _mediaSourceRepository.EnablePlexLibrarySync(toEnable);
|
||||
|
||||
@@ -6,7 +6,6 @@ using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
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 toUpdate = incoming.Except(toAdd).ToList();
|
||||
|
||||
plexMediaSource.PathReplacements.AddRange(toAdd);
|
||||
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();
|
||||
return _mediaSourceRepository.UpdatePathReplacements(plexMediaSource.Id, toAdd, toUpdate, toRemove);
|
||||
}
|
||||
|
||||
private static PlexPathReplacement Project(PlexPathReplacementItem vm) =>
|
||||
|
||||
@@ -16,5 +16,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IRequest<Either<BaseError, ProgramScheduleItemViewModel>>, IProgramScheduleItemRequest;
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MultipleCount { get; }
|
||||
TimeSpan? PlayoutDuration { get; }
|
||||
bool? OfflineTail { get; }
|
||||
string CustomTitle { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.One => new ProgramScheduleItemOne
|
||||
{
|
||||
@@ -109,7 +110,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
StartTime = item.StartTime,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId
|
||||
MediaItemId = item.MediaItemId,
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Multiple => new ProgramScheduleItemMultiple
|
||||
{
|
||||
@@ -119,7 +121,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
Count = item.MultipleCount.GetValueOrDefault()
|
||||
Count = item.MultipleCount.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
PlayoutMode.Duration => new ProgramScheduleItemDuration
|
||||
{
|
||||
@@ -130,7 +133,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
CollectionId = item.CollectionId,
|
||||
MediaItemId = item.MediaItemId,
|
||||
PlayoutDuration = item.PlayoutDuration.GetValueOrDefault(),
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault()
|
||||
OfflineTail = item.OfflineTail.GetValueOrDefault(),
|
||||
CustomTitle = item.CustomTitle
|
||||
},
|
||||
_ => throw new NotSupportedException($"Unsupported playout mode {item.PlayoutMode}")
|
||||
};
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace ErsatzTV.Application.ProgramSchedules.Commands
|
||||
int? MediaItemId,
|
||||
int? MultipleCount,
|
||||
TimeSpan? PlayoutDuration,
|
||||
bool? OfflineTail) : IProgramScheduleItemRequest;
|
||||
bool? OfflineTail,
|
||||
string CustomTitle) : IProgramScheduleItemRequest;
|
||||
|
||||
public record ReplaceProgramScheduleItems
|
||||
(int ProgramScheduleId, List<ReplaceProgramScheduleItem> Items) : IRequest<
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
_ => null
|
||||
},
|
||||
duration.PlayoutDuration,
|
||||
duration.OfflineTail),
|
||||
duration.OfflineTail,
|
||||
duration.CustomTitle),
|
||||
ProgramScheduleItemFlood flood =>
|
||||
new ProgramScheduleItemFloodViewModel(
|
||||
flood.Id,
|
||||
@@ -44,7 +45,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
flood.CustomTitle),
|
||||
ProgramScheduleItemMultiple multiple =>
|
||||
new ProgramScheduleItemMultipleViewModel(
|
||||
multiple.Id,
|
||||
@@ -61,7 +63,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
},
|
||||
multiple.Count),
|
||||
multiple.Count,
|
||||
multiple.CustomTitle),
|
||||
ProgramScheduleItemOne one =>
|
||||
new ProgramScheduleItemOneViewModel(
|
||||
one.Id,
|
||||
@@ -77,7 +80,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show),
|
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season),
|
||||
_ => null
|
||||
}),
|
||||
},
|
||||
one.CustomTitle),
|
||||
_ => throw new NotSupportedException(
|
||||
$"Unsupported program schedule item type {programScheduleItem.GetType().Name}")
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
TimeSpan playoutDuration,
|
||||
bool offlineTail) : base(
|
||||
bool offlineTail,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -24,7 +25,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Duration,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
PlayoutDuration = playoutDuration;
|
||||
OfflineTail = offlineTail;
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Flood,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
int count) : base(
|
||||
int count,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -23,7 +24,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.Multiple,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem) =>
|
||||
mediaItem,
|
||||
customTitle) =>
|
||||
Count = count;
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
@@ -14,7 +14,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
TimeSpan? startTime,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
NamedMediaItemViewModel mediaItem) : base(
|
||||
NamedMediaItemViewModel mediaItem,
|
||||
string customTitle) : base(
|
||||
id,
|
||||
index,
|
||||
startType,
|
||||
@@ -22,7 +23,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode.One,
|
||||
collectionType,
|
||||
collection,
|
||||
mediaItem)
|
||||
mediaItem,
|
||||
customTitle)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace ErsatzTV.Application.ProgramSchedules
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
MediaCollectionViewModel Collection,
|
||||
NamedMediaItemViewModel MediaItem)
|
||||
NamedMediaItemViewModel MediaItem,
|
||||
string CustomTitle)
|
||||
{
|
||||
public string Name => CollectionType switch
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Commands
|
||||
{
|
||||
public record RebuildSearchIndex : MediatR.IRequest<Unit>, IBackgroundServiceRequest;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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.Metadata;
|
||||
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 ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
|
||||
public RebuildSearchIndexHandler(
|
||||
ISearchIndex searchIndex,
|
||||
ISearchRepository searchRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<RebuildSearchIndexHandler> logger)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_logger = logger;
|
||||
_searchRepository = searchRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_localFileSystem = localFileSystem;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
|
||||
{
|
||||
bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder);
|
||||
|
||||
await _searchIndex.Initialize(_localFileSystem);
|
||||
|
||||
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(_searchRepository, 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,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexAllItems(string Query) : IRequest<SearchResultAllItemsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class
|
||||
QuerySearchIndexAllItemsHandler : IRequestHandler<QuerySearchIndexAllItems, SearchResultAllItemsViewModel>
|
||||
{
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexAllItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
|
||||
|
||||
public async Task<SearchResultAllItemsViewModel> Handle(
|
||||
QuerySearchIndexAllItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<int> movieIds = await _searchIndex.Search($"type:movie AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> showIds = await _searchIndex.Search($"type:show AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> artistIds = await _searchIndex.Search($"type:artist AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> musicVideoIds = await _searchIndex.Search($"type:music_video AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
|
||||
return new SearchResultAllItemsViewModel(movieIds, showIds, artistIds, musicVideoIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user