Compare commits
60 Commits
v0.5.1-bet
...
v0.5.6-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6326189444 | ||
|
|
198c693208 | ||
|
|
1431b33a98 | ||
|
|
e81a8e58ea | ||
|
|
daf7114ce2 | ||
|
|
8542bc20b1 | ||
|
|
9decb91bf7 | ||
|
|
fcfd579b37 | ||
|
|
e9be182bed | ||
|
|
610e261cd7 | ||
|
|
f65818c838 | ||
|
|
1651d2895e | ||
|
|
b90c536dcb | ||
|
|
5c98eb3df5 | ||
|
|
bdff5eba75 | ||
|
|
7d112eda05 | ||
|
|
4f16431ca0 | ||
|
|
69b39c6940 | ||
|
|
fe7181ea1d | ||
|
|
88b287a094 | ||
|
|
7953e3ad85 | ||
|
|
8ba6374165 | ||
|
|
973dd4b53d | ||
|
|
6facd745ec | ||
|
|
32c4c4ec8b | ||
|
|
ecb6ed37f0 | ||
|
|
2a8bf57930 | ||
|
|
1ebc4b62e3 | ||
|
|
4ae671b633 | ||
|
|
87aa69f4cc | ||
|
|
404ea49e35 | ||
|
|
4ed40acfbe | ||
|
|
17f540dc99 | ||
|
|
780ebc01ee | ||
|
|
0a0fb71b94 | ||
|
|
53d6ecae8d | ||
|
|
837f311ec0 | ||
|
|
a9a89d04ea | ||
|
|
2e1073eb53 | ||
|
|
7687278b80 | ||
|
|
392aebd46f | ||
|
|
0a4f6d9b62 | ||
|
|
d879ce0d0d | ||
|
|
558e8acf5f | ||
|
|
89a2ac9455 | ||
|
|
39c05a24d8 | ||
|
|
78383bd5fa | ||
|
|
d67251bfa0 | ||
|
|
e91ec98007 | ||
|
|
097b8c3d1f | ||
|
|
7284ee9fb7 | ||
|
|
fccb9003a0 | ||
|
|
cc9c2f6ae3 | ||
|
|
ec6eab97b2 | ||
|
|
3ede136ff3 | ||
|
|
7c27241ab6 | ||
|
|
3713711864 | ||
|
|
d755d0ae29 | ||
|
|
c02b83d0d6 | ||
|
|
e250e93a8e |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2021.2.2",
|
||||
"version": "2022.1.0",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
|
||||
4
.github/workflows/artifacts.yml
vendored
4
.github/workflows/artifacts.yml
vendored
@@ -190,10 +190,10 @@ jobs:
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -r "${{ matrix.target}}"
|
||||
run: dotnet restore -r "${{ matrix.target }}"
|
||||
|
||||
- uses: suisei-cn/actions-download-file@v1
|
||||
if: ${{ matrix.kind }} == "windows"
|
||||
if: ${{ matrix.kind == 'windows' }}
|
||||
id: downloadffmpeg
|
||||
name: Download ffmpeg
|
||||
with:
|
||||
|
||||
73
.github/workflows/docker.yml
vendored
73
.github/workflows/docker.yml
vendored
@@ -24,23 +24,38 @@ jobs:
|
||||
name: Build & Publish
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: base
|
||||
path: ''
|
||||
suffix: ''
|
||||
qemu: false
|
||||
- name: nvidia
|
||||
path: 'nvidia/'
|
||||
suffix: '-nvidia'
|
||||
qemu: false
|
||||
- name: vaapi
|
||||
path: 'vaapi/'
|
||||
suffix: '-vaapi'
|
||||
qemu: false
|
||||
- name: arm64
|
||||
path: 'arm64/'
|
||||
suffix: '-arm64'
|
||||
qemu: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx Base
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-base
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
if: ${{ matrix.qemu == true }}
|
||||
|
||||
- name: Set up Docker Buildx NVIDIA
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-nvidia
|
||||
|
||||
- name: Set up Docker Buildx VAAPI
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-vaapi
|
||||
id: docker-buildx
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
@@ -48,41 +63,31 @@ jobs:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Build and push base
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-base.outputs.name }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name != 'arm64' }}
|
||||
|
||||
- name: Build and push nvidia
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/nvidia/Dockerfile
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
platforms: 'linux/arm64'
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
|
||||
|
||||
- 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
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker-vaapi
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-vaapi
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-vaapi
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm64' }}
|
||||
|
||||
96
CHANGELOG.md
96
CHANGELOG.md
@@ -5,6 +5,95 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.5.6-beta] - 2022-05-06
|
||||
### Fixed
|
||||
- Fix processing local movie NFO metadata without a `year` value
|
||||
- Fix processing local movie fallback metadata
|
||||
- Fix search edge case where very recently added items (hours) would not be returned by relative date queries
|
||||
- Fix search index validation on startup; improper validation was causing a rebuild with every startup
|
||||
- Block library scanning until search index has been recreated/upgraded
|
||||
- Fix occasional erroneous log messages when HLS channel playback times out because all clients have left
|
||||
- Fix fallback filler playback
|
||||
- Fix stream continuity when error messages are displayed
|
||||
- Fix duplicate scanning within `Other Video` libraries (i.e. folders would be scanned multiple times)
|
||||
|
||||
### Added
|
||||
- Add `show_genre` and `show_tag` to search index for seasons and episodes
|
||||
- Use `aired` value to source release date from music video nfo metadata
|
||||
- Add NFO metadata support to `Other Video` libraries
|
||||
- `Other Video` NFO metadata must be in the movie NFO metadata format
|
||||
|
||||
## [0.5.5-beta] - 2022-05-03
|
||||
### Fixed
|
||||
- Fix adding episodes with no title to the search index
|
||||
- This behavior was preventing some items from being removed from the trash
|
||||
- Support combination NFO metadata for movies, shows, artists and music videos
|
||||
- Note that ErsatzTV does not scrape any metadata; any URLs after the XML will be ignored
|
||||
- Fix bug causing some Jellyfin and Emby content to incorrectly show as unavailable
|
||||
- Fix extracting embedded `mov_text` subtitles
|
||||
- Properly extract embedded subtitles on playouts where subtitles are only enabled on schedule items (and not on the channel itself)
|
||||
|
||||
### Added
|
||||
- Add experimental `arm64` docker tags (`develop-arm64` and `latest-arm64`)
|
||||
- Use `Sort Title` from Movie NFO metadata if available
|
||||
- Support multiple `Artist` entries in music video NFO metadata
|
||||
|
||||
## [0.5.4-beta] - 2022-04-29
|
||||
### Fixed
|
||||
- Cleanly stop all library scans when service termination is requested
|
||||
- Fix health check crash when trash contains a show or a season
|
||||
- Fix ability of health check crash to crash home page
|
||||
- Remove and ignore Season 0/Specials from Plex shows that have no specials
|
||||
- Automatically delete and rebuild the search index on startup if it has become corrupt
|
||||
- Automatically scan Jellyfin and Emby libraries on startup and periodically
|
||||
- Properly remove un-synchronized Plex, Jellyfin and Emby items from the database and search index
|
||||
- Fix synchronizing movies within a collection from Jellyfin
|
||||
|
||||
### Changed
|
||||
- Update Plex, Jellyfin and Emby movie and show library scanners to share a significant amount of code
|
||||
- This should help maintain feature parity going forward
|
||||
- Optimize search-index rebuilding to complete 100x faster
|
||||
- **No longer use network paths to source content from Jellyfin and Emby**
|
||||
- **If you previously used path replacements to convert network paths to local paths, you should remove them**
|
||||
|
||||
### Added
|
||||
- Add `unavailable` state for Jellyfin and Emby movie and show libraries
|
||||
- Add `height` and `width` to search index for all videos
|
||||
- Add `season_number` and `episode_number` to search index for all episodes
|
||||
- Add `season_number` to search index for seasons
|
||||
- Add `show_title` to search index for seasons and episodes
|
||||
|
||||
## [0.5.3-beta] - 2022-04-24
|
||||
### Fixed
|
||||
- Cleanly stop Plex library scan when service termination is requested
|
||||
- Fix bug introduced with 0.5.2-beta that prevented some Plex content from being played
|
||||
- Fix spammy subtitle error message
|
||||
- Fix generating blur hashes for song backgrounds in Docker
|
||||
|
||||
### Changed
|
||||
- No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk
|
||||
- Instead, a new `unavailable` media state will be used to indicate this condition
|
||||
- After updating mounts, path replacements, etc - a library scan can be used to resolve this state
|
||||
|
||||
## [0.5.2-beta] - 2022-04-22
|
||||
### Fixed
|
||||
- Fix unlocking libraries when scanning fails for any reason
|
||||
- Fix software overlay of actual size watermark
|
||||
|
||||
### Added
|
||||
- Add support for burning in embedded and external text subtitles
|
||||
- **This requires a one-time full library scan, which may take a long time with large libraries.**
|
||||
- Sync Plex, Jellyfin and Emby collections as tags on movies, shows, seasons and episodes
|
||||
- This allows smart collections that use queries like `tag:"Collection Name"`
|
||||
- Note that Emby has an outstanding collections bug that prevents updates when removing items from a collection
|
||||
- Sync Plex labels as tags on movies and shows
|
||||
- This allows smart collections that use queries like `tag:"Plex Label Name"`
|
||||
- Add `Deep Scan` button for Plex libraries
|
||||
- This scanning mode is *slow* but is required to detect some changes like labels
|
||||
|
||||
### Changed
|
||||
- Improve the speed and change detection of the Plex library scanners
|
||||
|
||||
## [0.5.1-beta] - 2022-04-17
|
||||
### Fixed
|
||||
- Fix subtitles edge case with NVENC
|
||||
@@ -1082,7 +1171,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.6-beta...HEAD
|
||||
[0.5.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.5-beta...v0.5.6-beta
|
||||
[0.5.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.4-beta...v0.5.5-beta
|
||||
[0.5.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.3-beta...v0.5.4-beta
|
||||
[0.5.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.2-beta...v0.5.3-beta
|
||||
[0.5.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...v0.5.2-beta
|
||||
[0.5.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
|
||||
[0.5.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
|
||||
[0.4.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.4.2" />
|
||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,4 +11,4 @@ public record ArtistViewModel(
|
||||
List<string> Genres,
|
||||
List<string> Styles,
|
||||
List<string> Moods,
|
||||
List<CultureInfo> Languages);
|
||||
List<CultureInfo> Languages);
|
||||
|
||||
@@ -37,4 +37,4 @@ internal static class Mapper
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Artists;
|
||||
|
||||
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
|
||||
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
|
||||
|
||||
@@ -19,4 +19,4 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
|
||||
a => !string.IsNullOrWhiteSpace(
|
||||
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Artists;
|
||||
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
|
||||
@@ -29,4 +29,4 @@ public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<Artist
|
||||
},
|
||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ public record ChannelViewModel(
|
||||
int? FallbackFillerId,
|
||||
int PlayoutCount,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode);
|
||||
ChannelSubtitleMode SubtitleMode);
|
||||
|
||||
@@ -16,4 +16,4 @@ public record CreateChannel
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(c => PersistChannel(dbContext, c));
|
||||
return await LanguageExtensions.Apply(validation, c => PersistChannel(dbContext, c));
|
||||
}
|
||||
|
||||
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
|
||||
@@ -106,7 +106,9 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(
|
||||
TvContext dbContext,
|
||||
CreateChannel createChannel)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await dbContext.Channels
|
||||
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
|
||||
@@ -169,4 +171,4 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
o => o.ToValidation<BaseError>(
|
||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
|
||||
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
|
||||
|
||||
@@ -20,4 +20,4 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
|
||||
(await _channelRepository.Get(deleteChannel.ChannelId))
|
||||
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ public record UpdateChannel
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Subtitles;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using Channel = ErsatzTV.Core.Domain.Channel;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ChannelWriter<ISubtitleWorkerRequest> _ffmpegWorkerChannel;
|
||||
|
||||
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public UpdateChannelHandler(
|
||||
ChannelWriter<ISubtitleWorkerRequest> ffmpegWorkerChannel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_ffmpegWorkerChannel = ffmpegWorkerChannel;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
UpdateChannel request,
|
||||
@@ -60,11 +69,23 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
c.Artwork.Add(artwork);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (c.SubtitleMode != ChannelSubtitleMode.None)
|
||||
{
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == c.Id);
|
||||
|
||||
foreach (Playout playout in maybePlayout)
|
||||
{
|
||||
await _ffmpegWorkerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectToViewModel(c);
|
||||
}
|
||||
|
||||
@@ -114,4 +135,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred audio language code is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@ internal static class Mapper
|
||||
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
|
||||
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
|
||||
|
||||
@@ -18,4 +18,4 @@ public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi,
|
||||
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
|
||||
return channels.Map(ProjectToResponseModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<Channe
|
||||
|
||||
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
|
||||
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<Chan
|
||||
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Opt
|
||||
|
||||
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
|
||||
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGu
|
||||
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAllForGuide()
|
||||
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
|
||||
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
|
||||
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
|
||||
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
|
||||
|
||||
@@ -47,4 +47,4 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest<Unit>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigE
|
||||
await _configElementRepository.Upsert(request.Key, request.Value);
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class UpdateLibraryRefreshIntervalHandler :
|
||||
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
|
||||
IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
@@ -16,7 +16,10 @@ public class UpdateLibraryRefreshIntervalHandler :
|
||||
UpdateLibraryRefreshInterval request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
|
||||
.MapT(
|
||||
_ => _configElementRepository.Upsert(
|
||||
ConfigElementKey.LibraryRefreshInterval,
|
||||
request.LibraryRefreshInterval))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||
@@ -25,4 +28,4 @@ public class UpdateLibraryRefreshIntervalHandler :
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -56,4 +56,4 @@ public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDays
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Days to build must be greater than zero")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
|
||||
@@ -6,4 +6,4 @@ internal static class Mapper
|
||||
{
|
||||
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
|
||||
new(element.Key, element.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKe
|
||||
GetConfigElementByKey request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetLibraryRefreshInterval : IRequest<int>;
|
||||
public record GetLibraryRefreshInterval : IRequest<int>;
|
||||
|
||||
@@ -13,4 +13,4 @@ public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefres
|
||||
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
||||
.Map(result => result.IfNone(6));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetPlayoutDaysToBuild : IRequest<int>;
|
||||
public record GetPlayoutDaysToBuild : IRequest<int>;
|
||||
|
||||
@@ -13,4 +13,4 @@ public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuil
|
||||
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||
.Map(result => result.IfNone(2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record DisconnectEmby : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
|
||||
public class DisconnectEmbyHandler : IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
@@ -38,4 +38,4 @@ public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Eit
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ using ErsatzTV.Core.Emby;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record SaveEmbySecrets(EmbySecrets Secrets) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -6,7 +6,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
|
||||
public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
@@ -53,4 +53,4 @@ public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, E
|
||||
}
|
||||
|
||||
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyCollections(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
@@ -0,0 +1,73 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmbyCollections, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IEmbyCollectionScanner _scanner;
|
||||
|
||||
public SynchronizeEmbyCollectionsHandler(
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IEmbySecretStore embySecretStore,
|
||||
IEmbyCollectionScanner scanner)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
_scanner = scanner;
|
||||
}
|
||||
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
SynchronizeEmbyCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, ConnectionParameters> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
SynchronizeCollections,
|
||||
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyCollections request) =>
|
||||
MediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveApiKey);
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
|
||||
SynchronizeEmbyCollections request) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
EmbyMediaSource embyMediaSource)
|
||||
{
|
||||
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(connection))
|
||||
.ToValidation<BaseError>("Emby media source requires an active connection");
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
|
||||
ConnectionParameters connectionParameters)
|
||||
{
|
||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, Unit>> SynchronizeCollections(ConnectionParameters connectionParameters) =>
|
||||
await _scanner.ScanCollections(
|
||||
connectionParameters.ActiveConnection.Address,
|
||||
connectionParameters.ApiKey);
|
||||
|
||||
private record ConnectionParameters(EmbyConnection ActiveConnection)
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
|
||||
@@ -8,8 +8,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class
|
||||
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
|
||||
public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
@@ -72,32 +71,33 @@ public class
|
||||
connectionParameters.ActiveConnection.Address,
|
||||
connectionParameters.ApiKey);
|
||||
|
||||
await maybeLibraries.Match(
|
||||
async libraries =>
|
||||
{
|
||||
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
|
||||
.ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.EmbyMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove);
|
||||
if (ids.Any())
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
|
||||
connectionParameters.EmbyMediaSource.ServerName,
|
||||
error.Value);
|
||||
foreach (BaseError error in maybeLibraries.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
|
||||
connectionParameters.EmbyMediaSource.ServerName,
|
||||
error.Value);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
foreach (List<EmbyLibrary> libraries in maybeLibraries.RightToSeq())
|
||||
{
|
||||
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
|
||||
.ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toUpdate = libraries
|
||||
.Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.EmbyMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove,
|
||||
toUpdate);
|
||||
if (ids.Any())
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
@@ -108,4 +108,4 @@ public class
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchroni
|
||||
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Emby;
|
||||
@@ -17,6 +18,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
|
||||
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
|
||||
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _embyWorkerChannel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
|
||||
@@ -31,6 +33,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
|
||||
ILibraryRepository libraryRepository,
|
||||
IEntityLocker entityLocker,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ChannelWriter<IEmbyBackgroundServiceRequest> embyWorkerChannel,
|
||||
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
@@ -40,61 +43,81 @@ public class SynchronizeEmbyLibraryByIdHandler :
|
||||
_libraryRepository = libraryRepository;
|
||||
_entityLocker = entityLocker;
|
||||
_configElementRepository = configElementRepository;
|
||||
_embyWorkerChannel = embyWorkerChannel;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceSynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
CancellationToken cancellationToken) => HandleImpl(request, cancellationToken);
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
SynchronizeEmbyLibraryByIdIfNeeded request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
CancellationToken cancellationToken) => HandleImpl(request, cancellationToken);
|
||||
|
||||
private Task<Either<BaseError, string>>
|
||||
Handle(ISynchronizeEmbyLibraryById request) =>
|
||||
Validate(request)
|
||||
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||
private async Task<Either<BaseError, string>>
|
||||
HandleImpl(ISynchronizeEmbyLibraryById request, CancellationToken cancellationToken)
|
||||
{
|
||||
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
||||
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
||||
Validation<BaseError, RequestParameters> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
parameters => Synchronize(parameters, cancellationToken),
|
||||
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, string>> Synchronize(
|
||||
RequestParameters parameters,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (parameters.Library.MediaKind)
|
||||
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
||||
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _embyMovieLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _embyTelevisionLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath);
|
||||
break;
|
||||
Either<BaseError, Unit> result = parameters.Library.MediaKind switch
|
||||
{
|
||||
LibraryMediaKind.Movies =>
|
||||
await _embyMovieLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
cancellationToken),
|
||||
LibraryMediaKind.Shows =>
|
||||
await _embyTelevisionLibraryScanner.ScanLibrary(
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
};
|
||||
|
||||
if (result.IsRight)
|
||||
{
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
|
||||
await _embyWorkerChannel.WriteAsync(
|
||||
new SynchronizeEmbyCollections(parameters.Library.MediaSourceId),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
return result.Map(_ => parameters.Library.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Skipping unforced scan of emby media library {Name}", parameters.Library.Name);
|
||||
}
|
||||
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
return parameters.Library.Name;
|
||||
}
|
||||
else
|
||||
finally
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping unforced scan of emby media library {Name}",
|
||||
parameters.Library.Name);
|
||||
_entityLocker.UnlockLibrary(parameters.Library.Id);
|
||||
}
|
||||
|
||||
_entityLocker.UnlockLibrary(parameters.Library.Id);
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(
|
||||
@@ -181,4 +204,4 @@ public class SynchronizeEmbyLibraryByIdHandler :
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain;
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
IEmbyBackgroundServiceRequest;
|
||||
|
||||
@@ -32,4 +32,4 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
|
||||
|
||||
return mediaSources;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record UpdateEmbyLibraryPreferences
|
||||
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||
|
||||
@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Search;
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class
|
||||
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
|
||||
UpdateEmbyLibraryPreferencesHandler : IRequestHandler<UpdateEmbyLibraryPreferences,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
@@ -33,4 +33,4 @@ public class
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record UpdateEmbyPathReplacements(
|
||||
int EmbyMediaSourceId,
|
||||
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
List<EmbyPathReplacementItem> PathReplacements) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
|
||||
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
|
||||
|
||||
@@ -4,7 +4,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
|
||||
public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathReplacements,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
@@ -46,4 +46,4 @@ public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateE
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyConnectionParametersViewModel(string Address);
|
||||
public record EmbyConnectionParametersViewModel(string Address);
|
||||
|
||||
@@ -3,5 +3,10 @@ using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
|
||||
: LibraryViewModel("Emby", Id, Name, MediaKind);
|
||||
public record EmbyLibraryViewModel(
|
||||
int Id,
|
||||
string Name,
|
||||
LibraryMediaKind MediaKind,
|
||||
bool ShouldSyncItems,
|
||||
int MediaSourceId)
|
||||
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId);
|
||||
|
||||
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.Emby;
|
||||
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
|
||||
Id,
|
||||
Name,
|
||||
Address);
|
||||
Address);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
|
||||
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
|
||||
|
||||
@@ -11,8 +11,8 @@ internal static class Mapper
|
||||
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
|
||||
|
||||
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems, library.MediaSourceId);
|
||||
|
||||
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
|
||||
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
|
||||
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSour
|
||||
GetAllEmbyMediaSources request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
|
||||
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
|
||||
|
||||
@@ -63,4 +63,4 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
|
||||
private record ConnectionParameters(
|
||||
EmbyMediaSource EmbyMediaSource,
|
||||
EmbyConnection ActiveConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
|
||||
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
|
||||
|
||||
@@ -16,4 +16,4 @@ public class
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
|
||||
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
|
||||
|
||||
@@ -15,4 +15,4 @@ public class
|
||||
GetEmbyMediaSourceById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyPathReplacementsBySourceId
|
||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||
|
||||
@@ -16,4 +16,4 @@ public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyP
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbySecrets : IRequest<EmbySecrets>;
|
||||
public record GetEmbySecrets : IRequest<EmbySecrets>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets
|
||||
|
||||
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
|
||||
_embySecretStore.ReadSecrets();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application;
|
||||
|
||||
public record EntityIdResult(int Id);
|
||||
public record EntityIdResult(int Id);
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="3.0.1" />
|
||||
<PackageReference Include="CliWrap" Version="3.4.2" />
|
||||
<PackageReference Include="CliWrap" Version="3.4.4" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -3,4 +3,4 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record CopyFFmpegProfile
|
||||
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
|
||||
|
||||
@@ -29,4 +29,4 @@ public class
|
||||
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
|
||||
request.NotEmpty(x => x.Name)
|
||||
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,4 @@ public record CreateFFmpegProfile(
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeFramerate,
|
||||
bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
|
||||
bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
|
||||
|
||||
@@ -20,7 +20,7 @@ public class CreateFFmpegProfileHandler :
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
|
||||
return await LanguageExtensions.Apply(validation, profile => PersistFFmpegProfile(dbContext, profile));
|
||||
}
|
||||
|
||||
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
|
||||
@@ -32,7 +32,9 @@ public class CreateFFmpegProfileHandler :
|
||||
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
|
||||
private async Task<Validation<BaseError, FFmpegProfile>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateFFmpegProfile request) =>
|
||||
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
|
||||
.Apply(
|
||||
(name, threadCount, resolutionId) => new FFmpegProfile
|
||||
@@ -70,4 +72,4 @@ public class CreateFFmpegProfileHandler :
|
||||
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId)
|
||||
.MapT(r => r.Id)
|
||||
.Map(o => o.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
|
||||
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
DeleteFFmpegProfile request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -22,11 +22,11 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
|
||||
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p));
|
||||
}
|
||||
|
||||
private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
|
||||
{
|
||||
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
|
||||
await dbContext.SaveChangesAsync();
|
||||
return LanguageExt.Unit.Default;
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
|
||||
@@ -35,4 +35,4 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
|
||||
dbContext.FFmpegProfiles
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId)
|
||||
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {request.FFmpegProfileId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
/// Requests a new ffmpeg profile (view model) that contains
|
||||
/// appropriate default values.
|
||||
/// </summary>
|
||||
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
|
||||
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
|
||||
|
||||
@@ -29,4 +29,4 @@ public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegP
|
||||
|
||||
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@ public record UpdateFFmpegProfile(
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeFramerate,
|
||||
bool DeinterlaceVideo) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
|
||||
bool DeinterlaceVideo) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
|
||||
|
||||
@@ -20,7 +20,7 @@ public class
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
|
||||
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
|
||||
}
|
||||
|
||||
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
|
||||
@@ -77,4 +77,4 @@ public class
|
||||
.SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId)
|
||||
.MapT(r => r.Id)
|
||||
.Map(o => o.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record UpdateFFmpegSettings(FFmpegSettingsViewModel Settings) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -121,4 +121,4 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@ public record FFmpegProfileViewModel(
|
||||
int AudioChannels,
|
||||
int AudioSampleRate,
|
||||
bool NormalizeFramerate,
|
||||
bool DeinterlaceVideo);
|
||||
bool DeinterlaceVideo);
|
||||
|
||||
@@ -12,4 +12,4 @@ public class FFmpegSettingsViewModel
|
||||
public int HlsSegmenterIdleTimeout { get; set; }
|
||||
public int WorkAheadSegmenterLimit { get; set; }
|
||||
public int InitialSegmentCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Application.Resolutions;
|
||||
using ErsatzTV.Core.Api.FFmpegProfiles;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
@@ -26,6 +27,14 @@ internal static class Mapper
|
||||
profile.NormalizeFramerate,
|
||||
profile.DeinterlaceVideo == true);
|
||||
|
||||
internal static FFmpegProfileResponseModel ProjectToResponseModel(FFmpegProfile ffmpegProfile) =>
|
||||
new(
|
||||
ffmpegProfile.Id,
|
||||
ffmpegProfile.Name,
|
||||
$"{ffmpegProfile.Resolution.Width}x{ffmpegProfile.Resolution.Height}",
|
||||
ffmpegProfile.VideoFormat.ToString().ToLowerInvariant(),
|
||||
ffmpegProfile.AudioFormat.ToString().ToLowerInvariant());
|
||||
|
||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
|
||||
public record GetAllFFmpegProfiles : IRequest<List<FFmpegProfileViewModel>>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user