Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda0318868 | ||
|
|
ac413f731a | ||
|
|
5a9d27e196 | ||
|
|
cd4a9c1d16 | ||
|
|
f6249d9fa4 | ||
|
|
e2ffa70529 | ||
|
|
3e07bc6136 | ||
|
|
d6bfc2fd05 | ||
|
|
35116c64cd | ||
|
|
037cee873f | ||
|
|
cd28afcd91 | ||
|
|
7457301d3e | ||
|
|
7b7d378df7 | ||
|
|
f6dcaf9108 | ||
|
|
6cc2f1de17 | ||
|
|
c6ee41484e | ||
|
|
36d38c740f | ||
|
|
0f795e4e2f | ||
|
|
583cbf7b14 | ||
|
|
27c701b936 | ||
|
|
6e2c19d354 | ||
|
|
4d83dc019c | ||
|
|
462057a4b1 | ||
|
|
a04c72788f | ||
|
|
f94a440b62 | ||
|
|
f80069bb97 | ||
|
|
c2769a08b4 | ||
|
|
e679fee940 | ||
|
|
2271d5497b | ||
|
|
f71b6527c0 | ||
|
|
20d2fe71cd | ||
|
|
1994f171d5 | ||
|
|
76f7c88375 | ||
|
|
3805b9e48c | ||
|
|
9267edbcc9 | ||
|
|
c4c164df6a | ||
|
|
b90463e3af | ||
|
|
1fb27e3cfa | ||
|
|
06f233e5bd | ||
|
|
9917774671 | ||
|
|
5be929da18 | ||
|
|
25f4fb22e5 | ||
|
|
b04b517f7b | ||
|
|
d756c0c7c0 | ||
|
|
20e5b8a11a | ||
|
|
5c8489cbed | ||
|
|
7cfa298c72 | ||
|
|
4b0faf4da1 | ||
|
|
07cbf9936b | ||
|
|
2138d6437c | ||
|
|
5b9601a57b | ||
|
|
aeda5050d3 | ||
|
|
ea46a7a5ca | ||
|
|
69a1e718df | ||
|
|
4a59dafe51 |
@@ -33,10 +33,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-13
|
||||
- os: macos-14
|
||||
kind: macOS
|
||||
target: osx-x64
|
||||
- os: macos-13
|
||||
- os: macos-14
|
||||
kind: macOS
|
||||
target: osx-arm64
|
||||
steps:
|
||||
@@ -48,6 +48,8 @@ jobs:
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.203
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
@@ -71,8 +73,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net8.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net8.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
|
||||
- name: Bundle
|
||||
shell: bash
|
||||
@@ -166,6 +168,8 @@ jobs:
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.203
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
@@ -178,7 +182,7 @@ jobs:
|
||||
id: downloadffmpeg
|
||||
name: Download ffmpeg
|
||||
with:
|
||||
url: "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2025-06-12-14-05/ffmpeg-n7.1.1-22-g0f1fe3d153-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
|
||||
@@ -190,8 +194,8 @@ jobs:
|
||||
|
||||
# Build everything
|
||||
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net8.0 --runtime "${{ matrix.target }}" -c Release -o "scanner" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net8.0 --runtime "${{ matrix.target }}" -c Release -o "main" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
mkdir "$release_name"
|
||||
mv scanner/* "$release_name/"
|
||||
mv main/* "$release_name/"
|
||||
|
||||
@@ -31,14 +31,6 @@ jobs:
|
||||
path: ''
|
||||
suffix: ''
|
||||
qemu: false
|
||||
- name: nvidia
|
||||
path: 'nvidia/'
|
||||
suffix: '-nvidia'
|
||||
qemu: false
|
||||
- name: vaapi
|
||||
path: 'vaapi/'
|
||||
suffix: '-vaapi'
|
||||
qemu: false
|
||||
- name: arm32v7
|
||||
path: 'arm32v7/'
|
||||
suffix: '-arm'
|
||||
@@ -82,12 +74,12 @@ jobs:
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker
|
||||
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 }}
|
||||
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
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.203
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
@@ -38,23 +40,25 @@ jobs:
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.203
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
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 --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
|
||||
build_and_test_mac:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v4
|
||||
@@ -64,6 +68,8 @@ jobs:
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.203
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
+118
-2
@@ -5,6 +5,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Add new channel stream (audio and subtitle) selector system
|
||||
- Channel editor has a new field `Stream Selector Mode`
|
||||
- `Default` maintains existing behavior
|
||||
- `Custom` uses a YAML config file
|
||||
- The YAML config contains a prioritized list of stream selector "items" (audio and subtitle criteria pairs)
|
||||
- The items are tested against the media from top to bottom, and when (at least) a matching audio track is found, stream selection occurs
|
||||
- As an example, the custom stream selector config can specify (in priority order):
|
||||
- english audio (and disable subtitles)
|
||||
- any other audio (and english subtitles, if they exist)
|
||||
- Criteria can include
|
||||
- Stream language
|
||||
- Stream title (allowed title and/or blocked title)
|
||||
- Stream condition, which is an expression that can use
|
||||
- `id` (index)
|
||||
- `title`
|
||||
- `lang`
|
||||
- `default`
|
||||
- `forced`
|
||||
- `sdh` (subtitle only)
|
||||
- `external` (subtitle only)
|
||||
- `codec`
|
||||
- `channels` (audio only)
|
||||
- An example subtitle condition: `lang like 'en%' and external`
|
||||
- An example audio condition: `title like '%movie%' and channels > 2`
|
||||
- Add new channel setting `Active Mode`
|
||||
- `Active` - default value, channel streams as normal and has normal visibility
|
||||
- `Hidden` - channel streams as normal and is hidden from M3U/XMLTV/HDHR
|
||||
- `Inactive` - channel cannot stream (will 404) and is hidden from M3U/XMLTV/HDHR
|
||||
- Synchronize Plex "network" metadata for Plex show libraries
|
||||
- Shows will have new `network` search field
|
||||
- Episodes will have new `show_network` search field
|
||||
- YAML playout: add `stop_before_end` setting to `pad_until` and `duration` instructions
|
||||
- When `stop_before_end: false`, content can run over the desired time before executing the next instruction
|
||||
- YAML playout: add `offline_tail` setting to `pad_until` instruction
|
||||
- This can be used to stop primary content before the desired time (`stop_before_end: true` and `offline_tail: false`)
|
||||
- You can then have a second `pad_until` with the same target time and different content
|
||||
- YAML playout: make `tomorrow` an expression on `pad_until` instruction
|
||||
- `true` and `false` still work as normal
|
||||
- The current time (as a decimal) can also be used in the expression, e.g. `now > 23`
|
||||
- `now = hours + minutes / 60.0 + seconds / 3600.0`
|
||||
- So `10:30 AM` would be `10.5`, `10:45 PM` would be `22.75`, etc
|
||||
- YAML playout: make `skip_items` an expression
|
||||
- The following parameters can be used:
|
||||
- `count`: the total number of items in the content
|
||||
- `random`: a random number between zero and (count - 1)
|
||||
- For example:
|
||||
- `count / 2` will start in the middle of the content
|
||||
- `random` will start at a random point in the content
|
||||
- `2` (similar to before this change) will skip the first two items in the content
|
||||
- YAML playout: make `count` an expression
|
||||
- The following parameters can be used:
|
||||
- `count`: the total number of items in the content
|
||||
- `random`: a random number between zero and (count - 1)
|
||||
- For example:
|
||||
- `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
|
||||
|
||||
### 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
|
||||
|
||||
### Fixed
|
||||
- Fix QSV acceleration in docker with older Intel devices
|
||||
- Fix HDR transcoding with NVIDIA accel for:
|
||||
- All NVIDIA docker users
|
||||
- Windows NVIDIA users who have set the `ETV_DISABLE_VULKAN` env var
|
||||
- Fix audio sync issue with QSV acceleration
|
||||
- YAML playout: fix history for marathon and playlist content
|
||||
- This allows playouts to be extended correctly, instead of always resetting to the earliest item in each group
|
||||
- Fix using channel External Logo URL as watermark
|
||||
- Fix display of SVG channel logo and watermark in admin UI
|
||||
- Existing SVG logos and watermarks will have to be re-uploaded to display properly in the admin UI
|
||||
- This does not affect streaming at all; existing artwork still works fine for streaming
|
||||
- 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
|
||||
|
||||
## [25.2.0] - 2025-06-24
|
||||
### Added
|
||||
- Add `linux-musl-x64` artifact for users running Alpine x64
|
||||
- Add API endpoint to empty trash (POST to `/api/maintenance/empty_trash`)
|
||||
- e.g. `curl -XPOST -d '' http://localhost:8409/api/maintenance/empty_trash`
|
||||
@@ -26,8 +108,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- VAAPI may use hardware-accelerated tone mapping (when opencl accel is also available)
|
||||
- NVIDIA may use hardware-accelerated tone mapping (when vulkan accel and libplacebo filter are also available)
|
||||
- QSV may use hardware-accelerated tone mapping (when hardware decoding is used)
|
||||
- In all other cases, HDR content will use a software pipeline and the clip algorithm
|
||||
- In all other cases, HDR content will use a software pipeline
|
||||
- The tonemap algorithm can be configured in the ffmpeg profile
|
||||
- Use hardware-accelerated padding with VAAPI
|
||||
- Add environment variable `ETV_DISABLE_VULKAN`
|
||||
- Any non-empty value will disable use of Vulkan acceleration and force software tonemapping
|
||||
- This may be needed with misbehaving NVIDIA drivers on Windows
|
||||
- Add health check error when invalid VAAPI device and VAAPI driver combination is used in an active ffmpeg profile
|
||||
- This makes it obvious when hardware acceleration will not work as configured
|
||||
- Add button in schedule editor to clone schedule item
|
||||
- Allow YAML playout sequence definitions to reference other sequences
|
||||
- Cycles will be detected and logged, and sequences with cycles will prevent the playout from building
|
||||
- Add `repeat` property to YAML sequence instruction
|
||||
- This tells the playout builder how many times this sequence should repeat
|
||||
- Omitting this value is the same as setting it to `1`
|
||||
- Add `collection` (name) to search index for manual collections created within ETV
|
||||
- Collections synchronized from media servers are still indexed as `tag`
|
||||
- Allow searching by `smart_collection` (name)
|
||||
- Quotes are *always* required around each collection name when using this feature
|
||||
- e.g. `smart_collection:"one" OR smart_collection:"two"`
|
||||
- Cycles will be detected and logged, and searches with cycles will not work as expected
|
||||
- Add all `ETV_*` environment variables to Troubleshooting > General info
|
||||
- Add `External Logo URL` field to channel editor
|
||||
- Using external (public) logos should fix channel logo display for clients that don't proxy artwork (such as Plex)
|
||||
- Users who have customized the XMLTV channel template `channel.sbntxt` will need to update their templates again
|
||||
- This is because the templates require different logic for external URLs vs ETV-hosted URLs
|
||||
|
||||
### Changed
|
||||
- Start to make UI minimally responsive (functional on smaller screens)
|
||||
@@ -39,7 +144,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Remove requirement to have Jellyfin admin user; user id is no longer required on requests to latest Jellyfin server
|
||||
- Upgrade bundled ffmpeg on Windows from 6.1 to 7.1.1
|
||||
- Upgrade VAAPI docker image Ubuntu base from 22 to 24; bundled ffmpeg from 6.1 to 7.1.1
|
||||
- Upgrade NVIDIA docker image Ubuntu base from 20 to 24; bundled ffmpeg from 6.1 to 7.1.1
|
||||
- Upgrade base, arm, arm64 docker images bundled ffmpeg from 6.1 to 7.1.1
|
||||
- Unify all hardware acceleration methods in base docker images (`latest` and `develop`)
|
||||
- VAAPI, QSV and NVIDIA are now all supported in the base docker image
|
||||
- Other docker image tags are deprecated and will receive no new updates after the next release
|
||||
- A health check has been added to notify users (on `-vaapi` or `-nvidia` tags) of this change
|
||||
- Schedule items editor: show currently selected row using background color instead of font weight
|
||||
|
||||
### Fixed
|
||||
- Fix error message about synchronizing Plex collections from a Plex server that has zero collections
|
||||
@@ -50,6 +161,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Use cache busting to avoid UI errors after upgrading the MudBlazor library
|
||||
- Fix multi-variant playlist to report more accurate `BANDWIDTH` value based on ffmpeg profile
|
||||
- Fix detecting NVIDIA capabilities on Blackwell GPUs
|
||||
- Fix decoder selection in NVIDIA pipeline
|
||||
- Prevent playback order `Shuffle In Order` from being used with `Fill With Group Mode` as they are incompatible
|
||||
- Fix XMLTV items not grouping properly (guide mode: `Filler`) due to post-roll filler
|
||||
|
||||
## [25.1.0] - 2025-01-10
|
||||
### Added
|
||||
@@ -2219,7 +2333,9 @@ 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/v0.8.8-beta...HEAD
|
||||
[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
|
||||
[0.8.7-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.6-beta...v0.8.7-beta
|
||||
[0.8.6-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.5-beta...v0.8.6-beta
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use special_folder::SpecialFolder;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Child;
|
||||
@@ -21,11 +22,16 @@ fn main() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
tray.add_menu_item("Launch Web UI", || {
|
||||
let ui_port = env::var("ETV_UI_PORT")
|
||||
.ok()
|
||||
.and_then(|val| val.parse::<u16>().ok())
|
||||
.unwrap_or(8409);
|
||||
|
||||
let _ = Command::new("cmd")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("/C")
|
||||
.arg("start")
|
||||
.arg("http://localhost:8409")
|
||||
.arg(format!("http://localhost:{}", ui_port))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
|
||||
+1
-1
Submodule ErsatzTV-macOS updated: bf18a5e4a4...d4dd985fd6
@@ -0,0 +1,17 @@
|
||||
using System.Net;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Artworks;
|
||||
|
||||
public record ArtworkContentTypeModel(string Path, string ContentType)
|
||||
{
|
||||
public static readonly ArtworkContentTypeModel None = new(string.Empty, string.Empty);
|
||||
|
||||
public bool IsExternalUrl => Artwork.IsExternalUrl(Path);
|
||||
|
||||
public bool HasContentType => !string.IsNullOrWhiteSpace(ContentType);
|
||||
|
||||
public string UrlWithContentType => string.IsNullOrWhiteSpace(ContentType)
|
||||
? Path
|
||||
: $"{Path}?contentType={WebUtility.UrlEncode(ContentType)}";
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using System.Net;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
@@ -10,7 +11,9 @@ public record ChannelViewModel(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
@@ -22,7 +25,8 @@ public record ChannelViewModel(
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode)
|
||||
ChannelSongVideoMode SongVideoMode,
|
||||
ChannelActiveMode ActiveMode)
|
||||
{
|
||||
public string WebEncodedName => WebUtility.UrlEncode(Name);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
@@ -9,7 +10,9 @@ public record CreateChannel(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
@@ -20,4 +23,5 @@ public record CreateChannel(
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
ChannelSongVideoMode SongVideoMode,
|
||||
ChannelActiveMode ActiveMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
|
||||
@@ -49,13 +49,20 @@ public class CreateChannelHandler(
|
||||
fillerPresetId) =>
|
||||
{
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
|
||||
{
|
||||
string logo = request.Logo.Path;
|
||||
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||
{
|
||||
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||
}
|
||||
|
||||
artwork.Add(
|
||||
new Artwork
|
||||
{
|
||||
Path = request.Logo,
|
||||
Path = logo,
|
||||
ArtworkKind = ArtworkKind.Logo,
|
||||
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType) ? request.Logo.ContentType : null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow
|
||||
});
|
||||
@@ -71,13 +78,16 @@ public class CreateChannelHandler(
|
||||
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
|
||||
SongVideoMode = request.SongVideoMode,
|
||||
ActiveMode = request.ActiveMode
|
||||
};
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
|
||||
@@ -49,6 +49,18 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
|
||||
_localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder);
|
||||
|
||||
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
int inactiveCount = await dbContext.Channels
|
||||
.Where(c => c.Number == request.ChannelNumber && c.ActiveMode != ChannelActiveMode.Active)
|
||||
.CountAsync(cancellationToken);
|
||||
if (inactiveCount > 0)
|
||||
{
|
||||
File.Delete(targetFile);
|
||||
return;
|
||||
}
|
||||
|
||||
string movieTemplateFileName = GetMovieTemplateFileName();
|
||||
string episodeTemplateFileName = GetEpisodeTemplateFileName();
|
||||
string musicVideoTemplateFileName = GetMusicVideoTemplateFileName();
|
||||
@@ -85,8 +97,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
string otherVideoText = await File.ReadAllTextAsync(otherVideoTemplateFileName, cancellationToken);
|
||||
var otherVideoTemplate = Template.Parse(otherVideoText, otherVideoTemplateFileName);
|
||||
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<Playout> playouts = await dbContext.Playouts
|
||||
.AsNoTracking()
|
||||
.Filter(pi => pi.Channel.Number == request.ChannelNumber)
|
||||
@@ -244,7 +254,6 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
string tempFile = Path.GetTempFileName();
|
||||
await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken);
|
||||
|
||||
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
|
||||
File.Move(tempFile, targetFile, true);
|
||||
}
|
||||
|
||||
@@ -287,24 +296,11 @@ 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.Tail or FillerKind.Fallback or FillerKind.DecoDefault))
|
||||
or FillerKind.PostRoll or FillerKind.Tail or FillerKind.Fallback or FillerKind.DecoDefault))
|
||||
{
|
||||
finishIndex++;
|
||||
}
|
||||
|
||||
int customShowId = -1;
|
||||
if (displayItem.MediaItem is Episode ep)
|
||||
{
|
||||
customShowId = ep.Season.ShowId;
|
||||
}
|
||||
|
||||
bool isSameCustomShow = hasCustomTitle;
|
||||
for (int x = j; x <= finishIndex; x++)
|
||||
{
|
||||
isSameCustomShow = isSameCustomShow && sorted[x].MediaItem is Episode e &&
|
||||
customShowId == e.Season.ShowId;
|
||||
}
|
||||
|
||||
PlayoutItem finishItem = sorted[finishIndex];
|
||||
i = finishIndex;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Net;
|
||||
using System.Xml;
|
||||
using Dapper;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -77,6 +78,9 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
||||
|
||||
await foreach (ChannelResult channel in GetChannels(dbContext).WithCancellation(cancellationToken))
|
||||
{
|
||||
bool hasLogo = !string.IsNullOrWhiteSpace(channel.ArtworkPath);
|
||||
bool hasExternalLogo = hasLogo && Artwork.IsExternalUrl(channel.ArtworkPath);
|
||||
|
||||
var data = new
|
||||
{
|
||||
ChannelId = ChannelIdentifier.FromNumber(channel.Number),
|
||||
@@ -84,7 +88,8 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
||||
ChannelNumber = channel.Number,
|
||||
ChannelName = channel.Name,
|
||||
ChannelCategories = GetCategories(channel.Categories),
|
||||
ChannelHasArtwork = !string.IsNullOrWhiteSpace(channel.ArtworkPath),
|
||||
ChannelHasExternalArtwork = hasExternalLogo,
|
||||
ChannelHasArtwork = hasLogo,
|
||||
ChannelArtworkPath = channel.ArtworkPath,
|
||||
ChannelNameEncoded = WebUtility.UrlEncode(channel.Name)
|
||||
};
|
||||
@@ -113,7 +118,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
|
||||
const string QUERY = @"select C.Number, C.Name, C.Categories, A.Path as ArtworkPath
|
||||
from Channel C
|
||||
left outer join Artwork A on C.Id = A.ChannelId and A.ArtworkKind = 2
|
||||
where C.Id in (select ChannelId from Playout)
|
||||
where C.Id in (select ChannelId from Playout) and C.ActiveMode = 0
|
||||
order by CAST(C.Number as double)";
|
||||
// TODO: this needs to be fixed for sqlite/mariadb
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
@@ -10,7 +11,9 @@ public record UpdateChannel(
|
||||
string Group,
|
||||
string Categories,
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
ArtworkContentTypeModel Logo,
|
||||
ChannelStreamSelectorMode StreamSelectorMode,
|
||||
string StreamSelector,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
ChannelProgressMode ProgressMode,
|
||||
@@ -21,4 +24,5 @@ public record UpdateChannel(
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
ChannelSongVideoMode SongVideoMode,
|
||||
ChannelActiveMode ActiveMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Subtitles;
|
||||
using ErsatzTV.Core;
|
||||
@@ -35,6 +34,8 @@ public class UpdateChannelHandler(
|
||||
c.Group = update.Group;
|
||||
c.Categories = update.Categories;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.StreamSelectorMode = update.StreamSelectorMode;
|
||||
c.StreamSelector = update.StreamSelector;
|
||||
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
||||
c.PreferredAudioTitle = update.PreferredAudioTitle;
|
||||
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||
@@ -42,30 +43,53 @@ public class UpdateChannelHandler(
|
||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||
c.SongVideoMode = update.SongVideoMode;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
c.ActiveMode = update.ActiveMode;
|
||||
c.Artwork ??= [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo?.Path))
|
||||
{
|
||||
Option<Artwork> maybeLogo =
|
||||
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
|
||||
string logo = update.Logo.Path;
|
||||
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
|
||||
{
|
||||
logo = logo.Replace("iptv/logos/", string.Empty);
|
||||
}
|
||||
|
||||
maybeLogo.Match(
|
||||
artwork =>
|
||||
Option<Artwork> maybeLogo = c.Artwork.Where(a => a.ArtworkKind == ArtworkKind.Logo).HeadOrNone();
|
||||
foreach (Artwork artwork in maybeLogo)
|
||||
{
|
||||
artwork.Path = logo;
|
||||
artwork.OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
|
||||
? update.Logo.ContentType
|
||||
: null;
|
||||
artwork.DateUpdated = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (maybeLogo.IsNone)
|
||||
{
|
||||
var artwork = new Artwork
|
||||
{
|
||||
artwork.Path = update.Logo;
|
||||
artwork.DateUpdated = DateTime.UtcNow;
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var artwork = new Artwork
|
||||
{
|
||||
Path = update.Logo,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow,
|
||||
ArtworkKind = ArtworkKind.Logo
|
||||
};
|
||||
c.Artwork.Add(artwork);
|
||||
});
|
||||
Path = logo,
|
||||
OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
|
||||
? update.Logo.ContentType
|
||||
: null,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow,
|
||||
ArtworkKind = ArtworkKind.Logo
|
||||
};
|
||||
c.Artwork.Add(artwork);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await dbContext.Entry(c)
|
||||
.Collection(channel => channel.Artwork)
|
||||
.LoadAsync();
|
||||
|
||||
foreach (Artwork artwork in c.Artwork.Where(x => x.ArtworkKind is ArtworkKind.Logo).ToList())
|
||||
{
|
||||
c.Artwork.Remove(artwork);
|
||||
dbContext.Artwork.Remove(artwork);
|
||||
}
|
||||
}
|
||||
|
||||
c.ProgressMode = update.ProgressMode;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Api.Channels;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core.Api.Channels;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
@@ -14,6 +15,8 @@ internal static class Mapper
|
||||
channel.Categories,
|
||||
channel.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.StreamSelectorMode,
|
||||
channel.StreamSelector,
|
||||
channel.PreferredAudioLanguageCode,
|
||||
channel.PreferredAudioTitle,
|
||||
channel.ProgressMode,
|
||||
@@ -25,7 +28,8 @@ internal static class Mapper
|
||||
channel.SubtitleMode,
|
||||
channel.MusicVideoCreditsMode,
|
||||
channel.MusicVideoCreditsTemplate,
|
||||
channel.SongVideoMode);
|
||||
channel.SongVideoMode,
|
||||
channel.ActiveMode);
|
||||
|
||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||
new(
|
||||
@@ -42,9 +46,21 @@ internal static class Mapper
|
||||
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
|
||||
new(resolution.Height, resolution.Width, bitrate);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
private static ArtworkContentTypeModel GetLogo(Channel channel)
|
||||
{
|
||||
Option<Artwork> maybeArtwork = channel.Artwork
|
||||
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||
.HeadOrNone();
|
||||
|
||||
foreach (Artwork artwork in maybeArtwork)
|
||||
{
|
||||
return artwork.IsExternalUrl()
|
||||
? new ArtworkContentTypeModel(artwork.Path, string.Empty)
|
||||
: new ArtworkContentTypeModel($"iptv/logos/{artwork.Path}", artwork.OriginalContentType);
|
||||
}
|
||||
|
||||
return ArtworkContentTypeModel.None;
|
||||
}
|
||||
|
||||
private static string GetStreamingMode(Channel channel) =>
|
||||
channel.StreamingMode switch
|
||||
|
||||
@@ -3,13 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
|
||||
public class GetChannelByIdHandler(IChannelRepository channelRepository)
|
||||
: IRequestHandler<GetChannelById, Option<ChannelViewModel>>
|
||||
{
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
|
||||
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||
|
||||
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetChannel(request.Id)
|
||||
channelRepository.GetChannel(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -29,6 +31,12 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var inactiveChannelNumbers = dbContext.Channels
|
||||
.Where(c => c.ActiveMode != ChannelActiveMode.Active)
|
||||
.Select(c => c.Number)
|
||||
.AsEnumerable()
|
||||
.Select(n => $"{n}.xml")
|
||||
.ToImmutableHashSet();
|
||||
|
||||
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
|
||||
if (!_localFileSystem.FileExists(channelsFile))
|
||||
@@ -60,6 +68,11 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inactiveChannelNumbers.Contains(Path.GetFileName(fileName)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
|
||||
|
||||
channelDataFragment = channelDataFragment
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Hdhr;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Hdhr;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
@@ -11,5 +12,5 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
|
||||
|
||||
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
.Map(channels => channels.Where(c => c.ActiveMode is ChannelActiveMode.Active).Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
||||
var result = new List<Channel>();
|
||||
foreach (Channel channel in channels)
|
||||
{
|
||||
if (channel.ActiveMode is not ChannelActiveMode.Active)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (mode.ToLowerInvariant())
|
||||
{
|
||||
case "segmenter":
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelStreamSelectors : IRequest<List<string>>;
|
||||
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class GetChannelStreamSelectorsHandler(ILocalFileSystem localFileSystem)
|
||||
: IRequestHandler<GetChannelStreamSelectors, List<string>>
|
||||
{
|
||||
public Task<List<string>> Handle(GetChannelStreamSelectors request, CancellationToken cancellationToken) =>
|
||||
localFileSystem.ListFiles(FileSystemLayout.ChannelStreamSelectorsFolder)
|
||||
.Map(Path.GetFileName)
|
||||
.ToList()
|
||||
.AsTask();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<NoWarn>VSTHRD200</NoWarn>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AnalysisLevel>latest-Recommended</AnalysisLevel>
|
||||
@@ -10,18 +10,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.8.2" />
|
||||
<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.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.13.61">
|
||||
<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.17.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ public record CreateFFmpegProfile(
|
||||
FFmpegProfileBitDepth BitDepth,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||
FFmpegProfileAudioFormat AudioFormat,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
|
||||
@@ -60,6 +60,7 @@ public class CreateFFmpegProfileHandler :
|
||||
BitDepth = request.BitDepth,
|
||||
VideoBitrate = request.VideoBitrate,
|
||||
VideoBufferSize = request.VideoBufferSize,
|
||||
TonemapAlgorithm = request.TonemapAlgorithm,
|
||||
AudioFormat = request.AudioFormat,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
|
||||
@@ -22,6 +22,7 @@ public record UpdateFFmpegProfile(
|
||||
FFmpegProfileBitDepth BitDepth,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||
FFmpegProfileAudioFormat AudioFormat,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
|
||||
@@ -54,6 +54,7 @@ public class
|
||||
|
||||
p.VideoBitrate = update.VideoBitrate;
|
||||
p.VideoBufferSize = update.VideoBufferSize;
|
||||
p.TonemapAlgorithm = update.TonemapAlgorithm;
|
||||
p.AudioFormat = update.AudioFormat;
|
||||
p.AudioBitrate = update.AudioBitrate;
|
||||
p.AudioBufferSize = update.AudioBufferSize;
|
||||
|
||||
@@ -22,6 +22,7 @@ public record FFmpegProfileViewModel(
|
||||
FFmpegProfileBitDepth BitDepth,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||
FFmpegProfileAudioFormat AudioFormat,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
|
||||
@@ -24,6 +24,7 @@ internal static class Mapper
|
||||
profile.BitDepth,
|
||||
profile.VideoBitrate,
|
||||
profile.VideoBufferSize,
|
||||
profile.TonemapAlgorithm,
|
||||
profile.AudioFormat,
|
||||
profile.AudioBitrate,
|
||||
profile.AudioBufferSize,
|
||||
@@ -54,6 +55,7 @@ internal static class Mapper
|
||||
(int)ffmpegProfile.VideoFormat,
|
||||
ffmpegProfile.VideoBitrate,
|
||||
ffmpegProfile.VideoBufferSize,
|
||||
(int)ffmpegProfile.TonemapAlgorithm,
|
||||
(int)ffmpegProfile.AudioFormat,
|
||||
ffmpegProfile.AudioBitrate,
|
||||
ffmpegProfile.AudioBufferSize,
|
||||
|
||||
@@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain;
|
||||
namespace ErsatzTV.Application.Images;
|
||||
|
||||
// ReSharper disable once SuggestBaseTypeForParameter
|
||||
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
|
||||
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType) : IRequest<Either<BaseError, string>>;
|
||||
|
||||
@@ -3,5 +3,5 @@ using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Images;
|
||||
|
||||
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, int? MaxHeight = null) : IRequest<
|
||||
public record GetCachedImagePath(string FileName, ArtworkKind ArtworkKind, string ContentType, int? MaxHeight = null) : IRequest<
|
||||
Either<BaseError, CachedImagePathViewModel>>;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class
|
||||
{
|
||||
try
|
||||
{
|
||||
MimeType mimeType;
|
||||
string mimeType;
|
||||
|
||||
string cachePath = _imageCache.GetPathForImage(
|
||||
request.FileName,
|
||||
@@ -84,7 +84,7 @@ public class
|
||||
|
||||
File.Move(withExtension, cachePath);
|
||||
|
||||
mimeType = new MimeType("image/jpeg");
|
||||
mimeType = "image/jpeg";
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -93,10 +93,12 @@ public class
|
||||
}
|
||||
else
|
||||
{
|
||||
mimeType = MimeTypes.GetMimeTypeFromFile(cachePath);
|
||||
mimeType = !string.IsNullOrWhiteSpace(request.ContentType)
|
||||
? request.ContentType
|
||||
: MimeTypes.GetMimeTypeFromFile(cachePath).Name;
|
||||
}
|
||||
|
||||
return new CachedImagePathViewModel(cachePath, mimeType.Name);
|
||||
return new CachedImagePathViewModel(cachePath, mimeType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,76 +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."));
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public record CreateLocalLibraryPath(int LibraryId, string Path)
|
||||
: IRequest<Either<BaseError, LocalLibraryPathViewModel>>;
|
||||
@@ -1,51 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using static ErsatzTV.Application.Libraries.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Libraries;
|
||||
|
||||
public class CreateLocalLibraryPathHandler : IRequestHandler<CreateLocalLibraryPath,
|
||||
Either<BaseError, LocalLibraryPathViewModel>>
|
||||
{
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
|
||||
public CreateLocalLibraryPathHandler(ILibraryRepository libraryRepository) =>
|
||||
_libraryRepository = libraryRepository;
|
||||
|
||||
public Task<Either<BaseError, LocalLibraryPathViewModel>> Handle(
|
||||
CreateLocalLibraryPath request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request).MapT(PersistLocalLibraryPath).Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<LocalLibraryPathViewModel> PersistLocalLibraryPath(LibraryPath p) =>
|
||||
_libraryRepository.Add(p).Map(ProjectToViewModel);
|
||||
|
||||
private Task<Validation<BaseError, LibraryPath>> Validate(CreateLocalLibraryPath request) =>
|
||||
ValidateFolder(request)
|
||||
.MapT(
|
||||
folder =>
|
||||
new LibraryPath
|
||||
{
|
||||
LibraryId = request.LibraryId,
|
||||
Path = folder
|
||||
});
|
||||
|
||||
private async Task<Validation<BaseError, string>> ValidateFolder(CreateLocalLibraryPath request)
|
||||
{
|
||||
List<string> allPaths = await _libraryRepository.GetLocalPaths(request.LibraryId)
|
||||
.Map(list => list.Map(c => c.Path).ToList());
|
||||
|
||||
return Optional(request.Path)
|
||||
.Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||
}
|
||||
|
||||
private static bool AreSubPaths(string path1, string path2)
|
||||
{
|
||||
string one = path1 + Path.DirectorySeparatorChar;
|
||||
string two = path2 + Path.DirectorySeparatorChar;
|
||||
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
|
||||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -19,23 +19,44 @@ public abstract class LocalLibraryHandlerBase
|
||||
LocalLibrary localLibrary,
|
||||
int? existingLibraryId = null)
|
||||
{
|
||||
List<string> allPaths = await dbContext.LocalLibraries
|
||||
List<LocalPath> allPaths = await dbContext.LocalLibraries
|
||||
.Include(ll => ll.Paths)
|
||||
.Filter(ll => existingLibraryId == null || ll.Id != existingLibraryId)
|
||||
.ToListAsync()
|
||||
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
||||
.Map(list => list.SelectMany(ll => ll.Paths.Map(lp => new LocalPath(ll.MediaKind, lp.Path))).ToList());
|
||||
|
||||
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
||||
var localPaths = localLibrary.Paths.Map(lp => new LocalPath(localLibrary.MediaKind, lp.Path)).ToList();
|
||||
|
||||
return Optional(localPaths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder))))
|
||||
.Where(length => length == 0)
|
||||
.Map(_ => localLibrary)
|
||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||
}
|
||||
|
||||
private static bool AreSubPaths(string path1, string path2)
|
||||
private static bool AreSubPaths(LocalPath path1, LocalPath path2)
|
||||
{
|
||||
string one = path1 + Path.DirectorySeparatorChar;
|
||||
string two = path2 + Path.DirectorySeparatorChar;
|
||||
return one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
|
||||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
|
||||
string one = path1.Path + Path.DirectorySeparatorChar;
|
||||
string two = path2.Path + Path.DirectorySeparatorChar;
|
||||
|
||||
bool isConflict = one == two || one.StartsWith(two, StringComparison.OrdinalIgnoreCase) ||
|
||||
two.StartsWith(one, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 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);
|
||||
|
||||
if (imagesAndOtherVideos)
|
||||
{
|
||||
isConflict = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isConflict;
|
||||
}
|
||||
|
||||
protected record LocalPath(LibraryMediaKind MediaKind, string Path);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public class UpdateLocalLibraryHandler : LocalLibraryHandlerBase,
|
||||
{
|
||||
Name = request.Name,
|
||||
Paths = request.Paths.Map(p => new LibraryPath { Id = p.Id, Path = p.Path }).ToList(),
|
||||
MediaKind = existing.MediaKind,
|
||||
MediaSourceId = existing.Id
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U
|
||||
EmptyTrash request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult result = await _searchIndex.Search(_client, "state:FileNotFound", 0, 10_000);
|
||||
SearchResult result = await _searchIndex.Search(_client, "state:FileNotFound", string.Empty, 0, 10_000);
|
||||
var ids = result.Items.Map(i => i.Id).ToList();
|
||||
|
||||
// ElasticSearch remove items may fail, so do that first
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddArtistToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -41,6 +45,8 @@ public class AddArtistToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Artist);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Artist.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddEpisodeToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -43,6 +47,8 @@ public class AddEpisodeToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Episode);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Episode.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -13,17 +14,20 @@ 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;
|
||||
|
||||
public AddImageToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -40,6 +44,8 @@ public class AddImageToCollectionHandler : IRequestHandler<AddImageToCollection,
|
||||
parameters.Collection.MediaItems.Add(parameters.Image);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Image.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,6 +15,7 @@ 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;
|
||||
@@ -24,13 +26,15 @@ public class AddItemsToCollectionHandler :
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IMovieRepository movieRepository,
|
||||
ITelevisionRepository televisionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_movieRepository = movieRepository;
|
||||
_televisionRepository = televisionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -67,6 +71,8 @@ public class AddItemsToCollectionHandler :
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems(toAddIds.ToArray()));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddMovieToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -41,6 +45,8 @@ public class AddMovieToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Movie);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Movie.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddMusicVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -43,6 +47,8 @@ public class AddMusicVideoToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.MusicVideo);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.MusicVideo.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddOtherVideoToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -43,6 +47,8 @@ public class AddOtherVideoToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.OtherVideo);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.OtherVideo.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddSeasonToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -41,6 +45,8 @@ public class AddSeasonToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Season);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Season.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddShowToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -41,6 +45,8 @@ public class AddShowToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Show);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Show.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -14,17 +15,20 @@ 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;
|
||||
|
||||
public AddSongToCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -41,6 +45,8 @@ public class AddSongToCollectionHandler :
|
||||
parameters.Collection.MediaItems.Add(parameters.Song);
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems([parameters.Song.Id]));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
@@ -12,11 +13,16 @@ public class CreateSmartCollectionHandler :
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly ISmartCollectionCache _smartCollectionCache;
|
||||
|
||||
public CreateSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
public CreateSmartCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ISearchTargets searchTargets,
|
||||
ISmartCollectionCache smartCollectionCache)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
_smartCollectionCache = smartCollectionCache;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, SmartCollectionViewModel>> Handle(
|
||||
@@ -35,6 +41,7 @@ public class CreateSmartCollectionHandler :
|
||||
await dbContext.SmartCollections.AddAsync(smartCollection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
await _smartCollectionCache.Refresh();
|
||||
return ProjectToViewModel(smartCollection);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -11,11 +12,16 @@ public class DeleteSmartCollectionHandler : IRequestHandler<DeleteSmartCollectio
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly ISmartCollectionCache _smartCollectionCache;
|
||||
|
||||
public DeleteSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
|
||||
public DeleteSmartCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ISearchTargets searchTargets,
|
||||
ISmartCollectionCache smartCollectionCache)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_searchTargets = searchTargets;
|
||||
_smartCollectionCache = smartCollectionCache;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -32,6 +38,7 @@ public class DeleteSmartCollectionHandler : IRequestHandler<DeleteSmartCollectio
|
||||
dbContext.SmartCollections.Remove(smartCollection);
|
||||
await dbContext.SaveChangesAsync();
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
await _smartCollectionCache.Refresh();
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Application.Search;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
@@ -13,17 +14,20 @@ 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;
|
||||
|
||||
public RemoveItemsFromCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> searchChannel)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchChannel = searchChannel;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -48,6 +52,8 @@ public class RemoveItemsFromCollectionHandler : IRequestHandler<RemoveItemsFromC
|
||||
|
||||
if (itemsToRemove.Count != 0 && await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
await _searchChannel.WriteAsync(new ReindexMediaItems(itemsToRemove.Select(mi => mi.Id).ToArray()));
|
||||
|
||||
// refresh all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collection.Id))
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -17,17 +18,20 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
private readonly ISearchTargets _searchTargets;
|
||||
private readonly ISmartCollectionCache _smartCollectionCache;
|
||||
|
||||
public UpdateSmartCollectionHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
ISearchTargets searchTargets)
|
||||
ISearchTargets searchTargets,
|
||||
ISmartCollectionCache smartCollectionCache)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_channel = channel;
|
||||
_searchTargets = searchTargets;
|
||||
_smartCollectionCache = smartCollectionCache;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
@@ -47,6 +51,7 @@ public class UpdateSmartCollectionHandler : IRequestHandler<UpdateSmartCollectio
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
_searchTargets.SearchTargetsChanged();
|
||||
await _smartCollectionCache.Refresh();
|
||||
|
||||
// refresh all playouts that use this smart collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingSmartCollection(request.Id))
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Globalization;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Plex;
|
||||
|
||||
public class CallPlexNetworkScannerHandler : CallLibraryScannerHandler<SynchronizePlexNetworks>,
|
||||
IRequestHandler<SynchronizePlexNetworks, Either<BaseError, Unit>>
|
||||
{
|
||||
public CallPlexNetworkScannerHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||
IMediator mediator,
|
||||
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>>
|
||||
Handle(SynchronizePlexNetworks request, CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, string> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
scanner => PerformScan(scanner, request, cancellationToken),
|
||||
error =>
|
||||
{
|
||||
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||
{
|
||||
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
|
||||
}
|
||||
|
||||
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
|
||||
});
|
||||
}
|
||||
|
||||
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizePlexNetworks request)
|
||||
{
|
||||
DateTime minDateTime = await dbContext.PlexLibraries
|
||||
.Filter(l => l.MediaKind == LibraryMediaKind.Shows)
|
||||
.SelectOneAsync(l => l.Id, l => l.Id == request.PlexLibraryId)
|
||||
.Match(l => l.LastNetworksScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||
|
||||
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
protected override bool ScanIsRequired(
|
||||
DateTimeOffset lastScan,
|
||||
int libraryRefreshInterval,
|
||||
SynchronizePlexNetworks request)
|
||||
{
|
||||
if (lastScan == SystemTime.MaxValueUtc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, Unit>> PerformScan(
|
||||
string scanner,
|
||||
SynchronizePlexNetworks request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"scan-plex-networks", request.PlexLibraryId.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
if (request.ForceScan)
|
||||
{
|
||||
arguments.Add("--force");
|
||||
}
|
||||
|
||||
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Plex;
|
||||
|
||||
public record SynchronizePlexNetworks(int PlexLibraryId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
|
||||
IScannerBackgroundServiceRequest;
|
||||
@@ -99,6 +99,12 @@ public class ReplaceProgramScheduleItemsHandler : ProgramScheduleItemCommandBase
|
||||
var keyOrders = new Dictionary<CollectionKey, System.Collections.Generic.HashSet<PlaybackOrder>>();
|
||||
foreach (ReplaceProgramScheduleItem item in request.Items)
|
||||
{
|
||||
if (item.PlaybackOrder is PlaybackOrder.ShuffleInOrder &&
|
||||
item.FillWithGroupMode is not FillWithGroupMode.None)
|
||||
{
|
||||
return new BaseError("Shuffle in Order cannot be used with Fill With Group Mode");
|
||||
}
|
||||
|
||||
var key = new CollectionKey(
|
||||
item.CollectionType,
|
||||
item.CollectionId,
|
||||
|
||||
@@ -72,6 +72,11 @@ public class GetProgramScheduleItemsHandler :
|
||||
item = item with { PlayoutMode = PlayoutMode.One };
|
||||
}
|
||||
}
|
||||
|
||||
if (item.PlaybackOrder is PlaybackOrder.ShuffleInOrder)
|
||||
{
|
||||
item = item with { FillWithGroupMode = FillWithGroupMode.None };
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
|
||||
@@ -30,5 +30,5 @@ public class QuerySearchIndexAllItemsHandler : IRequestHandler<QuerySearchIndexA
|
||||
await GetIds(LuceneSearchIndex.ImageType, request.Query));
|
||||
|
||||
private async Task<List<int>> GetIds(string type, string query) =>
|
||||
(await _searchIndex.Search(_client, $"type:{type} AND ({query})", 0, 0)).Items.Map(i => i.Id).ToList();
|
||||
(await _searchIndex.Search(_client, $"type:{type} AND ({query})", string.Empty, 0, 0)).Items.Map(i => i.Id).ToList();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ public class QuerySearchIndexArtistsHandler : IRequestHandler<QuerySearchIndexAr
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ public class
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public class QuerySearchIndexImagesHandler(IClient client, ISearchIndex searchIn
|
||||
SearchResult searchResult = await searchIndex.Search(
|
||||
client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ public class QuerySearchIndexMoviesHandler : IRequestHandler<QuerySearchIndexMov
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ public class
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ public class
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public class
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ public class
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class QuerySearchIndexSongsHandler : IRequestHandler<QuerySearchIndexSong
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
_client,
|
||||
request.Query,
|
||||
string.Empty,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
|
||||
+7
-7
@@ -410,16 +410,16 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
List<Subtitle> allSubtitles = playoutItemWithPath.PlayoutItem.MediaItem switch
|
||||
{
|
||||
Episode episode => await Optional(episode.EpisodeMetadata).Flatten().HeadOrNone()
|
||||
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
|
||||
.IfNoneAsync(new List<Subtitle>()),
|
||||
.Map(mm => mm.Subtitles ?? [])
|
||||
.IfNoneAsync([]),
|
||||
Movie movie => await Optional(movie.MovieMetadata).Flatten().HeadOrNone()
|
||||
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
|
||||
.IfNoneAsync(new List<Subtitle>()),
|
||||
.Map(mm => mm.Subtitles ?? [])
|
||||
.IfNoneAsync([]),
|
||||
MusicVideo musicVideo => await GetMusicVideoSubtitles(musicVideo, channel, settings),
|
||||
OtherVideo otherVideo => await Optional(otherVideo.OtherVideoMetadata).Flatten().HeadOrNone()
|
||||
.Map(mm => mm.Subtitles ?? new List<Subtitle>())
|
||||
.IfNoneAsync(new List<Subtitle>()),
|
||||
_ => new List<Subtitle>()
|
||||
.Map(mm => mm.Subtitles ?? [])
|
||||
.IfNoneAsync([]),
|
||||
_ => []
|
||||
};
|
||||
|
||||
bool isMediaServer = playoutItemWithPath.PlayoutItem.MediaItem is PlexMovie or PlexEpisode or
|
||||
|
||||
@@ -532,7 +532,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
|
||||
|
||||
byte[] textData = Encoding.UTF8.GetBytes(text);
|
||||
byte[] hash = MD5.HashData(textData);
|
||||
return BitConverter.ToString(hash).Replace("-", string.Empty);
|
||||
return Convert.ToHexString(hash);
|
||||
}
|
||||
|
||||
private sealed record SubtitleToExtract(Subtitle Subtitle, string OutputPath);
|
||||
|
||||
@@ -22,21 +22,21 @@ internal static class Mapper
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Plot ?? string.Empty).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetPoster(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => GetFanArt(m, maybeJellyfin, maybeEmby)).IfNone(string.Empty),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList())
|
||||
.IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone([]),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Where(t => string.IsNullOrWhiteSpace(t.ExternalTypeId)).Map(g => g.Name).ToList()).IfNone([]),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList()).IfNone([]),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Where(t => t.ExternalTypeId == Tag.PlexNetworkTypeId).Map(g => g.Name).ToList()).IfNone([]),
|
||||
show.ShowMetadata.HeadOrNone()
|
||||
.Map(
|
||||
m => (m.ContentRating ?? string.Empty).Split("/").Map(s => s.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone(new List<string>()),
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()).IfNone([]),
|
||||
LanguagesForShow(languages),
|
||||
show.ShowMetadata.HeadOrNone()
|
||||
.Map(
|
||||
m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id)
|
||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||
.ToList())
|
||||
.IfNone(new List<ActorCardViewModel>()));
|
||||
.IfNone([]));
|
||||
|
||||
internal static TelevisionSeasonViewModel ProjectToViewModel(
|
||||
Season season,
|
||||
|
||||
@@ -13,6 +13,7 @@ public record TelevisionShowViewModel(
|
||||
List<string> Genres,
|
||||
List<string> Tags,
|
||||
List<string> Studios,
|
||||
List<string> Networks,
|
||||
List<string> ContentRatings,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -133,8 +134,23 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
}
|
||||
}
|
||||
|
||||
var environment = new Dictionary<string, string>();
|
||||
foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
|
||||
{
|
||||
if (de is { Key: string key, Value: string value })
|
||||
{
|
||||
if (key.StartsWith("ETV_", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith("ASPNETCORE_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
environment[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TroubleshootingInfo(
|
||||
version,
|
||||
environment,
|
||||
healthCheckSummaries,
|
||||
ffmpegSettings,
|
||||
activeFFmpegProfiles,
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ErsatzTV.Application.Troubleshooting;
|
||||
|
||||
public record TroubleshootingInfo(
|
||||
string Version,
|
||||
Dictionary<string, string> Environment,
|
||||
IEnumerable<HealthCheckResultSummary> Health,
|
||||
FFmpegSettingsViewModel FFmpegSettings,
|
||||
IEnumerable<FFmpegProfile> FFmpegProfiles,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
|
||||
@@ -6,7 +7,7 @@ namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
public record CreateWatermark(
|
||||
string Name,
|
||||
string Image,
|
||||
ArtworkContentTypeModel Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
WatermarkLocation Location,
|
||||
|
||||
@@ -39,21 +39,33 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
|
||||
private static Validation<BaseError, ChannelWatermark> Validate(CreateWatermark request) =>
|
||||
ValidateName(request)
|
||||
.Map(
|
||||
_ => new ChannelWatermark
|
||||
_ =>
|
||||
{
|
||||
Name = request.Name,
|
||||
Image = request.ImageSource == ChannelWatermarkImageSource.Custom ? request.Image : null,
|
||||
Mode = request.Mode,
|
||||
ImageSource = request.ImageSource,
|
||||
Location = request.Location,
|
||||
Size = request.Size,
|
||||
WidthPercent = request.Width,
|
||||
HorizontalMarginPercent = request.HorizontalMargin,
|
||||
VerticalMarginPercent = request.VerticalMargin,
|
||||
FrequencyMinutes = request.FrequencyMinutes,
|
||||
DurationSeconds = request.DurationSeconds,
|
||||
Opacity = request.Opacity,
|
||||
PlaceWithinSourceContent = request.PlaceWithinSourceContent
|
||||
var watermark = new ChannelWatermark
|
||||
{
|
||||
Name = request.Name,
|
||||
Image = null,
|
||||
OriginalContentType = null,
|
||||
Mode = request.Mode,
|
||||
ImageSource = request.ImageSource,
|
||||
Location = request.Location,
|
||||
Size = request.Size,
|
||||
WidthPercent = request.Width,
|
||||
HorizontalMarginPercent = request.HorizontalMargin,
|
||||
VerticalMarginPercent = request.VerticalMargin,
|
||||
FrequencyMinutes = request.FrequencyMinutes,
|
||||
DurationSeconds = request.DurationSeconds,
|
||||
Opacity = request.Opacity,
|
||||
PlaceWithinSourceContent = request.PlaceWithinSourceContent
|
||||
};
|
||||
|
||||
if (request.ImageSource == ChannelWatermarkImageSource.Custom)
|
||||
{
|
||||
watermark.Image = request.Image?.Path;
|
||||
watermark.OriginalContentType = request.Image?.ContentType;
|
||||
}
|
||||
|
||||
return watermark;
|
||||
});
|
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateWatermark request) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace ErsatzTV.Application.Watermarks;
|
||||
public record UpdateWatermark(
|
||||
int Id,
|
||||
string Name,
|
||||
string Image,
|
||||
ArtworkContentTypeModel Image,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
WatermarkLocation Location,
|
||||
|
||||
@@ -33,7 +33,15 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
|
||||
UpdateWatermark update)
|
||||
{
|
||||
p.Name = update.Name;
|
||||
p.Image = update.ImageSource == ChannelWatermarkImageSource.Custom ? update.Image : null;
|
||||
|
||||
p.Image = null;
|
||||
p.OriginalContentType = null;
|
||||
if (update.ImageSource == ChannelWatermarkImageSource.Custom)
|
||||
{
|
||||
p.Image = update.Image?.Path;
|
||||
p.OriginalContentType = update.Image?.ContentType;
|
||||
}
|
||||
|
||||
p.Mode = update.Mode;
|
||||
p.ImageSource = update.ImageSource;
|
||||
p.Location = update.Location;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
@@ -7,7 +8,7 @@ internal static class Mapper
|
||||
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) =>
|
||||
new(
|
||||
watermark.Id,
|
||||
watermark.Image,
|
||||
new ArtworkContentTypeModel(watermark.Image, watermark.OriginalContentType),
|
||||
watermark.Name,
|
||||
watermark.Mode,
|
||||
watermark.ImageSource,
|
||||
|
||||
@@ -16,7 +16,7 @@ public class GetWatermarkByIdHandler : IRequestHandler<GetWatermarkById, Option<
|
||||
GetWatermarkById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.ChannelWatermarks
|
||||
.SelectOneAsync(w => w.Id, w => w.Id == request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Application.Artworks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
|
||||
namespace ErsatzTV.Application.Watermarks;
|
||||
|
||||
public record WatermarkViewModel(
|
||||
int Id,
|
||||
string Image,
|
||||
ArtworkContentTypeModel Image,
|
||||
string Name,
|
||||
ChannelWatermarkMode Mode,
|
||||
ChannelWatermarkImageSource ImageSource,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Domain;
|
||||
@@ -18,7 +18,7 @@ public class PlayoutItemTests
|
||||
|
||||
string actual = item.GetDisplayDuration();
|
||||
|
||||
actual.Should().Be("3:05:04");
|
||||
actual.ShouldBe("3:05:04");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -32,6 +32,6 @@ public class PlayoutItemTests
|
||||
|
||||
string actual = item.GetDisplayDuration();
|
||||
|
||||
actual.Should().Be("27:05:04");
|
||||
actual.ShouldBe("27:05:04");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
@@ -42,7 +42,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,7 +74,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -98,7 +98,7 @@ public class EmbyPathReplacementServiceTests
|
||||
@"\\192.168.1.100\Something\Some Shared Folder",
|
||||
@"C:\mnt\something else\Some Shared Folder");
|
||||
|
||||
result.Should().Be(@"C:\mnt\something else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\mnt\something else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -130,7 +130,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -162,7 +162,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -194,7 +194,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"\\SERVERNAME\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -226,7 +226,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -258,7 +258,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -290,7 +290,7 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -322,6 +322,6 @@ public class EmbyPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<NoWarn>VSTHRD200</NoWarn>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.8.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.2.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.13.61">
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="NUnit" Version="4.3.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,732 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Tests.Fakes;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using NUnit.Framework;
|
||||
using Shouldly;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg;
|
||||
|
||||
[TestFixture]
|
||||
public class CustomStreamSelectorTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SelectStreams
|
||||
{
|
||||
private static readonly string TestFileName = Path.Combine(FileSystemLayout.ChannelStreamSelectorsFolder, "test.yml");
|
||||
|
||||
private Channel _channel;
|
||||
private MediaItemAudioVersion _audioVersion;
|
||||
private List<Subtitle> _subtitles;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_channel = new Channel(Guid.Empty)
|
||||
{
|
||||
StreamSelectorMode = ChannelStreamSelectorMode.Custom,
|
||||
StreamSelector = TestFileName
|
||||
};
|
||||
|
||||
_audioVersion = GetTestAudioVersion("eng");
|
||||
|
||||
_subtitles =
|
||||
[
|
||||
new Subtitle { Id = 1, Language = "eng", Title = "Words", SubtitleKind = SubtitleKind.Embedded },
|
||||
new Subtitle { Id = 2, Language = "en", Title = "Signs" },
|
||||
new Subtitle { Id = 3, Language = "en", Title = "Songs" },
|
||||
new Subtitle { Id = 4, Language = "en", Forced = true, SubtitleKind = SubtitleKind.Sidecar },
|
||||
new Subtitle { Id = 5, Language = "jp" }
|
||||
];
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Audio_Exact_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "eng"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_und_Audio_Missing_Language()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language: ["und"]
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(3);
|
||||
audioStream.Language.ShouldBeNullOrWhiteSpace();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Audio_Languages()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language: ["en", "eng"]
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Audio_Exact_Match_Multiple_Items()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "de"
|
||||
subtitle_language:
|
||||
- "eng"
|
||||
- audio_language:
|
||||
- "eng"
|
||||
disable_subtitles: true
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Audio_Pattern_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_en_Audio_Pattern_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
""";
|
||||
_audioVersion = GetTestAudioVersion("en");
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task disable_subtitles_Should_Select_No_Subtitles()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "eng"
|
||||
disable_subtitles: true
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Subtitle_Exact_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "ja"
|
||||
subtitle_language:
|
||||
- "eng"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(1);
|
||||
subtitle.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_eng_Subtitle_Pattern_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "ja"
|
||||
subtitle_language:
|
||||
- "en*"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(1);
|
||||
subtitle.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_en_Subtitle_Pattern_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "ja"
|
||||
subtitle_language:
|
||||
- "en*"
|
||||
""";
|
||||
_audioVersion = GetTestAudioVersion("en");
|
||||
|
||||
_subtitles =
|
||||
[
|
||||
new Subtitle { Id = 1, Language = "en", Title = "Words" }
|
||||
];
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(1);
|
||||
subtitle.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_No_Subtitle_Exact_Match_Multiple_Items()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "de"
|
||||
subtitle_language:
|
||||
- "eng"
|
||||
- audio_language:
|
||||
- "eng"
|
||||
disable_subtitles: true
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(1);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Foreign_Audio_And_English_Subtitle_Multiple_Items()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "ja"
|
||||
subtitle_language:
|
||||
- "eng"
|
||||
- audio_language:
|
||||
- "eng"
|
||||
disable_subtitles: true
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(0);
|
||||
audioStream.Language.ShouldBe("ja");
|
||||
}
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(1);
|
||||
subtitle.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Ignore_Blocked_Audio_Title()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
audio_title_blocklist:
|
||||
- "riff"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(2);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Allowed_Audio_Title()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
audio_title_allowlist:
|
||||
- "movie"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(2);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Ignore_Blocked_Subtitle_Title()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "*"
|
||||
subtitle_language:
|
||||
- "en"
|
||||
subtitle_title_blocklist:
|
||||
- "signs"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(3);
|
||||
subtitle.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Allowed_Subtitle_Title()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "*"
|
||||
subtitle_language:
|
||||
- "en"
|
||||
subtitle_title_allowlist:
|
||||
- "songs"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(3);
|
||||
subtitle.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Condition_Forced_Subtitle()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "*"
|
||||
subtitle_condition: "forced"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(4);
|
||||
subtitle.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Condition_External_Subtitle()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "*"
|
||||
subtitle_condition: "lang like 'en%' and external"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(4);
|
||||
subtitle.Language.ShouldBe("en");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Condition_Audio_Title()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
audio_condition: "title like '%movie%'"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(2);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Condition_Audio_Channels()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "en*"
|
||||
audio_condition: "channels > 2"
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(2);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Prioritized_Audio_Language()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language: ["en*","ja"]
|
||||
audio_title_blocklist: ["riff"]
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (MediaStream audioStream in result.AudioStream)
|
||||
{
|
||||
audioStream.Index.ShouldBe(2);
|
||||
audioStream.Language.ShouldBe("eng");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Prioritized_Subtitle_Language()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language:
|
||||
- "*"
|
||||
subtitle_language: ["jp","en*"]
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.Subtitle.IsSome.ShouldBeTrue();
|
||||
|
||||
foreach (Subtitle subtitle in result.Subtitle)
|
||||
{
|
||||
subtitle.Id.ShouldBe(5);
|
||||
subtitle.Language.ShouldBe("jp");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_No_Streams_When_Languages_Do_Not_Match()
|
||||
{
|
||||
const string YAML =
|
||||
"""
|
||||
---
|
||||
items:
|
||||
- audio_language: ["en"]
|
||||
subtitle_language: ["es*","de*"]
|
||||
- audio_language: ["ja"]
|
||||
subtitle_language: ["es*","de*"]
|
||||
""";
|
||||
|
||||
var streamSelector = new CustomStreamSelector(
|
||||
new FakeLocalFileSystem([new FakeFileEntry(TestFileName) { Contents = YAML }]),
|
||||
new NullLogger<CustomStreamSelector>());
|
||||
|
||||
StreamSelectorResult result = await streamSelector.SelectStreams(_channel, _audioVersion, _subtitles);
|
||||
|
||||
result.AudioStream.IsSome.ShouldBeFalse();
|
||||
result.Subtitle.IsSome.ShouldBeFalse();
|
||||
}
|
||||
|
||||
private static MediaItemAudioVersion GetTestAudioVersion(string englishLanguage)
|
||||
{
|
||||
var mediaItem = new OtherVideo();
|
||||
var mediaVersion = new MediaVersion
|
||||
{
|
||||
Streams =
|
||||
[
|
||||
new MediaStream
|
||||
{
|
||||
Index = 0,
|
||||
MediaStreamKind = MediaStreamKind.Audio,
|
||||
Channels = 2,
|
||||
Language = "ja",
|
||||
Title = "Some Title",
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Index = 1,
|
||||
MediaStreamKind = MediaStreamKind.Audio,
|
||||
Channels = 2,
|
||||
Language = englishLanguage,
|
||||
Title = "Riff Title",
|
||||
Default = true
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Index = 2,
|
||||
MediaStreamKind = MediaStreamKind.Audio,
|
||||
Channels = 6,
|
||||
Language = englishLanguage,
|
||||
Title = "Movie Title",
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Index = 3,
|
||||
MediaStreamKind = MediaStreamKind.Audio,
|
||||
Channels = 2,
|
||||
Title = "Who Knows",
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return new MediaItemAudioVersion(mediaItem, mediaVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg;
|
||||
@@ -24,7 +24,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -32,7 +31,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.FormatFlags.Should().NotContain("+genpts");
|
||||
actual.FormatFlags.ShouldNotContain("+genpts");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -49,7 +48,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -57,7 +55,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ThreadCount.Should().Be(7);
|
||||
actual.ThreadCount.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -70,7 +68,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -78,7 +75,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ThreadCount.Should().Be(7);
|
||||
actual.ThreadCount.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -91,7 +88,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -100,8 +96,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
None);
|
||||
|
||||
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
|
||||
actual.FormatFlags.Count.Should().Be(expected.Length);
|
||||
actual.FormatFlags.Should().Contain(expected);
|
||||
actual.FormatFlags.Count.ShouldBe(expected.Length);
|
||||
expected.ShouldBeSubsetOf(actual.FormatFlags);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -114,7 +110,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -123,8 +118,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
None);
|
||||
|
||||
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
|
||||
actual.FormatFlags.Count.Should().Be(expected.Length);
|
||||
actual.FormatFlags.Should().Contain(expected);
|
||||
actual.FormatFlags.Count.ShouldBe(expected.Length);
|
||||
expected.ShouldBeSubsetOf(actual.FormatFlags);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -137,7 +132,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -145,7 +139,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.RealtimeOutput.Should().BeTrue();
|
||||
actual.RealtimeOutput.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -158,7 +152,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -166,7 +159,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.RealtimeOutput.Should().BeTrue();
|
||||
actual.RealtimeOutput.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -181,7 +174,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
now,
|
||||
now.AddMinutes(5),
|
||||
TimeSpan.Zero,
|
||||
@@ -189,8 +181,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.StreamSeek.IsSome.Should().BeTrue();
|
||||
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
|
||||
actual.StreamSeek.IsSome.ShouldBeTrue();
|
||||
actual.StreamSeek.IfNone(TimeSpan.Zero).ShouldBe(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -205,7 +197,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
now,
|
||||
now.AddMinutes(5),
|
||||
TimeSpan.Zero,
|
||||
@@ -213,8 +204,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.StreamSeek.IsSome.Should().BeTrue();
|
||||
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
|
||||
actual.StreamSeek.IsSome.ShouldBeTrue();
|
||||
actual.StreamSeek.IfNone(TimeSpan.Zero).ShouldBe(TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -233,7 +224,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -241,7 +231,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -260,7 +250,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -268,7 +257,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -287,7 +276,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -295,8 +283,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -315,7 +303,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -323,8 +310,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -342,7 +329,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -351,9 +337,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
None);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.Should().Be(1280);
|
||||
scaledSize.Height.Should().Be(554);
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
scaledSize.Width.ShouldBe(1280);
|
||||
scaledSize.Height.ShouldBe(554);
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -372,7 +358,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -381,9 +366,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
None);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.Should().Be(1280);
|
||||
scaledSize.Height.Should().Be(976);
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
scaledSize.Width.ShouldBe(1280);
|
||||
scaledSize.Height.ShouldBe(976);
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -402,7 +387,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -411,9 +395,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
None);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.Should().Be(1694);
|
||||
scaledSize.Height.Should().Be(720);
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
scaledSize.Width.ShouldBe(1694);
|
||||
scaledSize.Height.ShouldBe(720);
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -432,7 +416,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -440,8 +423,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -462,7 +445,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -470,9 +452,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
actual.VideoFormat.ShouldBe(FFmpegProfileVideoFormat.H264);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -494,7 +476,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -502,9 +483,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
actual.VideoFormat.ShouldBe(FFmpegProfileVideoFormat.H264);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -525,7 +506,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -533,9 +513,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.Copy);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
actual.VideoFormat.ShouldBe(FFmpegProfileVideoFormat.Copy);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -556,7 +536,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "h264" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -564,9 +543,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoFormat.Should().Be(FFmpegProfileVideoFormat.H264);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
actual.VideoFormat.ShouldBe(FFmpegProfileVideoFormat.H264);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -586,7 +565,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -594,9 +572,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
actual.VideoBitrate.IfNone(0).Should().Be(2525);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
actual.VideoBitrate.IfNone(0).ShouldBe(2525);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -617,7 +595,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -625,9 +602,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoBitrate.IfNone(0).Should().Be(2525);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
actual.VideoBitrate.IfNone(0).ShouldBe(2525);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -647,7 +624,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -655,9 +631,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
actual.VideoBufferSize.IfNone(0).Should().Be(2525);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
actual.VideoBufferSize.IfNone(0).ShouldBe(2525);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -678,7 +654,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream { Codec = "mpeg2video" },
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -686,9 +661,9 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.VideoBufferSize.IfNone(0).Should().Be(2525);
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
actual.VideoBufferSize.IfNone(0).ShouldBe(2525);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -704,7 +679,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "aac" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -712,7 +686,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Aac);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -728,7 +702,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -736,7 +709,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Aac);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -752,7 +725,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -760,7 +732,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Copy);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -777,7 +749,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -785,7 +756,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioBitrate.IfNone(0).Should().Be(2424);
|
||||
actual.AudioBitrate.IfNone(0).ShouldBe(2424);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -802,7 +773,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -810,7 +780,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioBufferSize.IfNone(0).Should().Be(2424);
|
||||
actual.AudioBufferSize.IfNone(0).ShouldBe(2424);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -827,7 +797,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -835,7 +804,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioChannels.IfNone(0).Should().Be(6);
|
||||
actual.AudioChannels.IfNone(0).ShouldBe(6);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -852,7 +821,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -860,7 +828,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||
actual.AudioSampleRate.IfNone(0).ShouldBe(48);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -876,7 +844,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -884,7 +851,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioChannels.IfNone(0).Should().Be(6);
|
||||
actual.AudioChannels.IfNone(0).ShouldBe(6);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -900,7 +867,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -908,7 +874,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||
actual.AudioSampleRate.IfNone(0).ShouldBe(48);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -930,7 +896,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -938,7 +903,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
|
||||
actual.AudioDuration.IfNone(TimeSpan.MinValue).ShouldBe(TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -954,7 +919,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream { Codec = "ac3" },
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -962,7 +926,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.NormalizeLoudnessMode.Should().Be(NormalizeLoudnessMode.LoudNorm);
|
||||
actual.NormalizeLoudnessMode.ShouldBe(NormalizeLoudnessMode.LoudNorm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -980,7 +944,6 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
ffmpegProfile,
|
||||
TestVersion,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
@@ -988,7 +951,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
|
||||
actual.HardwareAcceleration.ShouldBe(HardwareAccelerationKind.Qsv);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Infrastructure.Scripting;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
@@ -64,10 +64,10 @@ public class FFmpegStreamSelectorTests
|
||||
Substitute.For<ILogger<FFmpegStreamSelector>>());
|
||||
|
||||
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, "jpn", "Whatever");
|
||||
selectedStream.IsSome.Should().BeTrue();
|
||||
selectedStream.IsSome.ShouldBeTrue();
|
||||
foreach (MediaStream stream in selectedStream)
|
||||
{
|
||||
stream.Language.Should().Be("ja");
|
||||
stream.Language.ShouldBe("ja");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +119,10 @@ public class FFmpegStreamSelectorTests
|
||||
Substitute.For<ILogger<FFmpegStreamSelector>>());
|
||||
|
||||
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, null, channel.PreferredAudioTitle);
|
||||
selectedStream.IsSome.Should().BeTrue();
|
||||
selectedStream.IsSome.ShouldBeTrue();
|
||||
foreach (MediaStream stream in selectedStream)
|
||||
{
|
||||
stream.Language.Should().Be("ja");
|
||||
stream.Language.ShouldBe("ja");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +166,10 @@ public class FFmpegStreamSelectorTests
|
||||
channel,
|
||||
"heb",
|
||||
ChannelSubtitleMode.Any);
|
||||
selectedStream.IsSome.Should().BeTrue();
|
||||
selectedStream.IsSome.ShouldBeTrue();
|
||||
foreach (Subtitle stream in selectedStream)
|
||||
{
|
||||
stream.Language.Should().Be("he");
|
||||
stream.Language.ShouldBe("he");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
@@ -42,9 +42,9 @@ live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
result.Playlist.Should().Be(
|
||||
result.PlaylistStart.ShouldBe(start);
|
||||
result.Sequence.ShouldBe(1137);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
@@ -87,9 +87,9 @@ live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(-30), input, 2);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
result.Playlist.Should().Be(
|
||||
result.PlaylistStart.ShouldBe(start);
|
||||
result.Sequence.ShouldBe(1137);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
@@ -134,9 +134,9 @@ live001139.ts").Split(Environment.NewLine);
|
||||
int.MaxValue,
|
||||
true);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
result.Playlist.Should().Be(
|
||||
result.PlaylistStart.ShouldBe(start);
|
||||
result.Sequence.ShouldBe(1137);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
@@ -180,9 +180,9 @@ live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input, 1);
|
||||
|
||||
result.PlaylistStart.Should().Be(start.AddSeconds(8));
|
||||
result.Sequence.Should().Be(1139);
|
||||
result.Playlist.Should().Be(
|
||||
result.PlaylistStart.ShouldBe(start.AddSeconds(8));
|
||||
result.Sequence.ShouldBe(1139);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
@@ -220,9 +220,9 @@ live001139.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(6), input);
|
||||
|
||||
result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(1137);
|
||||
result.Playlist.Should().Be(
|
||||
result.PlaylistStart.ShouldBe(start);
|
||||
result.Sequence.ShouldBe(1137);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
@@ -508,9 +508,9 @@ live000082.ts").Split(Environment.NewLine);
|
||||
|
||||
TrimPlaylistResult result = _hlsPlaylistFilter.TrimPlaylist(start, start.AddSeconds(220), input);
|
||||
|
||||
// result.PlaylistStart.Should().Be(start);
|
||||
result.Sequence.Should().Be(56);
|
||||
result.Playlist.Should().Be(
|
||||
// result.PlaylistStart.ShouldBe(start);
|
||||
result.Sequence.ShouldBe(56);
|
||||
result.Playlist.ShouldBe(
|
||||
NormalizeLineEndings(
|
||||
@"#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ErsatzTV.Core.FFmpeg;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.FFmpeg;
|
||||
@@ -18,6 +18,6 @@ public class WatermarkCalculatorTests
|
||||
15,
|
||||
10);
|
||||
|
||||
actual.Should().HaveCount(0);
|
||||
actual.Count.ShouldBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,6 @@
|
||||
public record FakeFileEntry(string Path)
|
||||
{
|
||||
public DateTime LastWriteTime { get; set; } = SystemTime.MinValueUtc;
|
||||
|
||||
public string Contents { get; set; }
|
||||
}
|
||||
|
||||
@@ -57,6 +57,12 @@ public class FakeLocalFileSystem : ILocalFileSystem
|
||||
|
||||
public Unit EmptyFolder(string folder) => Unit.Default;
|
||||
|
||||
public async Task<string> ReadAllText(string path) => await _files
|
||||
.Filter(f => f.Path == path)
|
||||
.HeadOrNone()
|
||||
.Select(f => f.Contents)
|
||||
.IfNoneAsync(string.Empty);
|
||||
|
||||
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
|
||||
|
||||
private static List<DirectoryInfo> Split(DirectoryInfo path)
|
||||
|
||||
@@ -28,7 +28,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
|
||||
public Task<List<MediaItem>> GetMultiCollectionItemsByName(string name) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItemsByName(string name) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(string query) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(string query, string smartCollectionName) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetShowItemsByShowGuids(List<string> guids) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetPlaylistItems(int id) => throw new NotSupportedException();
|
||||
public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException();
|
||||
|
||||
@@ -21,6 +21,7 @@ public class FakeTelevisionRepository : ITelevisionRepository
|
||||
public Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Episode>> GetShowItems(int showId) => throw new NotSupportedException();
|
||||
public Task<List<int>> GetEpisodeIdsForShow(int showId) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Season>> GetAllSeasons() => throw new NotSupportedException();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Iptv;
|
||||
@@ -16,7 +16,7 @@ public class ChannelIdentifierTests
|
||||
public void TestLegacy(string channelNumber, string expected)
|
||||
{
|
||||
string actual = ChannelIdentifier.LegacyFromNumber(channelNumber);
|
||||
actual.Should().Be(expected);
|
||||
actual.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[TestCase("1.23", "C1.23.150.ersatztv.org")]
|
||||
@@ -28,6 +28,6 @@ public class ChannelIdentifierTests
|
||||
public void TestNew(string channelNumber, string expected)
|
||||
{
|
||||
string actual = ChannelIdentifier.FromNumber(channelNumber);
|
||||
actual.Should().Be(expected);
|
||||
actual.ShouldBe(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
@@ -42,7 +42,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,7 +74,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -98,7 +98,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
@"\\192.168.1.100\Something\Some Shared Folder",
|
||||
@"C:\mnt\something else\Some Shared Folder");
|
||||
|
||||
result.Should().Be(@"C:\mnt\something else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\mnt\something else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -130,7 +130,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -162,7 +162,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -194,7 +194,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"\\SERVERNAME\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -226,7 +226,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -258,7 +258,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -290,7 +290,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -322,7 +322,7 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -354,6 +354,6 @@ public class JellyfinPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Flurl;
|
||||
using NUnit.Framework;
|
||||
|
||||
@@ -23,7 +23,7 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -41,7 +41,7 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -59,7 +59,7 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -77,7 +77,7 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server:1000/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -95,7 +95,7 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -113,6 +113,6 @@ public class JellyfinUrlTests
|
||||
|
||||
Url url = JellyfinUrl.ForArtwork(Some(mediaSource), artwork);
|
||||
|
||||
url.ToString().Should().Be("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
url.ToString().ShouldBe("https://some.jellyfin.server/jellyfin/Items/2/Images/3?tag=4");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bugsnag;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
|
||||
@@ -65,10 +65,10 @@ public class FallbackMetadataProviderTests
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Count.Should().Be(1);
|
||||
metadata.Count.ShouldBe(1);
|
||||
// TODO: how can we test season number? do we need to?
|
||||
// metadata.Season.Should().Be(season);
|
||||
metadata.Head().EpisodeNumber.Should().Be(episode);
|
||||
// metadata.Season.ShouldBe(season);
|
||||
metadata.Head().EpisodeNumber.ShouldBe(episode);
|
||||
}
|
||||
|
||||
[TestCase("Awesome Show - S01_BLAH.mkv", 0)]
|
||||
@@ -91,8 +91,8 @@ public class FallbackMetadataProviderTests
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Count.Should().Be(1);
|
||||
metadata.Head().EpisodeNumber.Should().Be(episode);
|
||||
metadata.Count.ShouldBe(1);
|
||||
metadata.Head().EpisodeNumber.ShouldBe(episode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -122,8 +122,8 @@ public class FallbackMetadataProviderTests
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Count.Should().Be(2);
|
||||
metadata.Map(m => m.EpisodeNumber).Should().BeEquivalentTo(new[] { episode1, episode2 });
|
||||
metadata.Count.ShouldBe(2);
|
||||
metadata.Map(m => m.EpisodeNumber).ShouldBe([episode1, episode2]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -147,8 +147,8 @@ public class FallbackMetadataProviderTests
|
||||
}
|
||||
});
|
||||
|
||||
metadata.Should().NotBeNull();
|
||||
metadata.Title.Should().Be(expectedTitle);
|
||||
metadata.ShouldNotBeNull();
|
||||
metadata.Title.ShouldBe(expectedTitle);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -172,6 +172,6 @@ public class FallbackMetadataProviderTests
|
||||
public void GetSeasonNumberForFolder_ShouldHandleVariousFormats(string folder, int? season)
|
||||
{
|
||||
Option<int> actual = _fallbackMetadataProvider.GetSeasonNumberForFolder(folder);
|
||||
actual.Should<Option<int>>().Be(Optional(season));
|
||||
actual.ShouldBe(Optional(season));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
@@ -42,7 +42,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -74,7 +74,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -106,7 +106,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -138,7 +138,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -170,7 +170,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"\\SERVERNAME\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -202,7 +202,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
result.ShouldBe(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -234,7 +234,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -266,7 +266,7 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -298,6 +298,6 @@ public class PlexPathReplacementServiceTests
|
||||
0,
|
||||
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||
|
||||
result.Should().Be(@"/Some Movie/Some Movie.mkv");
|
||||
result.ShouldBe(@"/Some Movie/Some Movie.mkv");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
|
||||
@@ -21,7 +21,7 @@ public static class BlockPlayoutChangeDetectionTests
|
||||
// test case: block has moved from one time to another time, nothing after
|
||||
// test case: block has moved from one time to another time, same block after
|
||||
// test case: block has moved from one time to another time, different block with same collection after
|
||||
// test case: block was moved from one time to another time, different block with different collection after
|
||||
// test case: block was moved from one time to another time, different block with different collection after
|
||||
|
||||
// test case: block was removed, nothing after
|
||||
// test case: block was removed, same block after
|
||||
@@ -79,10 +79,10 @@ public static class BlockPlayoutChangeDetectionTests
|
||||
collectionEtags);
|
||||
|
||||
// nothing to schedule
|
||||
result.Item1.Should().HaveCount(0);
|
||||
result.Item1.Count.ShouldBe(0);
|
||||
|
||||
// do not need to remove any playout items or history
|
||||
result.Item2.Should().HaveCount(0);
|
||||
result.Item2.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
private static List<Block> Blocks(DateTimeOffset dateUpdated)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.BlockScheduling;
|
||||
@@ -64,7 +64,7 @@ public static class EffectiveBlockTests
|
||||
|
||||
List<EffectiveBlock> result = EffectiveBlock.GetEffectiveBlocks(templates, start, 5);
|
||||
|
||||
result.Should().HaveCount(0);
|
||||
result.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -89,16 +89,16 @@ public static class EffectiveBlockTests
|
||||
|
||||
List<EffectiveBlock> result = EffectiveBlock.GetEffectiveBlocks(templates, start, 5);
|
||||
|
||||
result.Should().HaveCount(3);
|
||||
result.Count.ShouldBe(3);
|
||||
|
||||
result[0].Start.DayOfWeek.Should().Be(DayOfWeek.Monday);
|
||||
result[0].Start.Date.Should().Be(GetLocalDate(2024, 1, 15).Date);
|
||||
result[0].Start.DayOfWeek.ShouldBe(DayOfWeek.Monday);
|
||||
result[0].Start.Date.ShouldBe(GetLocalDate(2024, 1, 15).Date);
|
||||
|
||||
result[1].Start.DayOfWeek.Should().Be(DayOfWeek.Wednesday);
|
||||
result[1].Start.Date.Should().Be(GetLocalDate(2024, 1, 17).Date);
|
||||
result[1].Start.DayOfWeek.ShouldBe(DayOfWeek.Wednesday);
|
||||
result[1].Start.Date.ShouldBe(GetLocalDate(2024, 1, 17).Date);
|
||||
|
||||
result[2].Start.DayOfWeek.Should().Be(DayOfWeek.Friday);
|
||||
result[2].Start.Date.Should().Be(GetLocalDate(2024, 1, 19).Date);
|
||||
result[2].Start.DayOfWeek.ShouldBe(DayOfWeek.Friday);
|
||||
result[2].Start.Date.ShouldBe(GetLocalDate(2024, 1, 19).Date);
|
||||
}
|
||||
|
||||
// TODO: test when clocks spring forward
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||
@@ -23,8 +23,8 @@ public class ChronologicalContentTests
|
||||
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
chronologicalContent.Current.IsSome.Should().BeTrue();
|
||||
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||
chronologicalContent.Current.IsSome.ShouldBeTrue();
|
||||
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
|
||||
chronologicalContent.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class ChronologicalContentTests
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
chronologicalContent.State.Index.Should().Be(i % 10);
|
||||
chronologicalContent.State.Index.ShouldBe(i % 10);
|
||||
chronologicalContent.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -54,9 +54,9 @@ public class ChronologicalContentTests
|
||||
|
||||
for (var i = 6; i <= 10; i++)
|
||||
{
|
||||
chronologicalContent.Current.IsSome.Should().BeTrue();
|
||||
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||
chronologicalContent.State.Index.Should().Be(i - 1);
|
||||
chronologicalContent.Current.IsSome.ShouldBeTrue();
|
||||
chronologicalContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
|
||||
chronologicalContent.State.Index.ShouldBe(i - 1);
|
||||
chronologicalContent.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -69,8 +69,8 @@ public class ChronologicalContentTests
|
||||
|
||||
var chronologicalContent = new ChronologicalMediaCollectionEnumerator(contents, state);
|
||||
|
||||
chronologicalContent.State.Index.Should().Be(0);
|
||||
chronologicalContent.State.Seed.Should().Be(0);
|
||||
chronologicalContent.State.Index.ShouldBe(0);
|
||||
chronologicalContent.State.Seed.ShouldBe(0);
|
||||
}
|
||||
|
||||
private static List<MediaItem> Episodes(int count) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||
@@ -23,8 +23,8 @@ public class CustomOrderContentTests
|
||||
|
||||
for (var i = 10; i >= 1; i--)
|
||||
{
|
||||
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||
customOrderContent.Current.IsSome.ShouldBeTrue();
|
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
|
||||
customOrderContent.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class CustomOrderContentTests
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
customOrderContent.State.Index.Should().Be(i % 10);
|
||||
customOrderContent.State.Index.ShouldBe(i % 10);
|
||||
customOrderContent.MoveNext();
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,9 @@ public class CustomOrderContentTests
|
||||
|
||||
for (var i = 5; i >= 1; i--)
|
||||
{
|
||||
customOrderContent.Current.IsSome.Should().BeTrue();
|
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).Should().Be(i);
|
||||
customOrderContent.State.Index.Should().Be(5 - i + 5); // 5 through 10
|
||||
customOrderContent.Current.IsSome.ShouldBeTrue();
|
||||
customOrderContent.Current.Map(x => x.Id).IfNone(-1).ShouldBe(i);
|
||||
customOrderContent.State.Index.ShouldBe(5 - i + 5); // 5 through 10
|
||||
customOrderContent.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using FluentAssertions;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling;
|
||||
@@ -24,7 +24,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveOneItem(result, mediaItems[0]);
|
||||
ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]);
|
||||
ShouldHaveOneItem(result, mediaItems[3]);
|
||||
@@ -47,7 +47,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result.Count.ShouldBe(2);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[2]);
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result.Count.ShouldBe(2);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[2]);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result.Count.ShouldBe(2);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[2]);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[2]);
|
||||
ShouldHaveTwoItems(result, mediaItems[3], mediaItems[4]);
|
||||
@@ -140,7 +140,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result.Count.ShouldBe(2);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[1]);
|
||||
ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]);
|
||||
}
|
||||
@@ -162,7 +162,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveOneItem(result, mediaItems[0]);
|
||||
ShouldHaveOneItem(result, mediaItems[1]);
|
||||
ShouldHaveTwoItems(result, mediaItems[2], mediaItems[3]);
|
||||
@@ -185,7 +185,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveOneItem(result, mediaItems[0]);
|
||||
ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]);
|
||||
ShouldHaveOneItem(result, mediaItems[3]);
|
||||
@@ -208,7 +208,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(4);
|
||||
result.Count.ShouldBe(4);
|
||||
ShouldHaveOneItem(result, mediaItems[0]);
|
||||
ShouldHaveOneItem(result, mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[2]);
|
||||
@@ -232,7 +232,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveOneItem(result, mediaItems[0]);
|
||||
ShouldHaveTwoItems(result, mediaItems[1], mediaItems[2]);
|
||||
ShouldHaveOneItem(result, mediaItems[3]);
|
||||
@@ -259,7 +259,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, false);
|
||||
|
||||
result.Count.Should().Be(3);
|
||||
result.Count.ShouldBe(3);
|
||||
ShouldHaveTwoItems(result, mediaItems[0], mediaItems[2]);
|
||||
ShouldHaveOneItem(result, mediaItems[1]);
|
||||
ShouldHaveOneItem(result, mediaItems[3]);
|
||||
@@ -286,7 +286,7 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
List<GroupedMediaItem> result = MultiPartEpisodeGrouper.GroupMediaItems(mediaItems, true);
|
||||
|
||||
result.Count.Should().Be(2);
|
||||
result.Count.ShouldBe(2);
|
||||
ShouldHaveMultipleItems(result, mediaItems[0], new List<MediaItem> { mediaItems[1], mediaItems[2] });
|
||||
ShouldHaveOneItem(result, mediaItems[3]);
|
||||
}
|
||||
@@ -313,14 +313,14 @@ public class MultiPartEpisodeGrouperTests
|
||||
|
||||
private static void ShouldHaveOneItem(IEnumerable<GroupedMediaItem> result, MediaItem item) =>
|
||||
result.Filter(g => g.First == item && Optional(g.Additional).Flatten().HeadOrNone() == None)
|
||||
.Should().HaveCount(1);
|
||||
.Count().ShouldBe(1);
|
||||
|
||||
private static void ShouldHaveTwoItems(
|
||||
IEnumerable<GroupedMediaItem> result,
|
||||
MediaItem first,
|
||||
MediaItem additional) =>
|
||||
result.Filter(g => g.First == first && Optional(g.Additional).Flatten().HeadOrNone() == Some(additional))
|
||||
.Should().HaveCount(1);
|
||||
.Count().ShouldBe(1);
|
||||
|
||||
private static void ShouldHaveMultipleItems(
|
||||
IEnumerable<GroupedMediaItem> result,
|
||||
@@ -329,5 +329,5 @@ public class MultiPartEpisodeGrouperTests
|
||||
result.Filter(
|
||||
g => g.First == first && g.Additional != null && g.Additional.Count == additional.Count &&
|
||||
additional.ForAll(g.Additional.Contains))
|
||||
.Should().HaveCount(1);
|
||||
.Count().ShouldBe(1);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user