Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda0318868 | ||
|
|
ac413f731a |
@@ -3,11 +3,10 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2025.1.4",
|
||||
"version": "2024.1.1",
|
||||
"commands": [
|
||||
"jb"
|
||||
],
|
||||
"rollForward": false
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,7 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-arm
|
||||
- os: ubuntu-24.04-arm
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-arm64
|
||||
- os: windows-latest
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
id: downloadffmpeg
|
||||
name: Download ffmpeg
|
||||
with:
|
||||
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/7.1.1/ffmpeg-n7.1.1-56-gc2184b65d2-win64-gpl-7.1.zip"
|
||||
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/7.1.1/ffmpeg-n7.1.1-22-g0f1fe3d153-win64-gpl-7.1.zip"
|
||||
target: ffmpeg/
|
||||
|
||||
- name: Build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Build & Publish to Docker Hub
|
||||
name: Build & Publish to Docker Hub
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
@@ -22,29 +22,23 @@ on:
|
||||
jobs:
|
||||
build_and_push:
|
||||
name: Build & Publish
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: amd64
|
||||
os: ubuntu-latest
|
||||
- name: base
|
||||
path: ''
|
||||
suffix: '-amd64'
|
||||
suffix: ''
|
||||
qemu: false
|
||||
platform: 'linux/amd64'
|
||||
- name: arm32v7
|
||||
os: ubuntu-latest
|
||||
path: 'arm32v7/'
|
||||
suffix: '-arm'
|
||||
qemu: true
|
||||
platform: 'linux/arm/v7'
|
||||
- name: arm64
|
||||
os: ubuntu-24.04-arm
|
||||
path: 'arm64/'
|
||||
suffix: '-arm64'
|
||||
qemu: true
|
||||
platform: 'linux/arm64'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -79,8 +73,23 @@ jobs:
|
||||
context: .
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
provenance: false
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}
|
||||
if: ${{ matrix.name != 'arm64' && matrix.name != 'arm32v7' }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
platforms: 'linux/arm64'
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
@@ -88,44 +97,21 @@ jobs:
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm64' }}
|
||||
|
||||
merge_manifests:
|
||||
name: Merge Manifests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build_and_push
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifests
|
||||
run: |
|
||||
docker manifest create jasongdove/ersatztv:${{ inputs.base_version }} \
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-amd64 \
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-arm64 \
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-arm
|
||||
docker manifest push jasongdove/ersatztv:${{ inputs.base_version }}
|
||||
docker manifest create jasongdove/ersatztv:${{ inputs.tag_version }} \
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-amd64 \
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-arm64 \
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-arm
|
||||
docker manifest push jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }} \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}-amd64 \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}-arm64 \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}-arm
|
||||
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}
|
||||
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }} \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}-amd64 \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}-arm64 \
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}-arm
|
||||
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
platforms: 'linux/arm/v7'
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm32v7' }}
|
||||
|
||||
@@ -33,18 +33,7 @@ jobs:
|
||||
cd ErsatzTV-Windows
|
||||
cargo build --release --all-features
|
||||
build_and_test_linux:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: linux-x64
|
||||
- os: ubuntu-latest
|
||||
target: linux-musl-x64
|
||||
- os: ubuntu-latest
|
||||
target: linux-arm
|
||||
- os: ubuntu-24.04-arm
|
||||
target: linux-arm64
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
@@ -58,13 +47,13 @@ jobs:
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -p:RestoreEnablePackagePruning=true -r "${{ matrix.target }}"
|
||||
run: dotnet restore -p:RestoreEnablePackagePruning=true -r linux-x64
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build ErsatzTV/ErsatzTV.csproj --runtime "${{ matrix.target }}" --configuration Release --no-restore && dotnet build --configuration Release --no-restore
|
||||
run: dotnet build ErsatzTV/ErsatzTV.csproj --runtime linux-x64 --configuration Release --no-restore && dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
|
||||
|
||||
@@ -40,7 +40,6 @@ msbuild.wrn
|
||||
core
|
||||
|
||||
scripts/generate-api-sdk/swagger.json
|
||||
scripts/download-test-content.sh
|
||||
|
||||
docker-compose.override.yml
|
||||
|
||||
|
||||
+1
-89
@@ -4,8 +4,6 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [25.2.0] - 2025-07-24
|
||||
### Added
|
||||
- Add new channel stream (audio and subtitle) selector system
|
||||
- Channel editor has a new field `Stream Selector Mode`
|
||||
@@ -64,83 +62,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- `count / 2` will play half of the items in the content
|
||||
- `random % 4 + 1` will play between 1 and 4 items
|
||||
- `2` (similar to before this change) will play exactly two items
|
||||
- YAML playout: add `disable_watermarks` property to all content instructions
|
||||
- This property defaults to `false` (meaning watermarks are allowed by default)
|
||||
- Setting to `true` will prevent watermarks from ever appearing over the content
|
||||
- YAML playout: add `watermark` instruction
|
||||
- With value of `true` and `name` property, will override the watermark in the playout to the watermark with the provided name
|
||||
- With value of `false`, will restore default watermark value (channel watermark, global watermark)
|
||||
- Show health check warning and error badges in nav menu
|
||||
- Add `Expression` for mid-roll filler to allow custom logic for using or skipping chapter markers
|
||||
- The following parameters can be used:
|
||||
- `total_points`: total number of potential mid-roll points
|
||||
- `matched_points`: number of mid-roll points that have already matched the expression
|
||||
- `total_duration`: total duration of the content, in seconds
|
||||
- `total_progress`: normalized position from 0 to 1
|
||||
- `last_mid_filler`: seconds since last mid-roll filler
|
||||
- `remaining_duration`: duration of the content after this mid-roll point, in seconds
|
||||
- `point`: the position of the mid-roll point, in seconds
|
||||
- `num`: the mid-roll point number, starting with 1
|
||||
- Add `Disable Watermarks` checkbox to block items
|
||||
- Block items that have this checked will never display a watermark, even with Deco set to override watermark
|
||||
- Add `ETV_MAXIMUM_UPLOAD_MB` environment variable to allow uploading large watermarks
|
||||
- Default value is 10
|
||||
- Update ffmpeg health check to link to ErsatzTV-FFmpeg release that contains binaries for win64, linux64, linuxarm64
|
||||
- Add `Playback Troubleshooting` page
|
||||
- This tool lets you play specific content without needing a test channel or schedule
|
||||
- You can specify
|
||||
- The media item id (found in ETV media info, and ETV movie URLs)
|
||||
- The ffmpeg profile to use
|
||||
- The watermark to use (if any)
|
||||
- Clicking `Play` will play up to 30 seconds of the specified content using the desired settings
|
||||
- Clicking `Download Results` will generate a zip archive containing:
|
||||
- The FFmpeg report of the playback attempt
|
||||
- The media info for the content
|
||||
- The `Troubleshooting` > `General` output
|
||||
- Support `(Part [english number])` name suffixes for multi-part episode grouping, for example:
|
||||
- `Awesome Episode (Part One)`
|
||||
- `Better Episode (Part Two)`
|
||||
- `Not So Great (Part Three)`
|
||||
- Add Trakt List option `Auto Refresh` to automatically update list from trakt.tv once each day
|
||||
- Add Trakt List option `Generate Playlist` to automatically generate ETV Playlist from matched Trakt List items
|
||||
- Read `country` field from movie NFO files and include in search index as `country`
|
||||
- Add *experimental* and *incomplete* `Remote Stream` library kind
|
||||
- Remote Stream libraries have fallback metadata added like Other Video libraries (every folder is a tag)
|
||||
- Remote Stream library items consist of YAML (`.yml`) files with the following fields
|
||||
- `url`: the URL of the content that can be played directly by ffmpeg
|
||||
- `script`: the process name and arguments for a command that will output content to stdout
|
||||
- `is_live`: *required* property that indicates whether the remote stream contains live content
|
||||
- When this is set to `true`, ETV cannot work ahead on transcoding this item, which is a necessary tradeoff for supporting live content
|
||||
- When this is set to `false`, ETV will treat the stream as VOD and attempt to work ahead on transcoding like any other local item
|
||||
- This *will* cause errors when the content is actually live, so it's important to configure this correctly
|
||||
- `duration`: when the content is live and does not have duration metadata, this must be provided to allow scheduling
|
||||
- The remote stream definition (YAML file) may provide either a `url` or a `script`
|
||||
- If both are provided, `url` will be used
|
||||
- Include number of chapters in search index as `chapters`
|
||||
|
||||
### Changed
|
||||
- Allow `Other Video` libraries and `Image` libraries to use the same folders
|
||||
- Try to mitigate inotify limit error by disabling automatic reloading of `appsettings.json` config files
|
||||
- Support `movie`, `musicvideo` and `episodedetails` top-level tags in other video NFO files
|
||||
- Note that no change has been made to the metadata tags that are actually parsed, but this should help with various types of content
|
||||
- Remove some limits on multithreading that are no longer needed with latest ffmpeg
|
||||
- Mixed transcoding (software decode, hardware filters/encode) can now use multiple decode threads
|
||||
- Split main `Settings` page into multiple pages
|
||||
- Update UI layout on all pages to be less cramped and to work better on mobile
|
||||
- Add CPU and Video Controller info to `Troubleshooting` > `General` output
|
||||
- Enable write-ahead logging (WAL) mode on SQLite databases
|
||||
- Add `Multiple Mode` option to schedule items editor and remove support for count values of zero
|
||||
- `Count`: same behavior as before, requires a number of media items to play and will always schedule the same number
|
||||
- `Collection Size`: similar to count of zero before, will play all media items from the collection before continuing to the next schedule item
|
||||
- `Playlist Item Size`: will play all media items from the current playlist item before continuing to the next schedule item
|
||||
- `Multi-Episode Group Size`: will play all media items from the current multi-part episode group, or one ungrouped media item
|
||||
- Change watermark width and margins to allow decimals
|
||||
- Move `Add To Collection` button to overflow menu on all media cards, and add `Show Media Info` to overflow menu
|
||||
- This allows showing media info for all media kinds
|
||||
- Unify on a multi-platform base docker tag (`latest` and `develop`)
|
||||
- `amd64`, `arm64`, `arm/v7` platforms are now all supported in the base docker tag
|
||||
- Other docker platform tags are deprecated and will receive no new updates after the next release
|
||||
- A health check has been added to notify users (on `-arm` or `-arm64` tags) of this change
|
||||
|
||||
### Fixed
|
||||
- Fix QSV acceleration in docker with older Intel devices
|
||||
@@ -157,20 +84,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Classify HDHR endpoints as streaming endpoints
|
||||
- This allows these endpoints to be accessed through port `ETV_STREAMING_PORT` (default `8409`)
|
||||
- This only matters if you configured `ETV_UI_PORT` to be a different value, which makes UI endpoints inaccessible on the streaming port
|
||||
- Update Plex movie/other video plot ("summary") during library deep scan
|
||||
- Fix compatibility with ffmpeg 7.2+ when using NVIDIA accel and 10-bit source content
|
||||
- Fix some NVIDIA edge cases when media servers don't provide video bit depth information
|
||||
- Fix VAAPI tonemap failure
|
||||
- Fix green bars after VAAPI tonemap
|
||||
- Fix bug where playout mode `Multiple` would ignore fixed start time
|
||||
- Fix block playout EPG generation to use `XMLTV Time Zone` setting
|
||||
- Fix adding "official" Trakt lists
|
||||
- Fix searching for `collection` names with spaces or other special characters, e.g. `collection:"Movies - Action"`
|
||||
- Fix QSV transcoding errors when scaling
|
||||
- Fix QSV frame freezing in browser
|
||||
- Fix some stream continuity issues, and some cases where audio sync is lost at transition
|
||||
- Fix HDR transcoding with AMD VAAPI accel
|
||||
- Allow paths longer than 255 characters in MySql databases
|
||||
|
||||
## [25.2.0] - 2025-06-24
|
||||
### Added
|
||||
@@ -2420,8 +2333,7 @@ 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/ErsatzTV/ErsatzTV/compare/v25.3.0...HEAD
|
||||
[25.3.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.2.0...v25.3.0
|
||||
[Unreleased]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.2.0...HEAD
|
||||
[25.2.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.1.0...v25.2.0
|
||||
[25.1.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.8-beta...v25.1.0
|
||||
[0.8.8-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.7-beta...v0.8.8-beta
|
||||
|
||||
@@ -29,10 +29,9 @@ internal static class Mapper
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return languages
|
||||
.Map(lang => allCultures.Filter(ci => string.Equals(
|
||||
ci.ThreeLetterISOLanguageName,
|
||||
lang,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Flatten()
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
@@ -6,25 +6,24 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Artworks;
|
||||
|
||||
public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetArtwork, Either<BaseError, Artwork>>
|
||||
public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<GetArtwork, Either<BaseError, Artwork>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, Artwork>> Handle(
|
||||
GetArtwork request,
|
||||
GetArtwork request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
try {
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<Artwork> artwork = await dbContext.Artwork
|
||||
.AsNoTracking()
|
||||
.SelectOneAsync(a => a.Id, a => a.Id == request.Id)
|
||||
.MapT(Project);
|
||||
|
||||
|
||||
return artwork.ToEither(BaseError.New("Artwork not found"));
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -32,11 +31,12 @@ public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
}
|
||||
}
|
||||
|
||||
private static Artwork Project(Artwork artwork) =>
|
||||
new()
|
||||
{
|
||||
private static Artwork Project(Artwork artwork)
|
||||
{
|
||||
return new Artwork {
|
||||
Id = artwork.Id,
|
||||
Path = artwork.Path,
|
||||
ArtworkKind = artwork.ArtworkKind
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using System.Net;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
|
||||
@@ -40,69 +40,68 @@ public class CreateChannelHandler(
|
||||
await FFmpegProfileMustExist(dbContext, request),
|
||||
await WatermarkMustExist(dbContext, request),
|
||||
await FillerPresetMustExist(dbContext, request))
|
||||
.Apply((
|
||||
name,
|
||||
number,
|
||||
ffmpegProfileId,
|
||||
watermarkId,
|
||||
fillerPresetId) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
|
||||
.Apply(
|
||||
(
|
||||
name,
|
||||
number,
|
||||
ffmpegProfileId,
|
||||
watermarkId,
|
||||
fillerPresetId) =>
|
||||
{
|
||||
string logo = request.Logo.Path;
|
||||
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
|
||||
{
|
||||
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||
string logo = request.Logo.Path;
|
||||
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||
{
|
||||
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||
}
|
||||
|
||||
artwork.Add(
|
||||
new Artwork
|
||||
{
|
||||
Path = logo,
|
||||
ArtworkKind = ArtworkKind.Logo,
|
||||
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
artwork.Add(
|
||||
new Artwork
|
||||
{
|
||||
Path = logo,
|
||||
ArtworkKind = ArtworkKind.Logo,
|
||||
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType)
|
||||
? request.Logo.ContentType
|
||||
: null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
var channel = new Channel(Guid.NewGuid())
|
||||
{
|
||||
Name = name,
|
||||
Number = number,
|
||||
Group = request.Group,
|
||||
Categories = request.Categories,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
ProgressMode = request.ProgressMode,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork,
|
||||
StreamSelectorMode = request.StreamSelectorMode,
|
||||
StreamSelector = request.StreamSelector,
|
||||
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = request.SubtitleMode,
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
|
||||
SongVideoMode = request.SongVideoMode,
|
||||
ActiveMode = request.ActiveMode
|
||||
};
|
||||
|
||||
var channel = new Channel(Guid.NewGuid())
|
||||
{
|
||||
Name = name,
|
||||
Number = number,
|
||||
Group = request.Group,
|
||||
Categories = request.Categories,
|
||||
FFmpegProfileId = ffmpegProfileId,
|
||||
ProgressMode = request.ProgressMode,
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork,
|
||||
StreamSelectorMode = request.StreamSelectorMode,
|
||||
StreamSelector = request.StreamSelector,
|
||||
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
|
||||
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = request.SubtitleMode,
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
|
||||
SongVideoMode = request.SongVideoMode,
|
||||
ActiveMode = request.ActiveMode
|
||||
};
|
||||
foreach (int id in watermarkId)
|
||||
{
|
||||
channel.WatermarkId = id;
|
||||
}
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
{
|
||||
channel.WatermarkId = id;
|
||||
}
|
||||
foreach (int id in fillerPresetId)
|
||||
{
|
||||
channel.FallbackFillerId = id;
|
||||
}
|
||||
|
||||
foreach (int id in fillerPresetId)
|
||||
{
|
||||
channel.FallbackFillerId = id;
|
||||
}
|
||||
|
||||
return channel;
|
||||
});
|
||||
return channel;
|
||||
});
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
|
||||
createChannel.NotEmpty(c => c.Name)
|
||||
@@ -169,7 +168,8 @@ public class CreateChannelHandler(
|
||||
.Map(Optional)
|
||||
.Filter(c => c > 0)
|
||||
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
||||
.Map(o => o.ToValidation<BaseError>(
|
||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||
.Map(
|
||||
o => o.ToValidation<BaseError>(
|
||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +296,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
int finishIndex = j;
|
||||
while (finishIndex + 1 < sorted.Count && (sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup
|
||||
|| sorted[finishIndex + 1].FillerKind is FillerKind.GuideMode
|
||||
or FillerKind.PostRoll or FillerKind.Tail
|
||||
or FillerKind.Fallback or FillerKind.DecoDefault))
|
||||
or FillerKind.PostRoll or FillerKind.Tail or FillerKind.Fallback or FillerKind.DecoDefault))
|
||||
{
|
||||
finishIndex++;
|
||||
}
|
||||
@@ -346,7 +345,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WriteBlockPlayoutXml(
|
||||
private static async Task WriteBlockPlayoutXml(
|
||||
RefreshChannelData request,
|
||||
List<PlayoutItem> sorted,
|
||||
XmlTemplateContext templateContext,
|
||||
@@ -358,10 +357,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
XmlMinifier minifier,
|
||||
XmlWriter xml)
|
||||
{
|
||||
XmltvTimeZone xmltvTimeZone = await _configElementRepository
|
||||
.GetValue<XmltvTimeZone>(ConfigElementKey.XmltvTimeZone)
|
||||
.IfNoneAsync(XmltvTimeZone.Local);
|
||||
|
||||
var groups = sorted.GroupBy(s => new { s.GuideStart, s.GuideFinish, s.GuideGroup });
|
||||
foreach (var group in groups)
|
||||
{
|
||||
@@ -377,12 +372,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
|
||||
TimeSpan perItem = groupDuration / itemsToInclude.Count;
|
||||
|
||||
DateTimeOffset currentStart = xmltvTimeZone switch
|
||||
{
|
||||
XmltvTimeZone.Utc => new DateTimeOffset(groupStart, TimeSpan.Zero),
|
||||
_ => new DateTimeOffset(groupStart, TimeSpan.Zero).ToLocalTime()
|
||||
};
|
||||
|
||||
DateTimeOffset currentStart = new DateTimeOffset(groupStart, TimeSpan.Zero).ToLocalTime();
|
||||
DateTimeOffset currentFinish = currentStart + perItem;
|
||||
|
||||
foreach (PlayoutItem item in itemsToInclude)
|
||||
|
||||
@@ -12,6 +12,5 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
|
||||
|
||||
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active)
|
||||
.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
.Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active).Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
||||
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => EnsureMode(channels, request.Mode))
|
||||
.Map(channels => new ChannelPlaylist(
|
||||
request.Scheme,
|
||||
request.Host,
|
||||
request.BaseUrl,
|
||||
channels,
|
||||
request.UserAgent,
|
||||
request.AccessToken));
|
||||
.Map(
|
||||
channels => new ChannelPlaylist(
|
||||
request.Scheme,
|
||||
request.Host,
|
||||
request.BaseUrl,
|
||||
channels,
|
||||
request.UserAgent,
|
||||
request.AccessToken));
|
||||
|
||||
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest<Either<BaseError, Unit>>;
|
||||
+15
-15
@@ -4,12 +4,12 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class UpdateLoggingSettingsHandler : IRequestHandler<UpdateLoggingSettings, Either<BaseError, Unit>>
|
||||
public class UpdateGeneralSettingsHandler : IRequestHandler<UpdateGeneralSettings, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly LoggingLevelSwitches _loggingLevelSwitches;
|
||||
|
||||
public UpdateLoggingSettingsHandler(
|
||||
public UpdateGeneralSettingsHandler(
|
||||
LoggingLevelSwitches loggingLevelSwitches,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
@@ -18,33 +18,33 @@ public class UpdateLoggingSettingsHandler : IRequestHandler<UpdateLoggingSetting
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateLoggingSettings request,
|
||||
CancellationToken cancellationToken) => await ApplyUpdate(request.LoggingSettings);
|
||||
UpdateGeneralSettings request,
|
||||
CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings);
|
||||
|
||||
private async Task<Unit> ApplyUpdate(LoggingSettingsViewModel loggingSettings)
|
||||
private async Task<Unit> ApplyUpdate(GeneralSettingsViewModel generalSettings)
|
||||
{
|
||||
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, loggingSettings.DefaultMinimumLogLevel);
|
||||
_loggingLevelSwitches.DefaultLevelSwitch.MinimumLevel = loggingSettings.DefaultMinimumLogLevel;
|
||||
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.DefaultMinimumLogLevel);
|
||||
_loggingLevelSwitches.DefaultLevelSwitch.MinimumLevel = generalSettings.DefaultMinimumLogLevel;
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.MinimumLogLevelScanning,
|
||||
loggingSettings.ScanningMinimumLogLevel);
|
||||
_loggingLevelSwitches.ScanningLevelSwitch.MinimumLevel = loggingSettings.ScanningMinimumLogLevel;
|
||||
generalSettings.ScanningMinimumLogLevel);
|
||||
_loggingLevelSwitches.ScanningLevelSwitch.MinimumLevel = generalSettings.ScanningMinimumLogLevel;
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.MinimumLogLevelScheduling,
|
||||
loggingSettings.SchedulingMinimumLogLevel);
|
||||
_loggingLevelSwitches.SchedulingLevelSwitch.MinimumLevel = loggingSettings.SchedulingMinimumLogLevel;
|
||||
generalSettings.SchedulingMinimumLogLevel);
|
||||
_loggingLevelSwitches.SchedulingLevelSwitch.MinimumLevel = generalSettings.SchedulingMinimumLogLevel;
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.MinimumLogLevelStreaming,
|
||||
loggingSettings.StreamingMinimumLogLevel);
|
||||
_loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = loggingSettings.StreamingMinimumLogLevel;
|
||||
generalSettings.StreamingMinimumLogLevel);
|
||||
_loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = generalSettings.StreamingMinimumLogLevel;
|
||||
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.MinimumLogLevelHttp,
|
||||
loggingSettings.HttpMinimumLogLevel);
|
||||
_loggingLevelSwitches.HttpLevelSwitch.MinimumLevel = loggingSettings.HttpMinimumLogLevel;
|
||||
generalSettings.HttpMinimumLogLevel);
|
||||
_loggingLevelSwitches.HttpLevelSwitch.MinimumLevel = generalSettings.HttpMinimumLogLevel;
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
@@ -16,9 +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) =>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdateLoggingSettings(LoggingSettingsViewModel LoggingSettings) : IRequest<Either<BaseError, Unit>>;
|
||||
+1
-1
@@ -2,7 +2,7 @@ using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class LoggingSettingsViewModel
|
||||
public class GeneralSettingsViewModel
|
||||
{
|
||||
public LogEventLevel DefaultMinimumLogLevel { get; set; }
|
||||
public LogEventLevel ScanningMinimumLogLevel { get; set; }
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetGeneralSettings : IRequest<GeneralSettingsViewModel>;
|
||||
+4
-4
@@ -4,14 +4,14 @@ using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class GetLoggingSettingsHandler : IRequestHandler<GetLoggingSettings, LoggingSettingsViewModel>
|
||||
public class GetGeneralSettingsHandler : IRequestHandler<GetGeneralSettings, GeneralSettingsViewModel>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetLoggingSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||
public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public async Task<LoggingSettingsViewModel> Handle(GetLoggingSettings request, CancellationToken cancellationToken)
|
||||
public async Task<GeneralSettingsViewModel> Handle(GetGeneralSettings request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<LogEventLevel> maybeDefaultLevel =
|
||||
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
|
||||
@@ -28,7 +28,7 @@ public class GetLoggingSettingsHandler : IRequestHandler<GetLoggingSettings, Log
|
||||
Option<LogEventLevel> maybeHttpLevel =
|
||||
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelHttp);
|
||||
|
||||
return new LoggingSettingsViewModel
|
||||
return new GeneralSettingsViewModel
|
||||
{
|
||||
DefaultMinimumLogLevel = await maybeDefaultLevel.IfNoneAsync(LogEventLevel.Information),
|
||||
ScanningMinimumLogLevel = await maybeScanningLevel.IfNoneAsync(LogEventLevel.Information),
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetLoggingSettings : IRequest<LoggingSettingsViewModel>;
|
||||
@@ -43,6 +43,7 @@ public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathR
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
|
||||
UpdateEmbyPathReplacements request) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
|
||||
.Map(v => v.ToValidation<BaseError>(
|
||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||
}
|
||||
|
||||
@@ -54,8 +54,9 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
|
||||
|
||||
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
|
||||
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
|
||||
.Map(v => v.ToValidation<BaseError>(
|
||||
"Emby media source does not exist."));
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
"Emby media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
EmbyMediaSource embyMediaSource)
|
||||
|
||||
@@ -9,25 +9,25 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MediatR" Version="[12.5.0]" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artworks_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
@@ -45,7 +44,5 @@
|
||||
<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/=templates_005Cqueries/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=troubleshooting_005Ccommands/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=validators/@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>
|
||||
@@ -42,33 +42,34 @@ public class CreateFFmpegProfileHandler :
|
||||
TvContext dbContext,
|
||||
CreateFFmpegProfile request) =>
|
||||
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
|
||||
.Apply((name, threadCount, resolutionId) => new FFmpegProfile
|
||||
{
|
||||
Name = name,
|
||||
ThreadCount = threadCount,
|
||||
HardwareAcceleration = request.HardwareAcceleration,
|
||||
VaapiDriver = request.VaapiDriver,
|
||||
VaapiDevice = request.VaapiDevice,
|
||||
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
|
||||
ResolutionId = resolutionId,
|
||||
ScalingBehavior = request.ScalingBehavior,
|
||||
VideoFormat = request.VideoFormat,
|
||||
VideoProfile = request.VideoProfile,
|
||||
VideoPreset = request.VideoPreset,
|
||||
AllowBFrames = request.AllowBFrames,
|
||||
BitDepth = request.BitDepth,
|
||||
VideoBitrate = request.VideoBitrate,
|
||||
VideoBufferSize = request.VideoBufferSize,
|
||||
TonemapAlgorithm = request.TonemapAlgorithm,
|
||||
AudioFormat = request.AudioFormat,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeFramerate = request.NormalizeFramerate,
|
||||
DeinterlaceVideo = request.DeinterlaceVideo
|
||||
});
|
||||
.Apply(
|
||||
(name, threadCount, resolutionId) => new FFmpegProfile
|
||||
{
|
||||
Name = name,
|
||||
ThreadCount = threadCount,
|
||||
HardwareAcceleration = request.HardwareAcceleration,
|
||||
VaapiDriver = request.VaapiDriver,
|
||||
VaapiDevice = request.VaapiDevice,
|
||||
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
|
||||
ResolutionId = resolutionId,
|
||||
ScalingBehavior = request.ScalingBehavior,
|
||||
VideoFormat = request.VideoFormat,
|
||||
VideoProfile = request.VideoProfile,
|
||||
VideoPreset = request.VideoPreset,
|
||||
AllowBFrames = request.AllowBFrames,
|
||||
BitDepth = request.BitDepth,
|
||||
VideoBitrate = request.VideoBitrate,
|
||||
VideoBufferSize = request.VideoBufferSize,
|
||||
TonemapAlgorithm = request.TonemapAlgorithm,
|
||||
AudioFormat = request.AudioFormat,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeFramerate = request.NormalizeFramerate,
|
||||
DeinterlaceVideo = request.DeinterlaceVideo
|
||||
});
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||
createFFmpegProfile.NotEmpty(x => x.Name)
|
||||
|
||||
@@ -16,6 +16,5 @@ public record CreateFillerPreset(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId,
|
||||
string Expression
|
||||
int? SmartCollectionId
|
||||
) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -36,8 +36,7 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit
|
||||
CollectionId = request.CollectionId,
|
||||
MediaItemId = request.MediaItemId,
|
||||
MultiCollectionId = request.MultiCollectionId,
|
||||
SmartCollectionId = request.SmartCollectionId,
|
||||
Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null
|
||||
SmartCollectionId = request.SmartCollectionId
|
||||
};
|
||||
|
||||
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
||||
|
||||
@@ -17,6 +17,5 @@ public record UpdateFillerPreset(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId,
|
||||
string Expression
|
||||
int? SmartCollectionId
|
||||
) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -37,7 +37,6 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
|
||||
existing.MediaItemId = request.MediaItemId;
|
||||
existing.MultiCollectionId = request.MultiCollectionId;
|
||||
existing.SmartCollectionId = request.SmartCollectionId;
|
||||
existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -16,5 +16,4 @@ public record FillerPresetViewModel(
|
||||
int? CollectionId,
|
||||
int? MediaItemId,
|
||||
int? MultiCollectionId,
|
||||
int? SmartCollectionId,
|
||||
string Expression);
|
||||
int? SmartCollectionId);
|
||||
|
||||
@@ -18,6 +18,5 @@ internal static class Mapper
|
||||
fillerPreset.CollectionId,
|
||||
fillerPreset.MediaItemId,
|
||||
fillerPreset.MultiCollectionId,
|
||||
fillerPreset.SmartCollectionId,
|
||||
fillerPreset.Expression);
|
||||
fillerPreset.SmartCollectionId);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@ public class UpdateHDHRTunerCountHandler : IRequestHandler<UpdateHDHRTunerCount,
|
||||
UpdateHDHRTunerCount request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => _configElementRepository.Upsert(
|
||||
ConfigElementKey.HDHRTunerCount,
|
||||
request.TunerCount.ToString(CultureInfo.InvariantCulture)))
|
||||
.MapT(
|
||||
_ => _configElementRepository.Upsert(
|
||||
ConfigElementKey.HDHRTunerCount,
|
||||
request.TunerCount.ToString(CultureInfo.InvariantCulture)))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
|
||||
@@ -13,11 +13,12 @@ public class GetHDHRUUIDHandler : IRequestHandler<GetHDHRUUID, Guid>
|
||||
public async Task<Guid> Handle(GetHDHRUUID request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<Guid> maybeGuid = await _configElementRepository.GetValue<Guid>(ConfigElementKey.HDHRUUID);
|
||||
return await maybeGuid.IfNoneAsync(async () =>
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
await _configElementRepository.Upsert(ConfigElementKey.HDHRUUID, guid);
|
||||
return guid;
|
||||
});
|
||||
return await maybeGuid.IfNoneAsync(
|
||||
async () =>
|
||||
{
|
||||
Guid guid = Guid.NewGuid();
|
||||
await _configElementRepository.Upsert(ConfigElementKey.HDHRUUID, guid);
|
||||
return guid;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,4 @@ using ErsatzTV.Core.Domain;
|
||||
namespace ErsatzTV.Application.Images;
|
||||
|
||||
// ReSharper disable once SuggestBaseTypeForParameter
|
||||
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType)
|
||||
: IRequest<Either<BaseError, string>>;
|
||||
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType) : IRequest<Either<BaseError, string>>;
|
||||
|
||||
@@ -97,8 +97,9 @@ public class UpdateImageFolderDurationHandler(IDbContextFactory<TvContext> dbCon
|
||||
|
||||
// update all images in this folder
|
||||
await dbContext.ImageMetadata
|
||||
.Filter(im =>
|
||||
im.Image.MediaVersions.Any(mv => mv.MediaFiles.Any(mf => mf.LibraryFolderId == currentFolder.Id)))
|
||||
.Filter(
|
||||
im => im.Image.MediaVersions.Any(
|
||||
mv => mv.MediaFiles.Any(mf => mf.LibraryFolderId == currentFolder.Id)))
|
||||
.ExecuteUpdateAsync(
|
||||
setters => setters.SetProperty(im => im.DurationSeconds, effectiveDuration),
|
||||
cancellationToken);
|
||||
|
||||
@@ -3,6 +3,5 @@ using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Images;
|
||||
|
||||
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null)
|
||||
: IRequest<
|
||||
Either<BaseError, CachedImagePathViewModel>>;
|
||||
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null) : IRequest<
|
||||
Either<BaseError, CachedImagePathViewModel>>;
|
||||
|
||||
@@ -43,6 +43,7 @@ public class UpdateJellyfinPathReplacementsHandler : IRequestHandler<UpdateJelly
|
||||
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist(
|
||||
UpdateJellyfinPathReplacements request) =>
|
||||
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
|
||||
.Map(v => v.ToValidation<BaseError>(
|
||||
$"Jellyfin media source {request.JellyfinMediaSourceId} does not exist."));
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Jellyfin media source {request.JellyfinMediaSourceId} does not exist."));
|
||||
}
|
||||
|
||||
@@ -48,8 +48,9 @@ public class GetJellyfinConnectionParametersHandler : IRequestHandler<GetJellyfi
|
||||
|
||||
private Task<Validation<BaseError, JellyfinMediaSource>> JellyfinMediaSourceMustExist() =>
|
||||
_mediaSourceRepository.GetAllJellyfin().Map(list => list.HeadOrNone())
|
||||
.Map(v => v.ToValidation<BaseError>(
|
||||
"Jellyfin media source does not exist."));
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
"Jellyfin media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
JellyfinMediaSource jellyfinMediaSource)
|
||||
|
||||
@@ -54,9 +54,9 @@ public abstract class CallLibraryScannerHandler<TRequest>
|
||||
{
|
||||
using var forcefulCts = new CancellationTokenSource();
|
||||
|
||||
await using CancellationTokenRegistration link =
|
||||
cancellationToken.Register(() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10))
|
||||
);
|
||||
await using CancellationTokenRegistration link = cancellationToken.Register(
|
||||
() => forcefulCts.CancelAfter(TimeSpan.FromSeconds(10))
|
||||
);
|
||||
|
||||
CommandResult process = await Cli.Wrap(scanner)
|
||||
.WithArguments(arguments)
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.MediaSources;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Libraries.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public class CreateLocalLibraryHandler : LocalLibraryHandlerBase,
|
||||
IRequestHandler<CreateLocalLibrary, Either<BaseError, LocalLibraryViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
|
||||
|
||||
public CreateLocalLibraryHandler(
|
||||
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel,
|
||||
IEntityLocker entityLocker,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_scannerWorkerChannel = scannerWorkerChannel;
|
||||
_entityLocker = entityLocker;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, LocalLibraryViewModel>> Handle(
|
||||
CreateLocalLibrary request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
|
||||
}
|
||||
|
||||
private async Task<LocalLibraryViewModel> PersistLocalLibrary(
|
||||
TvContext dbContext,
|
||||
LocalLibrary localLibrary)
|
||||
{
|
||||
await dbContext.LocalLibraries.AddAsync(localLibrary);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (_entityLocker.LockLibrary(localLibrary.Id))
|
||||
{
|
||||
await _scannerWorkerChannel.WriteAsync(new ForceScanLocalLibrary(localLibrary.Id));
|
||||
}
|
||||
|
||||
return ProjectToViewModel(localLibrary);
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, LocalLibrary>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateLocalLibrary request) =>
|
||||
MediaSourceMustExist(dbContext, request)
|
||||
.BindT(localLibrary => NameMustBeValid(request, localLibrary))
|
||||
.BindT(localLibrary => PathsMustBeValid(dbContext, localLibrary));
|
||||
|
||||
private static Task<Validation<BaseError, LocalLibrary>> MediaSourceMustExist(
|
||||
TvContext dbContext,
|
||||
CreateLocalLibrary request) =>
|
||||
dbContext.LocalMediaSources
|
||||
.OrderBy(lms => lms.Id)
|
||||
.FirstOrDefaultAsync()
|
||||
.Map(Optional)
|
||||
.MapT(lms => new LocalLibrary
|
||||
{
|
||||
Name = request.Name,
|
||||
Paths = request.Paths.Map(p => new LibraryPath { Path = p }).ToList(),
|
||||
MediaKind = request.MediaKind,
|
||||
MediaSourceId = lms.Id
|
||||
})
|
||||
.Map(o => o.ToValidation<BaseError>("LocalMediaSource does not exist."));
|
||||
}
|
||||
@@ -44,10 +44,10 @@ public abstract class LocalLibraryHandlerBase
|
||||
// Images and OtherVideos do not conflict
|
||||
if (isConflict)
|
||||
{
|
||||
bool imagesAndOtherVideos = path1.MediaKind is LibraryMediaKind.Images &&
|
||||
path2.MediaKind is LibraryMediaKind.OtherVideos
|
||||
|| path2.MediaKind is LibraryMediaKind.Images &&
|
||||
path1.MediaKind is LibraryMediaKind.OtherVideos;
|
||||
bool imagesAndOtherVideos = (path1.MediaKind is LibraryMediaKind.Images &&
|
||||
path2.MediaKind is LibraryMediaKind.OtherVideos)
|
||||
|| (path2.MediaKind is LibraryMediaKind.Images &&
|
||||
path1.MediaKind is LibraryMediaKind.OtherVideos);
|
||||
|
||||
if (imagesAndOtherVideos)
|
||||
{
|
||||
|
||||
@@ -102,8 +102,9 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
|
||||
UpdateLocalLibrary request) =>
|
||||
LocalLibraryMustExist(dbContext, request)
|
||||
.BindT(parameters => NameMustBeValid(request, parameters.Incoming).MapT(_ => parameters))
|
||||
.BindT(parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id)
|
||||
.MapT(_ => parameters));
|
||||
.BindT(
|
||||
parameters => PathsMustBeValid(dbContext, parameters.Incoming, parameters.Existing.Id)
|
||||
.MapT(_ => parameters));
|
||||
|
||||
private static Task<Validation<BaseError, Parameters>> LocalLibraryMustExist(
|
||||
TvContext dbContext,
|
||||
@@ -111,18 +112,19 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
|
||||
dbContext.LocalLibraries
|
||||
.Include(ll => ll.Paths)
|
||||
.SelectOneAsync(ll => ll.Id, ll => ll.Id == request.Id)
|
||||
.MapT(existing =>
|
||||
{
|
||||
var incoming = new LocalLibrary
|
||||
.MapT(
|
||||
existing =>
|
||||
{
|
||||
Name = request.Name,
|
||||
Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(),
|
||||
MediaKind = existing.MediaKind,
|
||||
MediaSourceId = existing.Id
|
||||
};
|
||||
var incoming = new LocalLibrary
|
||||
{
|
||||
Name = request.Name,
|
||||
Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(),
|
||||
MediaKind = existing.MediaKind,
|
||||
MediaSourceId = existing.Id
|
||||
};
|
||||
|
||||
return new Parameters(existing, incoming);
|
||||
})
|
||||
return new Parameters(existing, incoming);
|
||||
})
|
||||
.Map(o => o.ToValidation<BaseError>("LocalLibrary does not exist."));
|
||||
|
||||
private static string NormalizePath(string path) =>
|
||||
|
||||
@@ -14,9 +14,10 @@ public class GetAllLocalLibrariesHandler : IRequestHandler<GetAllLocalLibraries,
|
||||
GetAllLocalLibraries request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_libraryRepository.GetAll()
|
||||
.Map(list => list
|
||||
.OfType<LocalLibrary>()
|
||||
.OrderBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel)
|
||||
.ToList());
|
||||
.Map(
|
||||
list => list
|
||||
.OfType<LocalLibrary>()
|
||||
.OrderBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
@@ -15,11 +15,12 @@ public class GetConfiguredLibrariesHandler : IRequestHandler<GetConfiguredLibrar
|
||||
GetConfiguredLibraries request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_libraryRepository.GetAll()
|
||||
.Map(list => list.Filter(ShouldIncludeLibrary)
|
||||
.OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1)
|
||||
.ThenBy(l => l.GetType().Name)
|
||||
.ThenBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel).ToList());
|
||||
.Map(
|
||||
list => list.Filter(ShouldIncludeLibrary)
|
||||
.OrderBy(l => l.MediaSource is LocalMediaSource ? 0 : 1)
|
||||
.ThenBy(l => l.GetType().Name)
|
||||
.ThenBy(l => l.MediaKind)
|
||||
.Map(ProjectToViewModel).ToList());
|
||||
|
||||
private static bool ShouldIncludeLibrary(Library library) =>
|
||||
library switch
|
||||
|
||||
@@ -46,13 +46,8 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
|
||||
.Map(jms => jms.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return jellyfinMediaSourceIds.Map(id => new LibraryViewModel(
|
||||
"Jellyfin",
|
||||
0,
|
||||
"Collections",
|
||||
0,
|
||||
id,
|
||||
string.Empty));
|
||||
return jellyfinMediaSourceIds.Map(
|
||||
id => new LibraryViewModel("Jellyfin", 0, "Collections", 0, id, string.Empty));
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<LibraryViewModel>> GetPlexExternalCollections(
|
||||
@@ -64,6 +59,7 @@ public class GetExternalCollectionsHandler : IRequestHandler<GetExternalCollecti
|
||||
.Map(pms => pms.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return plexMediaSourceIds.Map(id => new LibraryViewModel("Plex", 0, "Collections", 0, id, string.Empty));
|
||||
return plexMediaSourceIds.Map(
|
||||
id => new LibraryViewModel("Plex", 0, "Collections", 0, id, string.Empty));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ public class GetRecentLogEntriesHandler : IRequestHandler<GetRecentLogEntries, P
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Filter))
|
||||
{
|
||||
entries = entries.Filter(le =>
|
||||
le.Level.ToString().Contains(request.Filter, StringComparison.OrdinalIgnoreCase) ||
|
||||
le.Message.Contains(request.Filter, StringComparison.OrdinalIgnoreCase));
|
||||
entries = entries.Filter(
|
||||
le => le.Level.ToString().Contains(request.Filter, StringComparison.OrdinalIgnoreCase) ||
|
||||
le.Message.Contains(request.Filter, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
int count = entries.Count();
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
namespace ErsatzTV.Application.MediaCards;
|
||||
|
||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) :
|
||||
MediaCardViewModel(Id, Name, Role, Name, Thumb, State, HasMediaInfo: false);
|
||||
MediaCardViewModel(Id, Name, Role, Name, Thumb, State);
|
||||
|
||||
@@ -14,5 +14,4 @@ public record ArtistCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: false);
|
||||
State);
|
||||
|
||||
@@ -10,8 +10,7 @@ public record CollectionCardResultsViewModel(
|
||||
List<MusicVideoCardViewModel> MusicVideoCards,
|
||||
List<OtherVideoCardViewModel> OtherVideoCards,
|
||||
List<SongCardViewModel> SongCards,
|
||||
List<ImageCardViewModel> ImageCards,
|
||||
List<RemoteStreamCardViewModel> RemoteStreamCards)
|
||||
List<ImageCardViewModel> ImageCards)
|
||||
{
|
||||
public bool UseCustomPlaybackOrder { get; set; }
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ public record ImageCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
State)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -155,15 +155,6 @@ internal static class Mapper
|
||||
string.Empty, // TODO: thumbnail?
|
||||
imageMetadata.Image.State);
|
||||
|
||||
internal static RemoteStreamCardViewModel ProjectToViewModel(RemoteStreamMetadata remoteStreamMetadata) =>
|
||||
new(
|
||||
remoteStreamMetadata.RemoteStreamId,
|
||||
remoteStreamMetadata.Title,
|
||||
remoteStreamMetadata.OriginalTitle,
|
||||
remoteStreamMetadata.SortTitle,
|
||||
string.Empty, // TODO: thumbnail?
|
||||
remoteStreamMetadata.RemoteStream.State);
|
||||
|
||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||
new(
|
||||
artistMetadata.ArtistId,
|
||||
@@ -180,8 +171,8 @@ internal static class Mapper
|
||||
Option<EmbyMediaSource> maybeEmby) =>
|
||||
new(
|
||||
collection.Name,
|
||||
collection.MediaItems.OfType<Movie>().Map(m =>
|
||||
ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with
|
||||
collection.MediaItems.OfType<Movie>().Map(
|
||||
m => ProjectToViewModel(m.MovieMetadata.Head(), maybeJellyfin, maybeEmby) with
|
||||
{
|
||||
CustomIndex = GetCustomIndex(collection, m.Id)
|
||||
}).ToList(),
|
||||
@@ -192,12 +183,13 @@ internal static class Mapper
|
||||
.ToList(),
|
||||
// collection view doesn't use local paths
|
||||
collection.MediaItems.OfType<Episode>()
|
||||
.Map(e => ProjectToViewModel(
|
||||
e.EpisodeMetadata.Head(),
|
||||
maybeJellyfin,
|
||||
maybeEmby,
|
||||
true,
|
||||
string.Empty))
|
||||
.Map(
|
||||
e => ProjectToViewModel(
|
||||
e.EpisodeMetadata.Head(),
|
||||
maybeJellyfin,
|
||||
maybeEmby,
|
||||
false,
|
||||
string.Empty))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||
// collection view doesn't use local paths
|
||||
@@ -208,9 +200,7 @@ internal static class Mapper
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Song>().Map(s => ProjectToViewModel(s.SongMetadata.Head()))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Image>().Map(i => ProjectToViewModel(i.ImageMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<RemoteStream>().Map(i => ProjectToViewModel(i.RemoteStreamMetadata.Head()))
|
||||
.ToList())
|
||||
collection.MediaItems.OfType<Image>().Map(i => ProjectToViewModel(i.ImageMetadata.Head())).ToList())
|
||||
{ UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
internal static ActorCardViewModel ProjectToViewModel(
|
||||
|
||||
@@ -8,5 +8,4 @@ public record MediaCardViewModel(
|
||||
string Subtitle,
|
||||
string SortTitle,
|
||||
string Poster,
|
||||
MediaItemState State,
|
||||
bool HasMediaInfo);
|
||||
MediaItemState State);
|
||||
|
||||
@@ -14,8 +14,7 @@ public record MovieCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
State)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ public record MusicVideoCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
State)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ public record OtherVideoCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
State)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -79,11 +79,9 @@ public class GetCollectionCardsHandler :
|
||||
.ThenInclude(i => (i as Episode).Season)
|
||||
.ThenInclude(s => s.Show)
|
||||
.ThenInclude(s => s.ShowMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).Season)
|
||||
.ThenInclude(s => s.SeasonMetadata)
|
||||
.ThenInclude(sm => sm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Episode).MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
@@ -105,12 +103,6 @@ public class GetCollectionCardsHandler :
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as Image).MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as RemoteStream).RemoteStreamMetadata)
|
||||
.ThenInclude(ovm => ovm.Artwork)
|
||||
.Include(c => c.MediaItems)
|
||||
.ThenInclude(i => (i as RemoteStream).MediaVersions)
|
||||
.ThenInclude(mv => mv.MediaFiles)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core.Search;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards;
|
||||
|
||||
public record RemoteStreamCardResultsViewModel(int Count, List<RemoteStreamCardViewModel> Cards, SearchPageMap PageMap);
|
||||
@@ -1,21 +0,0 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards;
|
||||
|
||||
public record RemoteStreamCardViewModel(
|
||||
int RemoteStreamId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string SortTitle,
|
||||
string Poster,
|
||||
MediaItemState State) : MediaCardViewModel(
|
||||
RemoteStreamId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
@@ -14,8 +14,7 @@ public record SongCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true)
|
||||
State)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -24,5 +24,4 @@ public record TelevisionEpisodeCardViewModel(
|
||||
$"Episode {Episode}",
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: true);
|
||||
State);
|
||||
|
||||
@@ -17,5 +17,4 @@ public record TelevisionSeasonCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: false);
|
||||
State);
|
||||
|
||||
@@ -14,5 +14,4 @@ public record TelevisionShowCardViewModel(
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster,
|
||||
State,
|
||||
HasMediaInfo: false);
|
||||
State);
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddArtistToCollectionHandler :
|
||||
IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddArtistToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddEpisodeToCollectionHandler :
|
||||
IRequestHandler<AddEpisodeToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddEpisodeToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class AddImageToCollectionHandler : IRequestHandler<AddImageToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddImageToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -12,5 +12,4 @@ public record AddItemsToCollection(
|
||||
List<int> MusicVideoIds,
|
||||
List<int> OtherVideoIds,
|
||||
List<int> SongIds,
|
||||
List<int> ImageIds,
|
||||
List<int> RemoteStreamIds) : IRequest<Either<BaseError, Unit>>;
|
||||
List<int> ImageIds) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -15,10 +15,10 @@ public class AddItemsToCollectionHandler :
|
||||
IRequestHandler<AddItemsToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public AddItemsToCollectionHandler(
|
||||
@@ -60,7 +60,6 @@ public class AddItemsToCollectionHandler :
|
||||
.Append(request.OtherVideoIds)
|
||||
.Append(request.SongIds)
|
||||
.Append(request.ImageIds)
|
||||
.Append(request.RemoteStreamIds)
|
||||
.ToList();
|
||||
|
||||
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
||||
|
||||
@@ -12,5 +12,4 @@ public record AddItemsToPlaylist(
|
||||
List<int> MusicVideoIds,
|
||||
List<int> OtherVideoIds,
|
||||
List<int> SongIds,
|
||||
List<int> ImageIds,
|
||||
List<int> RemoteStreamIds) : IRequest<Either<BaseError, Unit>>;
|
||||
List<int> ImageIds) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record AddMediaItemToCollection(int CollectionId, int MediaItemId) : IRequest<Either<BaseError, Unit>>;
|
||||
@@ -1,83 +0,0 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public class AddMediaItemToCollectionHandler :
|
||||
IRequestHandler<AddMediaItemToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddMediaItemToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
AddMediaItemToCollection request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(parameters => ApplyAddMediaItemRequest(dbContext, parameters));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyAddMediaItemRequest(TvContext dbContext, Parameters parameters)
|
||||
{
|
||||
parameters.Collection.MediaItems.Add(parameters.MediaItem);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.MediaItem.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Refresh));
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||
TvContext dbContext,
|
||||
AddMediaItemToCollection request) =>
|
||||
(await CollectionMustExist(dbContext, request), await ValidateMediaItem(dbContext, request))
|
||||
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||
|
||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||
TvContext dbContext,
|
||||
AddMediaItemToCollection request) =>
|
||||
dbContext.Collections
|
||||
.Include(c => c.MediaItems)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private static Task<Validation<BaseError, MediaItem>> ValidateMediaItem(
|
||||
TvContext dbContext,
|
||||
AddMediaItemToCollection request) =>
|
||||
dbContext.MediaItems
|
||||
.SelectOneAsync(m => m.Id, e => e.Id == request.MediaItemId)
|
||||
.Map(o => o.ToValidation<BaseError>("MediaItem does not exist"));
|
||||
|
||||
private sealed record Parameters(Collection Collection, MediaItem MediaItem);
|
||||
}
|
||||
@@ -15,9 +15,9 @@ public class AddMovieToCollectionHandler :
|
||||
IRequestHandler<AddMovieToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddMovieToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddMusicVideoToCollectionHandler :
|
||||
IRequestHandler<AddMusicVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddMusicVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddOtherVideoToCollectionHandler :
|
||||
IRequestHandler<AddOtherVideoToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddOtherVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddSeasonToCollectionHandler :
|
||||
IRequestHandler<AddSeasonToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddSeasonToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddShowToCollectionHandler :
|
||||
IRequestHandler<AddShowToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddShowToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -15,9 +15,9 @@ public class AddSongToCollectionHandler :
|
||||
IRequestHandler<AddSongToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public AddSongToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -2,9 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record AddTraktList(string TraktListUrl, string User, string List, bool Unlock)
|
||||
: IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest
|
||||
{
|
||||
public static AddTraktList FromUrl(string traktListUrl) => new(traktListUrl, string.Empty, string.Empty, true);
|
||||
public static AddTraktList Existing(string user, string list, bool unlock) => new(string.Empty, user, list, unlock);
|
||||
}
|
||||
public record AddTraktList(string TraktListUrl) : IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;
|
||||
|
||||
@@ -42,23 +42,15 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (request.Unlock)
|
||||
{
|
||||
_entityLocker.UnlockTrakt();
|
||||
}
|
||||
_entityLocker.UnlockTrakt();
|
||||
}
|
||||
}
|
||||
|
||||
private static Validation<BaseError, Parameters> ValidateUrl(AddTraktList request)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(request.User) && !string.IsNullOrWhiteSpace(request.List))
|
||||
{
|
||||
return new Parameters(request.User, request.List);
|
||||
}
|
||||
|
||||
// if we get a url, ensure it's for trakt.tv
|
||||
Match match = Uri.IsWellFormedUriString(request.TraktListUrl, UriKind.Absolute)
|
||||
? MatchTraktListUrl(request.TraktListUrl)
|
||||
? UriTraktListRegex().Match(request.TraktListUrl)
|
||||
: ShorthandTraktListRegex().Match(request.TraktListUrl);
|
||||
|
||||
if (match.Success)
|
||||
@@ -71,17 +63,6 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
|
||||
return BaseError.New("Invalid Trakt list url");
|
||||
}
|
||||
|
||||
private static Match MatchTraktListUrl(string traktListUrl)
|
||||
{
|
||||
Match match = UriTraktListRegex().Match(traktListUrl);
|
||||
if (!match.Success)
|
||||
{
|
||||
match = UriTraktListRegex2().Match(traktListUrl);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, Unit>> DoAdd(Parameters parameters)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
@@ -91,7 +72,6 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
|
||||
|
||||
foreach (TraktList list in maybeList.RightToSeq())
|
||||
{
|
||||
list.User = parameters.User.ToLowerInvariant();
|
||||
maybeList = await SaveList(dbContext, list);
|
||||
}
|
||||
|
||||
@@ -109,14 +89,11 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
|
||||
return maybeList.Map(_ => Unit.Default);
|
||||
}
|
||||
|
||||
private sealed record Parameters(string User, string List);
|
||||
|
||||
[GeneratedRegex(@"https:\/\/trakt\.tv\/users\/([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex();
|
||||
|
||||
[GeneratedRegex(@"https:\/\/trakt\.tv\/lists\/([\w\-_]+)\/([\w\-_]+)")]
|
||||
private static partial Regex UriTraktListRegex2();
|
||||
|
||||
[GeneratedRegex(@"([\w\-_]+)\/(?:lists\/)?([\w\-_]+)")]
|
||||
private static partial Regex ShorthandTraktListRegex();
|
||||
|
||||
private sealed record Parameters(string User, string List);
|
||||
}
|
||||
|
||||
@@ -39,11 +39,12 @@ public class CreateCollectionHandler :
|
||||
private static Task<Validation<BaseError, Collection>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateCollection request) =>
|
||||
ValidateName(dbContext, request).MapT(name => new Collection
|
||||
{
|
||||
Name = name,
|
||||
MediaItems = new List<MediaItem>()
|
||||
});
|
||||
ValidateName(dbContext, request).MapT(
|
||||
name => new Collection
|
||||
{
|
||||
Name = name,
|
||||
MediaItems = new List<MediaItem>()
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||
TvContext dbContext,
|
||||
|
||||
@@ -51,42 +51,45 @@ public class CreateMultiCollectionHandler :
|
||||
private static Task<Validation<BaseError, MultiCollection>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateMultiCollection request) =>
|
||||
ValidateName(dbContext, request).MapT(name => new MultiCollection
|
||||
{
|
||||
Name = name,
|
||||
MultiCollectionItems = request.Items.Bind(i =>
|
||||
{
|
||||
if (i.CollectionId.HasValue)
|
||||
{
|
||||
return Some(
|
||||
new MultiCollectionItem
|
||||
ValidateName(dbContext, request).MapT(
|
||||
name => new MultiCollection
|
||||
{
|
||||
Name = name,
|
||||
MultiCollectionItems = request.Items.Bind(
|
||||
i =>
|
||||
{
|
||||
if (i.CollectionId.HasValue)
|
||||
{
|
||||
CollectionId = i.CollectionId.Value,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
});
|
||||
}
|
||||
return Some(
|
||||
new MultiCollectionItem
|
||||
{
|
||||
CollectionId = i.CollectionId.Value,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
});
|
||||
}
|
||||
|
||||
return Option<MultiCollectionItem>.None;
|
||||
})
|
||||
.ToList(),
|
||||
MultiCollectionSmartItems = request.Items.Bind(i =>
|
||||
{
|
||||
if (i.SmartCollectionId.HasValue)
|
||||
{
|
||||
return Some(
|
||||
new MultiCollectionSmartItem
|
||||
return Option<MultiCollectionItem>.None;
|
||||
})
|
||||
.ToList(),
|
||||
MultiCollectionSmartItems = request.Items.Bind(
|
||||
i =>
|
||||
{
|
||||
if (i.SmartCollectionId.HasValue)
|
||||
{
|
||||
SmartCollectionId = i.SmartCollectionId.Value,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
});
|
||||
}
|
||||
return Some(
|
||||
new MultiCollectionSmartItem
|
||||
{
|
||||
SmartCollectionId = i.SmartCollectionId.Value,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
});
|
||||
}
|
||||
|
||||
return Option<MultiCollectionSmartItem>.None;
|
||||
})
|
||||
.ToList()
|
||||
});
|
||||
return Option<MultiCollectionSmartItem>.None;
|
||||
})
|
||||
.ToList()
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||
TvContext dbContext,
|
||||
|
||||
@@ -25,11 +25,12 @@ public class CreatePlaylistHandler(IDbContextFactory<TvContext> dbContextFactory
|
||||
}
|
||||
|
||||
private static async Task<Validation<BaseError, Playlist>> Validate(TvContext dbContext, CreatePlaylist request) =>
|
||||
await ValidatePlaylistName(dbContext, request).MapT(name => new Playlist
|
||||
{
|
||||
PlaylistGroupId = request.PlaylistGroupId,
|
||||
Name = name
|
||||
});
|
||||
await ValidatePlaylistName(dbContext, request).MapT(
|
||||
name => new Playlist
|
||||
{
|
||||
PlaylistGroupId = request.PlaylistGroupId,
|
||||
Name = name
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidatePlaylistName(
|
||||
TvContext dbContext,
|
||||
|
||||
@@ -48,11 +48,12 @@ public class CreateSmartCollectionHandler :
|
||||
private static Task<Validation<BaseError, SmartCollection>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateSmartCollection request) =>
|
||||
ValidateName(dbContext, request).MapT(name => new SmartCollection
|
||||
{
|
||||
Name = name,
|
||||
Query = request.Query
|
||||
});
|
||||
ValidateName(dbContext, request).MapT(
|
||||
name => new SmartCollection
|
||||
{
|
||||
Name = name,
|
||||
Query = request.Query
|
||||
});
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||
TvContext dbContext,
|
||||
|
||||
@@ -18,11 +18,6 @@ public class DeletePlaylistGroupHandler(IDbContextFactory<TvContext> dbContextFa
|
||||
|
||||
foreach (PlaylistGroup playlistGroup in maybePlaylistGroup)
|
||||
{
|
||||
if (playlistGroup.IsSystem)
|
||||
{
|
||||
return BaseError.New("Cannot delete system playlist group");
|
||||
}
|
||||
|
||||
dbContext.PlaylistGroups.Remove(playlistGroup);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ public class DeletePlaylistHandler(IDbContextFactory<TvContext> dbContextFactory
|
||||
|
||||
foreach (Playlist playlist in maybePlaylist)
|
||||
{
|
||||
if (playlist.IsSystem)
|
||||
{
|
||||
return BaseError.New("Cannot delete system (generated) playlist");
|
||||
}
|
||||
|
||||
dbContext.Playlists.Remove(playlist);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ namespace ErsatzTV.Application.MediaCollections;
|
||||
public class RemoveItemsFromCollectionHandler : IRequestHandler<RemoveItemsFromCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ChannelWriter<ISearchIndexBackgroundServiceRequest> _searchChannel;
|
||||
|
||||
public RemoveItemsFromCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
|
||||
@@ -44,8 +44,6 @@ public abstract class TraktCommandBase
|
||||
dbContext.TraktLists
|
||||
.Include(l => l.Items)
|
||||
.ThenInclude(i => i.Guids)
|
||||
.Include(tl => tl.Playlist)
|
||||
.ThenInclude(tl => tl.Items)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == traktListId)
|
||||
.Map(o => o.ToValidation<BaseError>($"TraktList {traktListId} does not exist."));
|
||||
|
||||
@@ -94,7 +92,6 @@ public abstract class TraktCommandBase
|
||||
|
||||
list.Items.RemoveAll(toRemove.Contains);
|
||||
list.Items.AddRange(toAdd.Map(a => ProjectItem(list, a)));
|
||||
list.LastUpdate = DateTime.UtcNow;
|
||||
|
||||
foreach (TraktListItem existing in toUpdate)
|
||||
{
|
||||
@@ -175,45 +172,6 @@ public abstract class TraktCommandBase
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (list.GeneratePlaylist)
|
||||
{
|
||||
if (item.MediaItemId is not null)
|
||||
{
|
||||
// found matching item - sync as playlist item
|
||||
PlaylistItem playlistItem = list.Playlist.Items.FirstOrDefault(i => i.Index == item.Rank);
|
||||
if (playlistItem is null)
|
||||
{
|
||||
playlistItem = new PlaylistItem
|
||||
{
|
||||
Index = item.Rank,
|
||||
PlaylistId = list.Playlist.Id,
|
||||
Playlist = list.Playlist,
|
||||
IncludeInProgramGuide = true
|
||||
};
|
||||
|
||||
await dbContext.PlaylistItems.AddAsync(playlistItem);
|
||||
}
|
||||
|
||||
playlistItem.CollectionType = item.Kind switch
|
||||
{
|
||||
TraktListItemKind.Movie => ProgramScheduleItemCollectionType.Movie,
|
||||
TraktListItemKind.Show => ProgramScheduleItemCollectionType.TelevisionShow,
|
||||
TraktListItemKind.Season => ProgramScheduleItemCollectionType.TelevisionSeason,
|
||||
_ => ProgramScheduleItemCollectionType.Episode
|
||||
};
|
||||
|
||||
playlistItem.MediaItemId = item.MediaItemId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// could not find item; remove playlist item if present
|
||||
foreach (PlaylistItem maybeItem in list.Playlist.Items.Find(i => i.Index == item.Rank))
|
||||
{
|
||||
list.Playlist.Items.Remove(maybeItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
@@ -223,9 +181,6 @@ public abstract class TraktCommandBase
|
||||
|
||||
_searchIndex.Commit();
|
||||
|
||||
list.LastMatch = DateTime.UtcNow;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return list;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -50,14 +50,15 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
|
||||
.Filter(i => i.CollectionId.HasValue)
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
.Filter(i => c.MultiCollectionItems.All(i2 => i2.CollectionId != i.CollectionId.Value))
|
||||
.Map(i => new MultiCollectionItem
|
||||
{
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
CollectionId = i.CollectionId.Value,
|
||||
MultiCollectionId = c.Id,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
})
|
||||
.Map(
|
||||
i => new MultiCollectionItem
|
||||
{
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
CollectionId = i.CollectionId.Value,
|
||||
MultiCollectionId = c.Id,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
})
|
||||
.ToList();
|
||||
var toRemove = c.MultiCollectionItems
|
||||
.Filter(i => request.Items.All(i2 => i2.CollectionId != i.CollectionId))
|
||||
@@ -69,8 +70,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
|
||||
// update existing items
|
||||
foreach (MultiCollectionItem item in c.MultiCollectionItems)
|
||||
{
|
||||
foreach (UpdateMultiCollectionItem incoming in
|
||||
request.Items.Filter(i => i.CollectionId == item.CollectionId))
|
||||
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter(
|
||||
i => i.CollectionId == item.CollectionId))
|
||||
{
|
||||
item.ScheduleAsGroup = incoming.ScheduleAsGroup;
|
||||
item.PlaybackOrder = incoming.PlaybackOrder;
|
||||
@@ -84,14 +85,15 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
|
||||
.Filter(i => i.SmartCollectionId.HasValue)
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
.Filter(i => c.MultiCollectionSmartItems.All(i2 => i2.SmartCollectionId != i.SmartCollectionId.Value))
|
||||
.Map(i => new MultiCollectionSmartItem
|
||||
{
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
SmartCollectionId = i.SmartCollectionId.Value,
|
||||
MultiCollectionId = c.Id,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
})
|
||||
.Map(
|
||||
i => new MultiCollectionSmartItem
|
||||
{
|
||||
// ReSharper disable once PossibleInvalidOperationException
|
||||
SmartCollectionId = i.SmartCollectionId.Value,
|
||||
MultiCollectionId = c.Id,
|
||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||
PlaybackOrder = i.PlaybackOrder
|
||||
})
|
||||
.ToList();
|
||||
var toRemoveSmart = c.MultiCollectionSmartItems
|
||||
.Filter(i => request.Items.All(i2 => i2.SmartCollectionId != i.SmartCollectionId))
|
||||
@@ -103,8 +105,8 @@ public class UpdateMultiCollectionHandler : IRequestHandler<UpdateMultiCollectio
|
||||
// update existing items
|
||||
foreach (MultiCollectionSmartItem item in c.MultiCollectionSmartItems)
|
||||
{
|
||||
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter(i =>
|
||||
i.SmartCollectionId == item.SmartCollectionId))
|
||||
foreach (UpdateMultiCollectionItem incoming in request.Items.Filter(
|
||||
i => i.SmartCollectionId == item.SmartCollectionId))
|
||||
{
|
||||
item.ScheduleAsGroup = incoming.ScheduleAsGroup;
|
||||
item.PlaybackOrder = incoming.PlaybackOrder;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record UpdateTraktList(int Id, bool AutoRefresh, bool GeneratePlaylist) : IRequest<Option<BaseError>>;
|
||||
@@ -1,77 +0,0 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public class UpdateTraktListHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IEntityLocker entityLocker,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||
: IRequestHandler<UpdateTraktList, Option<BaseError>>
|
||||
{
|
||||
public async Task<Option<BaseError>> Handle(UpdateTraktList request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Option<TraktList> maybeTraktList = await dbContext.TraktLists
|
||||
.Include(tl => tl.Playlist)
|
||||
.SelectOneAsync(t => t.Id, t => t.Id == request.Id);
|
||||
|
||||
foreach (TraktList traktList in maybeTraktList)
|
||||
{
|
||||
traktList.AutoRefresh = request.AutoRefresh;
|
||||
traktList.GeneratePlaylist = request.GeneratePlaylist;
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
if (request.GeneratePlaylist)
|
||||
{
|
||||
if (traktList.PlaylistId is null)
|
||||
{
|
||||
PlaylistGroup traktListGroup = await dbContext.PlaylistGroups
|
||||
.Filter(pg => pg.IsSystem)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
var playlist = new Playlist
|
||||
{
|
||||
IsSystem = true,
|
||||
Name = $"{traktList.User}/{traktList.List}",
|
||||
PlaylistGroupId = traktListGroup.Id,
|
||||
PlaylistGroup = traktListGroup
|
||||
};
|
||||
|
||||
traktList.Playlist = playlist;
|
||||
|
||||
await dbContext.Playlists.AddAsync(playlist, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
if (entityLocker.LockTrakt())
|
||||
{
|
||||
await workerChannel.WriteAsync(new MatchTraktListItems(traktList.Id), cancellationToken);
|
||||
}
|
||||
}
|
||||
else if (traktList.PlaylistId is not null)
|
||||
{
|
||||
// delete playlist
|
||||
await dbContext.Playlists
|
||||
.Where(p => p.Id == traktList.PlaylistId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return Option<BaseError>.None;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BaseError.New(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using ErsatzTV.Application.Tree;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
@@ -30,9 +29,7 @@ internal static class Mapper
|
||||
$"{traktList.User}/{traktList.List}",
|
||||
traktList.Name,
|
||||
traktList.ItemCount,
|
||||
traktList.Items.Count(i => i.MediaItemId.HasValue),
|
||||
traktList.AutoRefresh,
|
||||
traktList.GeneratePlaylist);
|
||||
traktList.Items.Count(i => i.MediaItemId.HasValue));
|
||||
|
||||
private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) =>
|
||||
new(
|
||||
@@ -49,19 +46,11 @@ internal static class Mapper
|
||||
multiCollectionSmartItem.ScheduleAsGroup,
|
||||
multiCollectionSmartItem.PlaybackOrder);
|
||||
|
||||
internal static TreeViewModel ProjectToViewModel(List<PlaylistGroup> playlistGroups) =>
|
||||
new(
|
||||
playlistGroups.Map(bg => new TreeGroupViewModel(
|
||||
bg.Id,
|
||||
bg.Name,
|
||||
bg.Playlists.Map(b => new TreeItemViewModel(b.Id, b.Name, b.IsSystem)).ToList(),
|
||||
bg.IsSystem)).ToList());
|
||||
|
||||
internal static PlaylistGroupViewModel ProjectToViewModel(PlaylistGroup playlistGroup) =>
|
||||
new(playlistGroup.Id, playlistGroup.Name, playlistGroup.Playlists.Count, playlistGroup.IsSystem);
|
||||
new(playlistGroup.Id, playlistGroup.Name, playlistGroup.Playlists.Count);
|
||||
|
||||
internal static PlaylistViewModel ProjectToViewModel(Playlist playlist) =>
|
||||
new(playlist.Id, playlist.PlaylistGroupId, playlist.Name, playlist.IsSystem);
|
||||
new(playlist.Id, playlist.PlaylistGroupId, playlist.Name);
|
||||
|
||||
internal static PlaylistItemViewModel ProjectToViewModel(PlaylistItem playlistItem) =>
|
||||
new(
|
||||
|
||||
@@ -14,5 +14,4 @@ public record MediaCollectionViewModel(
|
||||
string.Empty,
|
||||
Name,
|
||||
string.Empty,
|
||||
State,
|
||||
HasMediaInfo: false);
|
||||
State);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record PlaylistGroupViewModel(int Id, string Name, int PlaylistCount, bool IsSystem);
|
||||
public record PlaylistGroupViewModel(int Id, string Name, int PlaylistCount);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record PlaylistViewModel(int Id, int PlaylistGroupId, string Name, bool IsSystem);
|
||||
public record PlaylistViewModel(int Id, int PlaylistGroupId, string Name);
|
||||
|
||||
@@ -15,8 +15,6 @@ public class GetAllPlaylistGroupsHandler(IDbContextFactory<TvContext> dbContextF
|
||||
|
||||
List<PlaylistGroup> playlistGroups = await dbContext.PlaylistGroups
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(pg => pg.IsSystem)
|
||||
.ThenBy(pg => pg.Name)
|
||||
.Include(g => g.Playlists)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Application.Tree;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record GetPlaylistTree : IRequest<TreeViewModel>;
|
||||
@@ -1,26 +0,0 @@
|
||||
using ErsatzTV.Application.Tree;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public class GetPlaylistTreeHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetPlaylistTree, TreeViewModel>
|
||||
{
|
||||
public async Task<TreeViewModel> Handle(
|
||||
GetPlaylistTree request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<PlaylistGroup> playlistGroups = await dbContext.PlaylistGroups
|
||||
.AsNoTracking()
|
||||
.OrderByDescending(pg => pg.IsSystem)
|
||||
.ThenBy(pg => pg.Name)
|
||||
.Include(g => g.Playlists)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Mapper.ProjectToViewModel(playlistGroups);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record GetTraktListById(int Id) : IRequest<Option<TraktListViewModel>>;
|
||||
@@ -1,18 +0,0 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public class GetTraktListByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetTraktListById, Option<TraktListViewModel>>
|
||||
{
|
||||
public async Task<Option<TraktListViewModel>> Handle(GetTraktListById request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.TraktLists
|
||||
.Include(tl => tl.Items)
|
||||
.SelectOneAsync(tl => tl.Id, tl => tl.Id == request.Id)
|
||||
.MapT(Mapper.ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
namespace ErsatzTV.Application.MediaCollections;
|
||||
|
||||
public record TraktListViewModel(
|
||||
int Id,
|
||||
int TraktId,
|
||||
string Slug,
|
||||
string Name,
|
||||
int ItemCount,
|
||||
int MatchCount,
|
||||
bool AutoRefresh,
|
||||
bool GeneratePlaylist);
|
||||
public record TraktListViewModel(int Id, int TraktId, string Slug, string Name, int ItemCount, int MatchCount);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user