Compare commits
83
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6b90da8982 | ||
|
|
1184dc565c | ||
|
|
d82ccf8fb5 | ||
|
|
4d83cc705f | ||
|
|
f80addacba | ||
|
|
18c2a816dc | ||
|
|
4f085c1950 | ||
|
|
dad0662fa6 | ||
|
|
dfdfa6f349 | ||
|
|
5fe3e97b31 | ||
|
|
da1cfab5f4 | ||
|
|
6d152e4b4a | ||
|
|
2ca722523b | ||
|
|
d9a3496bf5 | ||
|
|
c8f5b51d93 | ||
|
|
956734ce39 | ||
|
|
e44a391f00 | ||
|
|
b43b66dd35 | ||
|
|
140a663da4 | ||
|
|
876d79c11c | ||
|
|
57a4480c3f | ||
|
|
8f1b57eb88 | ||
|
|
70472ac84e | ||
|
|
6aab8f53b8 | ||
|
|
b30b458574 | ||
|
|
6fe6382485 | ||
|
|
100ae0adda | ||
|
|
f0d5200843 | ||
|
|
af36218cc2 | ||
|
|
eca62e8bec | ||
|
|
03d4ab8c72 | ||
|
|
57b590cd0f | ||
|
|
f0a5d89f73 | ||
|
|
17a77694a0 | ||
|
|
838c2a1661 | ||
|
|
375a306edc | ||
|
|
759052c725 | ||
|
|
dc112f0c7d | ||
|
|
c8ec87b01f | ||
|
|
fbb7a661fb | ||
|
|
37ceac5651 | ||
|
|
0953e258a5 | ||
|
|
fdbd8a07b6 | ||
|
|
5465c45a51 | ||
|
|
a0bef3568b | ||
|
|
f75bb25a1a | ||
|
|
e4ff825ae8 | ||
|
|
6c4f63ad91 | ||
|
|
c0a14ba81c | ||
|
|
c063720169 | ||
|
|
5d0f40978d | ||
|
|
063f463951 | ||
|
|
e7817b1460 | ||
|
|
7d6faee27b | ||
|
|
7bba422880 | ||
|
|
db6ae27384 |
@@ -33,10 +33,10 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-12
|
||||
- os: macos-13
|
||||
kind: macOS
|
||||
target: osx-x64
|
||||
- os: macos-12
|
||||
- os: macos-13
|
||||
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
|
||||
@@ -130,7 +132,7 @@ jobs:
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
files: |
|
||||
${{ env.RELEASE_NAME }}.dmg
|
||||
@@ -146,6 +148,9 @@ jobs:
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-x64
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-musl-x64
|
||||
- os: ubuntu-latest
|
||||
kind: linux
|
||||
target: linux-arm
|
||||
@@ -163,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
|
||||
@@ -175,7 +182,7 @@ jobs:
|
||||
id: downloadffmpeg
|
||||
name: Download ffmpeg
|
||||
with:
|
||||
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/6.1-working-cuvid/ffmpeg-6.1-working-cuvid.7z"
|
||||
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"
|
||||
target: ffmpeg/
|
||||
|
||||
- name: Build
|
||||
@@ -187,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/"
|
||||
@@ -230,7 +237,7 @@ jobs:
|
||||
- name: Publish
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
tag_name: ${{ inputs.release_tag }}
|
||||
files: |
|
||||
${{ env.RELEASE_NAME }}.zip
|
||||
|
||||
@@ -19,14 +19,14 @@ jobs:
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
tag2="${tag:1}"
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag2/beta/$short}"
|
||||
final="${tag2}-${short}"
|
||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag/beta/$short}"
|
||||
final="${tag}-${short}"
|
||||
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||
outputs:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Qodana
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
qodana:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
checks: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
|
||||
fetch-depth: 0 # a full history is required for pull request analysis
|
||||
- name: 'Qodana Scan'
|
||||
uses: JetBrains/qodana-action@v2024.1
|
||||
env:
|
||||
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
|
||||
@@ -67,6 +67,13 @@ jobs:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -79,6 +86,8 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name != 'arm64' && matrix.name != 'arm32v7' }}
|
||||
|
||||
- name: Build and push
|
||||
@@ -94,6 +103,8 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm64' }}
|
||||
|
||||
- name: Build and push
|
||||
@@ -109,4 +120,6 @@ jobs:
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm32v7' }}
|
||||
|
||||
@@ -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-12
|
||||
runs-on: macos-13
|
||||
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
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
+125
-1
@@ -5,6 +5,128 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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`
|
||||
- Add remote IP and user agent to HTTP request logging
|
||||
- Add environment variables to allow ETV to run UI and streaming on separate ports
|
||||
- `ETV_STREAMING_PORT`: port used for streaming requests, defaults to 8409
|
||||
- `ETV_UI_PORT`: port used for admin UI, defaults to 8409
|
||||
- Publish docker images to ghcr.io (`ghcr.io/ersatztv/ersatztv`)
|
||||
- Add new option `Fixed Start Time Behavior` to Schedules and Schedule Items
|
||||
- Schedules can set a default behavior for all items
|
||||
- Schedule items can override this default behavior
|
||||
- Possible values are:
|
||||
- `Strict`: Always wait for the exact start time, even if that means waiting (adding unscheduled time) until the next day
|
||||
- `Flexible`: Start scheduling immediately (do not wait) if waiting (adding unscheduled time) would go into the next day
|
||||
- As an example, if the current scheduling time is 6:02 AM and the next schedule item has a fixed start time of 6:00 AM
|
||||
- `Strict` will add nearly 24h (23:58) of unscheduled time so that it can start exactly at 6:00 AM the next day
|
||||
- `Flexible` will NOT add unscheduled time, and will schedule its item at 6:02 AM (which may also affect the scheduling of later items)
|
||||
- Add basic HDR transcoding support
|
||||
- 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
|
||||
- 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 when using this feature
|
||||
- e.g. `smart_collection:"one" NOT 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)
|
||||
- Change how ETV determines which address to use for Plex connections
|
||||
- The active Plex connection (address) will only be cached for 30 seconds
|
||||
- When the connection is no longer cached, a ping will be sent to the last used address for Plex (the last address that had a successful ping)
|
||||
- If the ping is successful, the address will be cached for another 30 seconds
|
||||
- If the ping is not successful, all addresses will be checked again, and the first address to return a successful ping will be cached for 30 seconds
|
||||
- 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
|
||||
- Fix navigation after form submission when using `ETV_BASE_URL` environment variable
|
||||
- Fix UI crashes when channel numbers contain a period `.` in locales that have a different decimal separator (e.g. `,`)
|
||||
- Fix playout detail table to only reload once when resetting a playout
|
||||
- Fix date formatting in playout detail table on reload (will now respect browser's `Accept-Language` header)
|
||||
- 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
|
||||
- Add `Reset All Playouts` button to top of playouts page
|
||||
- Add `rewind_on_reset` option to `wait_until` YAML playout instruction
|
||||
- This option allows YAML playouts to start in the past
|
||||
- Add `advance` option to `epg_group` YAML playout instruction
|
||||
- When set to `false`, this option will lock the guide group without starting a new guide group
|
||||
- This can be helpful for "post roll" items that should be part of the previous item's guide group
|
||||
- Add `Song Video Mode` to channel settings
|
||||
- `Default` - existing behavior
|
||||
- `With Progress` - show animated progress bar at bottom of generated video
|
||||
- Thanks to @JeckDev for the idea and the artwork
|
||||
- Add fallback album art image for songs that have no album art
|
||||
- Add `Vaapi Display` option to FFmpeg Profile
|
||||
- Possible values will be install-specific and sourced from `vainfo`
|
||||
- `drm` was the previous default value, and should be used in most cases
|
||||
- Test all `Vaapi Display` values in `Troubleshooting` > `VAAPI Capabilities`
|
||||
- Add `tag_full` field to search index
|
||||
- This field contains the same values as the existing `tag` field, but it is not analyzed or tokenized
|
||||
|
||||
### Changed
|
||||
- **BREAKING CHANGE**: Change channel identifiers used in XMLTV to work around bad behavior in Plex
|
||||
|
||||
### Fixed
|
||||
- Fix startup error with MySql backend caused by database cleaner
|
||||
- Fix emptying trash with ElasticSearch backend
|
||||
- Fix double loading of trash UI elements, and fix reloading of all UI elements after emptying trash
|
||||
- Fix destroying channel preview player when preview dialog is closed
|
||||
- This bug made it difficult to "stop" a channel after previewing it
|
||||
- Fix bug where deco default filler would never use hardware acceleration
|
||||
- Fix deleting local libraries with MySql backend
|
||||
- Fix `Scaling Behavior` `Crop` when content is smaller than FFmpeg Profile resolution
|
||||
- Now, content will properly scale beyond the desired resolution before cropping
|
||||
- Fix displaying playout item durations that are greater than 24 hours
|
||||
- Fix building playouts when playlist has been changed to have fewer items
|
||||
- Fix selecting audio stream with preferred title
|
||||
- Fix synchronizing Plex collections
|
||||
- If this breaks collection sync for you, you will need to update your Plex server
|
||||
- Fix guide group generation for `duration` YAML instructions
|
||||
- Fix default song background when targeting 4:3 resolutions
|
||||
- Previously the background was always 16:9 and was padded, now it will fill 4:3
|
||||
|
||||
## [0.8.8-beta] - 2024-09-19
|
||||
### Added
|
||||
- Add support for Plex Other Video libraries
|
||||
@@ -2131,7 +2253,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
@@ -1,6 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Artworks;
|
||||
|
||||
public record GetArtwork(int Id) : IRequest<Either<BaseError, Artwork>>;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Artworks;
|
||||
|
||||
public record GetArtwork(int Id) : IRequest<Either<BaseError, Artwork>>;
|
||||
|
||||
@@ -21,7 +21,8 @@ public record ChannelViewModel(
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate)
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode)
|
||||
{
|
||||
public string WebEncodedName => WebUtility.UrlEncode(Name);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ public record CreateChannel(
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
|
||||
@@ -51,10 +51,16 @@ public class CreateChannelHandler(
|
||||
var artwork = new List<Artwork>();
|
||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||
{
|
||||
string logo = request.Logo;
|
||||
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,
|
||||
DateAdded = DateTime.UtcNow,
|
||||
DateUpdated = DateTime.UtcNow
|
||||
@@ -76,7 +82,8 @@ public class CreateChannelHandler(
|
||||
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
|
||||
SubtitleMode = request.SubtitleMode,
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
|
||||
SongVideoMode = request.SongVideoMode
|
||||
};
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
|
||||
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Domain.Filler;
|
||||
using ErsatzTV.Core.Emby;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using ErsatzTV.Core.Streaming;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
@@ -286,24 +287,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.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;
|
||||
|
||||
@@ -514,6 +502,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
ProgrammeStart = start,
|
||||
ProgrammeStop = stop,
|
||||
ChannelId = ChannelIdentifier.FromNumber(request.ChannelNumber),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(request.ChannelNumber),
|
||||
request.ChannelNumber,
|
||||
HasCustomTitle = hasCustomTitle,
|
||||
displayItem.CustomTitle,
|
||||
@@ -569,6 +559,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
ProgrammeStart = start,
|
||||
ProgrammeStop = stop,
|
||||
ChannelId = ChannelIdentifier.FromNumber(request.ChannelNumber),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(request.ChannelNumber),
|
||||
request.ChannelNumber,
|
||||
HasCustomTitle = hasCustomTitle,
|
||||
displayItem.CustomTitle,
|
||||
@@ -629,6 +621,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
ProgrammeStart = start,
|
||||
ProgrammeStop = stop,
|
||||
ChannelId = ChannelIdentifier.FromNumber(request.ChannelNumber),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(request.ChannelNumber),
|
||||
request.ChannelNumber,
|
||||
HasCustomTitle = hasCustomTitle,
|
||||
displayItem.CustomTitle,
|
||||
@@ -686,6 +680,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
ProgrammeStart = start,
|
||||
ProgrammeStop = stop,
|
||||
ChannelId = ChannelIdentifier.FromNumber(request.ChannelNumber),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(request.ChannelNumber),
|
||||
request.ChannelNumber,
|
||||
HasCustomTitle = hasCustomTitle,
|
||||
displayItem.CustomTitle,
|
||||
@@ -738,6 +734,8 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
ProgrammeStart = start,
|
||||
ProgrammeStop = stop,
|
||||
ChannelId = ChannelIdentifier.FromNumber(request.ChannelNumber),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(request.ChannelNumber),
|
||||
request.ChannelNumber,
|
||||
HasCustomTitle = hasCustomTitle,
|
||||
displayItem.CustomTitle,
|
||||
|
||||
@@ -3,7 +3,9 @@ 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;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -76,12 +78,18 @@ 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),
|
||||
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(channel.Number),
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -20,4 +20,5 @@ public record UpdateChannel(
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
string MusicVideoCreditsTemplate,
|
||||
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
|
||||
@@ -41,30 +41,47 @@ public class UpdateChannelHandler(
|
||||
c.SubtitleMode = update.SubtitleMode;
|
||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
c.SongVideoMode = update.SongVideoMode;
|
||||
c.Artwork ??= [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
{
|
||||
Option<Artwork> maybeLogo =
|
||||
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
|
||||
string logo = update.Logo;
|
||||
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.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,
|
||||
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;
|
||||
|
||||
@@ -24,7 +24,8 @@ internal static class Mapper
|
||||
channel.PreferredSubtitleLanguageCode,
|
||||
channel.SubtitleMode,
|
||||
channel.MusicVideoCreditsMode,
|
||||
channel.MusicVideoCreditsTemplate);
|
||||
channel.MusicVideoCreditsTemplate,
|
||||
channel.SongVideoMode);
|
||||
|
||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||
new(
|
||||
@@ -38,9 +39,22 @@ internal static class Mapper
|
||||
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
|
||||
new(resolution.Height, resolution.Width);
|
||||
|
||||
private static string GetLogo(Channel channel) =>
|
||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
|
||||
new(resolution.Height, resolution.Width, bitrate);
|
||||
|
||||
private static string GetLogo(Channel channel)
|
||||
{
|
||||
Option<Artwork> maybeArtwork = channel.Artwork
|
||||
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||
.HeadOrNone();
|
||||
|
||||
foreach (Artwork artwork in maybeArtwork)
|
||||
{
|
||||
return artwork.IsExternalUrl() ? artwork.Path : $"iptv/logos/{artwork.Path}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetStreamingMode(Channel channel) =>
|
||||
channel.StreamingMode switch
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelResolution(string ChannelNumber) : IRequest<Option<ResolutionViewModel>>;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelResolutionAndBitrate(string ChannelNumber) : IRequest<Option<ResolutionAndBitrateViewModel>>;
|
||||
+7
-5
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class GetChannelResolutionHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetChannelResolution, Option<ResolutionViewModel>>
|
||||
public class GetChannelResolutionAndBitrateHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetChannelResolutionAndBitrate, Option<ResolutionAndBitrateViewModel>>
|
||||
{
|
||||
public async Task<Option<ResolutionViewModel>> Handle(
|
||||
GetChannelResolution request,
|
||||
public async Task<Option<ResolutionAndBitrateViewModel>> Handle(
|
||||
GetChannelResolutionAndBitrate request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
@@ -20,6 +20,8 @@ public class GetChannelResolutionHandler(IDbContextFactory<TvContext> dbContextF
|
||||
.ThenInclude(ff => ff.Resolution)
|
||||
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber);
|
||||
|
||||
return maybeChannel.Map(c => Mapper.ProjectToViewModel(c.FFmpegProfile.Resolution));
|
||||
return maybeChannel.Map(c => Mapper.ProjectToViewModel(
|
||||
c.FFmpegProfile.Resolution,
|
||||
(int)((c.FFmpegProfile.VideoBitrate * 1000 + c.FFmpegProfile.AudioBitrate * 1000) * 1.2)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record ResolutionAndBitrateViewModel(int Height, int Width, int Bitrate);
|
||||
@@ -26,7 +26,6 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
|
||||
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
|
||||
foreach (EmbyMediaSource mediaSource in mediaSources)
|
||||
{
|
||||
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
|
||||
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -9,19 +9,19 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bugsnag" Version="3.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<PackageReference Include="Bugsnag" Version="4.0.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.11.20">
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.17.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
|
||||
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ public record CreateFFmpegProfile(
|
||||
string Name,
|
||||
int ThreadCount,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
string VaapiDisplay,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
int? QsvExtraHardwareFrames,
|
||||
@@ -20,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,
|
||||
|
||||
@@ -9,6 +9,7 @@ public record UpdateFFmpegProfile(
|
||||
string Name,
|
||||
int ThreadCount,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
string VaapiDisplay,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
int? QsvExtraHardwareFrames,
|
||||
@@ -21,6 +22,7 @@ public record UpdateFFmpegProfile(
|
||||
FFmpegProfileBitDepth BitDepth,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||
FFmpegProfileAudioFormat AudioFormat,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
|
||||
@@ -36,6 +36,7 @@ public class
|
||||
p.Name = update.Name;
|
||||
p.ThreadCount = update.ThreadCount;
|
||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||
p.VaapiDisplay = update.VaapiDisplay;
|
||||
p.VaapiDriver = update.VaapiDriver;
|
||||
p.VaapiDevice = update.VaapiDevice;
|
||||
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
|
||||
@@ -53,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;
|
||||
|
||||
@@ -9,6 +9,7 @@ public record FFmpegProfileViewModel(
|
||||
string Name,
|
||||
int ThreadCount,
|
||||
HardwareAccelerationKind HardwareAcceleration,
|
||||
string VaapiDisplay,
|
||||
VaapiDriver VaapiDriver,
|
||||
string VaapiDevice,
|
||||
int? QsvExtraHardwareFrames,
|
||||
@@ -21,6 +22,7 @@ public record FFmpegProfileViewModel(
|
||||
FFmpegProfileBitDepth BitDepth,
|
||||
int VideoBitrate,
|
||||
int VideoBufferSize,
|
||||
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
|
||||
FFmpegProfileAudioFormat AudioFormat,
|
||||
int AudioBitrate,
|
||||
int AudioBufferSize,
|
||||
|
||||
@@ -11,6 +11,7 @@ internal static class Mapper
|
||||
profile.Name,
|
||||
profile.ThreadCount,
|
||||
profile.HardwareAcceleration,
|
||||
profile.VaapiDisplay ?? "drm",
|
||||
profile.VaapiDriver,
|
||||
profile.VaapiDevice,
|
||||
profile.QsvExtraHardwareFrames,
|
||||
@@ -23,6 +24,7 @@ internal static class Mapper
|
||||
profile.BitDepth,
|
||||
profile.VideoBitrate,
|
||||
profile.VideoBufferSize,
|
||||
profile.TonemapAlgorithm,
|
||||
profile.AudioFormat,
|
||||
profile.AudioBitrate,
|
||||
profile.AudioBufferSize,
|
||||
@@ -46,12 +48,14 @@ internal static class Mapper
|
||||
ffmpegProfile.Name,
|
||||
ffmpegProfile.ThreadCount,
|
||||
(int)ffmpegProfile.HardwareAcceleration,
|
||||
ffmpegProfile.VaapiDisplay,
|
||||
(int)ffmpegProfile.VaapiDriver,
|
||||
ffmpegProfile.VaapiDevice,
|
||||
ffmpegProfile.ResolutionId,
|
||||
(int)ffmpegProfile.VideoFormat,
|
||||
ffmpegProfile.VideoBitrate,
|
||||
ffmpegProfile.VideoBufferSize,
|
||||
(int)ffmpegProfile.TonemapAlgorithm,
|
||||
(int)ffmpegProfile.AudioFormat,
|
||||
ffmpegProfile.AudioBitrate,
|
||||
ffmpegProfile.AudioBufferSize,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin;
|
||||
|
||||
public record SynchronizeJellyfinAdminUserId(int JellyfinMediaSourceId) : IRequest<Either<BaseError, Unit>>,
|
||||
IScannerBackgroundServiceRequest;
|
||||
@@ -1,107 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Jellyfin;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Jellyfin;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Jellyfin;
|
||||
|
||||
public class
|
||||
SynchronizeJellyfinAdminUserIdHandler : IRequestHandler<SynchronizeJellyfinAdminUserId,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IJellyfinApiClient _jellyfinApiClient;
|
||||
private readonly IJellyfinSecretStore _jellyfinSecretStore;
|
||||
private readonly ILogger<SynchronizeJellyfinAdminUserIdHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
|
||||
public SynchronizeJellyfinAdminUserIdHandler(
|
||||
IMemoryCache memoryCache,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IJellyfinSecretStore jellyfinSecretStore,
|
||||
IJellyfinApiClient jellyfinApiClient,
|
||||
ILogger<SynchronizeJellyfinAdminUserIdHandler> logger)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_jellyfinSecretStore = jellyfinSecretStore;
|
||||
_jellyfinApiClient = jellyfinApiClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
SynchronizeJellyfinAdminUserId request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.Map(v => v.ToEither<ConnectionParameters>())
|
||||
.BindT(PerformSync);
|
||||
|
||||
private async Task<Either<BaseError, Unit>> PerformSync(ConnectionParameters parameters)
|
||||
{
|
||||
if (_memoryCache.TryGetValue($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", out string _))
|
||||
{
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
Either<BaseError, string> maybeUserId = await _jellyfinApiClient.GetAdminUserId(
|
||||
parameters.ActiveConnection.Address,
|
||||
parameters.ApiKey);
|
||||
|
||||
return await maybeUserId.Match(
|
||||
userId =>
|
||||
{
|
||||
// _logger.LogDebug("Jellyfin admin user id is {UserId}", userId);
|
||||
_memoryCache.Set($"jellyfin_admin_user_id.{parameters.JellyfinMediaSource.Id}", userId);
|
||||
return Task.FromResult<Either<BaseError, Unit>>(Unit.Default);
|
||||
},
|
||||
async error =>
|
||||
{
|
||||
// clear api key if unable to sync with jellyfin
|
||||
if (error.Value.Contains("Unauthorized"))
|
||||
{
|
||||
await _jellyfinSecretStore.SaveSecrets(
|
||||
new JellyfinSecrets { Address = parameters.ActiveConnection.Address, ApiKey = null });
|
||||
}
|
||||
|
||||
return Left<BaseError, Unit>(error);
|
||||
});
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeJellyfinAdminUserId request) =>
|
||||
MediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveApiKey);
|
||||
|
||||
private Task<Validation<BaseError, JellyfinMediaSource>> MediaSourceMustExist(
|
||||
SynchronizeJellyfinAdminUserId request) =>
|
||||
_mediaSourceRepository.GetJellyfin(request.JellyfinMediaSourceId)
|
||||
.Map(o => o.ToValidation<BaseError>("Jellyfin media source does not exist."));
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
JellyfinMediaSource jellyfinMediaSource)
|
||||
{
|
||||
Option<JellyfinConnection> maybeConnection = jellyfinMediaSource.Connections.HeadOrNone();
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(jellyfinMediaSource, connection))
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an active connection");
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
|
||||
ConnectionParameters connectionParameters)
|
||||
{
|
||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||
.Where(match => match)
|
||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||
}
|
||||
|
||||
private sealed record ConnectionParameters(
|
||||
JellyfinMediaSource JellyfinMediaSource,
|
||||
JellyfinConnection ActiveConnection)
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,6 @@ public class SynchronizeJellyfinMediaSourcesHandler : IRequestHandler<Synchroniz
|
||||
List<JellyfinMediaSource> mediaSources = await _mediaSourceRepository.GetAllJellyfin();
|
||||
foreach (JellyfinMediaSource mediaSource in mediaSources)
|
||||
{
|
||||
await _scannerWorkerChannel.WriteAsync(
|
||||
new SynchronizeJellyfinAdminUserId(mediaSource.Id),
|
||||
cancellationToken);
|
||||
await _scannerWorkerChannel.WriteAsync(new SynchronizeJellyfinLibraries(mediaSource.Id), cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,9 +47,12 @@ public class DeleteLocalLibraryHandler : LocalLibraryHandlerBase,
|
||||
"""
|
||||
DELETE FROM MediaItem WHERE Id IN
|
||||
(
|
||||
SELECT MI.Id FROM MediaItem MI
|
||||
INNER JOIN LibraryPath LP ON MI.LibraryPathId = LP.Id
|
||||
WHERE LP.LibraryId = @LibraryId
|
||||
SELECT Id FROM
|
||||
(
|
||||
SELECT MI.Id FROM MediaItem MI
|
||||
INNER JOIN LibraryPath LP ON MI.LibraryPathId = LP.Id
|
||||
WHERE LP.LibraryId = @LibraryId
|
||||
) AS MI
|
||||
)
|
||||
""",
|
||||
new { LibraryId = localLibrary.Id });
|
||||
|
||||
@@ -3,7 +3,6 @@ using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Infrastructure.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance;
|
||||
|
||||
@@ -27,33 +26,16 @@ public class EmptyTrashHandler : IRequestHandler<EmptyTrash, Either<BaseError, U
|
||||
EmptyTrash request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string[] types =
|
||||
{
|
||||
LuceneSearchIndex.MovieType,
|
||||
LuceneSearchIndex.ShowType,
|
||||
LuceneSearchIndex.SeasonType,
|
||||
LuceneSearchIndex.EpisodeType,
|
||||
LuceneSearchIndex.MusicVideoType,
|
||||
LuceneSearchIndex.OtherVideoType,
|
||||
LuceneSearchIndex.SongType,
|
||||
LuceneSearchIndex.ArtistType
|
||||
};
|
||||
SearchResult result = await _searchIndex.Search(_client, "state:FileNotFound", string.Empty, 0, 10_000);
|
||||
var ids = result.Items.Map(i => i.Id).ToList();
|
||||
|
||||
var ids = new List<int>();
|
||||
|
||||
foreach (string type in types)
|
||||
// ElasticSearch remove items may fail, so do that first
|
||||
if (await _searchIndex.RemoveItems(ids))
|
||||
{
|
||||
SearchResult result = await _searchIndex.Search(_client, $"type:{type} AND (state:FileNotFound)", 0, 0);
|
||||
ids.AddRange(result.Items.Map(i => i.Id));
|
||||
}
|
||||
|
||||
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(ids);
|
||||
if (deleteResult.IsRight)
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
return await _mediaItemRepository.DeleteItems(ids);
|
||||
}
|
||||
|
||||
return deleteResult;
|
||||
return BaseError.New("Failed to empty trash");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -69,7 +69,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
{
|
||||
try
|
||||
{
|
||||
_entityLocker.LockPlayout(playout.Id);
|
||||
await _entityLocker.LockPlayout(playout.Id);
|
||||
|
||||
switch (playout.ProgramSchedulePlayoutType)
|
||||
{
|
||||
@@ -135,7 +135,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
}
|
||||
finally
|
||||
{
|
||||
_entityLocker.UnlockPlayout(playout.Id);
|
||||
await _entityLocker.UnlockPlayout(playout.Id);
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record ResetAllPlayouts : IRequest;
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public class ResetAllPlayoutsHandler(
|
||||
IEntityLocker locker,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<ResetAllPlayouts>
|
||||
{
|
||||
public async Task Handle(ResetAllPlayouts request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
foreach (Playout playout in await dbContext.Playouts.ToListAsync(cancellationToken))
|
||||
{
|
||||
switch (playout.ProgramSchedulePlayoutType)
|
||||
{
|
||||
case ProgramSchedulePlayoutType.Flood:
|
||||
case ProgramSchedulePlayoutType.Block:
|
||||
case ProgramSchedulePlayoutType.Yaml:
|
||||
if (!locker.IsPlayoutLocked(playout.Id))
|
||||
{
|
||||
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset), cancellationToken);
|
||||
}
|
||||
break;
|
||||
case ProgramSchedulePlayoutType.ExternalJson:
|
||||
case ProgramSchedulePlayoutType.None:
|
||||
default:
|
||||
// external json cannot be reset
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ internal static class Mapper
|
||||
GetDisplayTitle(playoutItem),
|
||||
playoutItem.StartOffset,
|
||||
playoutItem.FinishOffset,
|
||||
GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
|
||||
playoutItem.GetDisplayDuration());
|
||||
|
||||
internal static PlayoutAlternateScheduleViewModel ProjectToViewModel(
|
||||
ProgramScheduleAlternate programScheduleAlternate) =>
|
||||
@@ -81,10 +81,4 @@ internal static class Mapper
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal static string GetDisplayDuration(TimeSpan duration) =>
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
duration.TotalHours >= 1 ? @"{0:h\:mm\:ss}" : @"{0:mm\:ss}",
|
||||
duration);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Plex;
|
||||
|
||||
public class SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePlexMediaSources,
|
||||
public class SynchronizePlexMediaSourcesHandler : PlexBaseConnectionHandler, IRequestHandler<SynchronizePlexMediaSources,
|
||||
Either<BaseError, List<PlexMediaSource>>>
|
||||
{
|
||||
private const string LocalhostUri = "http://localhost:32400";
|
||||
@@ -31,6 +31,7 @@ public class SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePle
|
||||
ChannelWriter<IScannerBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
: base(plexServerApiClient, mediaSourceRepository, logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
@@ -102,63 +103,35 @@ public class SynchronizePlexMediaSourcesHandler : IRequestHandler<SynchronizePle
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
await _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
await FindConnectionToActivate(existing);
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
await FindConnectionToActivate(existing, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeExisting.IsNone)
|
||||
{
|
||||
await _mediaSourceRepository.Add(server);
|
||||
await FindConnectionToActivate(server);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FindConnectionToActivate(PlexMediaSource server)
|
||||
{
|
||||
var prioritized = server.Connections
|
||||
.OrderByDescending(pc => pc.Uri == LocalhostUri)
|
||||
.ThenByDescending(pc => pc.IsActive)
|
||||
.ToList();
|
||||
|
||||
foreach (PlexConnection connection in server.Connections)
|
||||
{
|
||||
connection.IsActive = false;
|
||||
}
|
||||
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
foreach (PlexConnection connection in prioritized)
|
||||
Option<PlexServerAuthToken> maybeToken = await _plexSecretStore.GetServerAuthToken(server.ClientIdentifier);
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||
if (await _plexServerApiClient.Ping(connection, token))
|
||||
{
|
||||
_logger.LogInformation("Located Plex at {Uri}", connection.Uri);
|
||||
connection.IsActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
await FindConnectionToActivate(server, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeToken.IsNone)
|
||||
{
|
||||
_logger.LogError(
|
||||
"Unable to activate Plex connection for server {Server} without auth token",
|
||||
server.ServerName);
|
||||
}
|
||||
|
||||
if (server.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
_logger.LogError("Unable to locate Plex");
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await _mediaSourceRepository.Update(server, new List<PlexConnection>(), new List<PlexConnection>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,15 @@ public class TryCompletePlexPinFlowHandler : IRequestHandler<TryCompletePlexPinF
|
||||
public async Task<Either<BaseError, bool>>
|
||||
Handle(TryCompletePlexPinFlow request, CancellationToken cancellationToken)
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
||||
CancellationToken token = cts.Token;
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
||||
using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, cancellationToken);
|
||||
CancellationToken token = linkedTokenSource.Token;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
bool result = await _plexTvApiClient.TryCompletePinFlow(request.AuthPin);
|
||||
if (result)
|
||||
{
|
||||
await _channel.WriteAsync(new SynchronizePlexMediaSources(), cancellationToken);
|
||||
await _channel.WriteAsync(new SynchronizePlexMediaSources(), token);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Concurrent;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Plex;
|
||||
|
||||
public abstract class PlexBaseConnectionHandler(
|
||||
IPlexServerApiClient plexServerApiClient,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
ILogger logger)
|
||||
{
|
||||
protected async Task<Option<PlexConnection>> FindConnectionToActivate(PlexMediaSource server, PlexServerAuthToken token)
|
||||
{
|
||||
Option<PlexConnection> result = Option<PlexConnection>.None;
|
||||
|
||||
foreach (PlexConnection connection in server.Connections)
|
||||
{
|
||||
connection.IsActive = false;
|
||||
}
|
||||
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
ConcurrentDictionary<PlexConnection, TimeSpan> successfulTimes = new();
|
||||
var tasks = server.Connections
|
||||
.Map(connection => PingPlexConnection(connection, token, successfulTimes, cts.Token))
|
||||
.ToList();
|
||||
|
||||
while (tasks.Count > 0)
|
||||
{
|
||||
Task completed = await Task.WhenAny(tasks);
|
||||
if (completed.IsCompletedSuccessfully)
|
||||
{
|
||||
if (!successfulTimes.IsEmpty)
|
||||
{
|
||||
await cts.CancelAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tasks.Remove(completed);
|
||||
}
|
||||
|
||||
Option<PlexConnection> maybeBest = successfulTimes.OrderByDescending(kv => kv.Value).Select(kvp => kvp.Key).HeadOrNone();
|
||||
foreach (PlexConnection connection in maybeBest)
|
||||
{
|
||||
connection.IsActive = true;
|
||||
}
|
||||
|
||||
if (server.Connections.All(c => !c.IsActive))
|
||||
{
|
||||
logger.LogError("Unable to locate Plex");
|
||||
server.Connections.Head().IsActive = true;
|
||||
}
|
||||
|
||||
await mediaSourceRepository.Update(server, [], []);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task PingPlexConnection(PlexConnection connection, PlexServerAuthToken token, ConcurrentDictionary<PlexConnection, TimeSpan> successfulTimes, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug("Attempting to locate to Plex at {Uri}", connection.Uri);
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
bool pingResult = await plexServerApiClient.Ping(connection, token, cancellationToken);
|
||||
sw.Stop();
|
||||
if (pingResult)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Located Plex at {Uri} in {Milliseconds} ms",
|
||||
connection.Uri,
|
||||
sw.ElapsedMilliseconds);
|
||||
successfulTimes.TryAdd(connection, sw.Elapsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug(
|
||||
"Unable to locate Plex at {Uri} after {Milliseconds} ms",
|
||||
connection.Uri,
|
||||
sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,32 @@ using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Plex;
|
||||
|
||||
public class GetPlexConnectionParametersHandler : IRequestHandler<GetPlexConnectionParameters,
|
||||
public class GetPlexConnectionParametersHandler : PlexBaseConnectionHandler, IRequestHandler<GetPlexConnectionParameters,
|
||||
Either<BaseError, PlexConnectionParametersViewModel>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IPlexServerApiClient _plexServerApiClient;
|
||||
private readonly IPlexSecretStore _plexSecretStore;
|
||||
private readonly ILogger<GetPlexConnectionParametersHandler> _logger;
|
||||
|
||||
public GetPlexConnectionParametersHandler(
|
||||
IMemoryCache memoryCache,
|
||||
IPlexServerApiClient plexServerApiClient,
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexSecretStore plexSecretStore)
|
||||
IPlexSecretStore plexSecretStore,
|
||||
ILogger<GetPlexConnectionParametersHandler> logger)
|
||||
: base(plexServerApiClient, mediaSourceRepository, logger)
|
||||
{
|
||||
_memoryCache = memoryCache;
|
||||
_plexServerApiClient = plexServerApiClient;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, PlexConnectionParametersViewModel>> Handle(
|
||||
@@ -33,55 +41,43 @@ public class GetPlexConnectionParametersHandler : IRequestHandler<GetPlexConnect
|
||||
return parameters;
|
||||
}
|
||||
|
||||
Either<BaseError, PlexConnectionParametersViewModel> maybeParameters =
|
||||
await Validate(request)
|
||||
.MapT(
|
||||
cp => new PlexConnectionParametersViewModel(
|
||||
new Uri(cp.ActiveConnection.Uri),
|
||||
cp.PlexServerAuthToken.AuthToken))
|
||||
.Map(v => v.ToEither<PlexConnectionParametersViewModel>());
|
||||
|
||||
return maybeParameters.Match(
|
||||
p =>
|
||||
Option<PlexMediaSource> maybeMediaSource = await _mediaSourceRepository.GetPlex(request.PlexMediaSourceId);
|
||||
foreach (PlexMediaSource mediaSource in maybeMediaSource)
|
||||
{
|
||||
Option<PlexServerAuthToken> maybeToken =
|
||||
await _plexSecretStore.GetServerAuthToken(mediaSource.ClientIdentifier);
|
||||
foreach (PlexServerAuthToken token in maybeToken)
|
||||
{
|
||||
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
|
||||
return maybeParameters;
|
||||
},
|
||||
error => error);
|
||||
}
|
||||
// try to keep the same connection
|
||||
Option<PlexConnection> maybeActiveConnection = mediaSource.Connections.Filter(c => c.IsActive).HeadOrNone();
|
||||
foreach (PlexConnection activeConnection in maybeActiveConnection)
|
||||
{
|
||||
if (await _plexServerApiClient.Ping(activeConnection, token, cancellationToken))
|
||||
{
|
||||
_logger.LogDebug("Plex connection is still active at {Uri}", activeConnection.Uri);
|
||||
var p = new PlexConnectionParametersViewModel(new Uri(activeConnection.Uri), token.AuthToken);
|
||||
_memoryCache.Set(request, p, TimeSpan.FromSeconds(30));
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, ConnectionParameters>> Validate(GetPlexConnectionParameters request) =>
|
||||
PlexMediaSourceMustExist(request)
|
||||
.BindT(MediaSourceMustHaveActiveConnection)
|
||||
.BindT(MediaSourceMustHaveToken);
|
||||
_logger.LogInformation("Plex connection is no longer active, searching for a new connection");
|
||||
|
||||
private Task<Validation<BaseError, PlexMediaSource>> PlexMediaSourceMustExist(
|
||||
GetPlexConnectionParameters request) =>
|
||||
_mediaSourceRepository.GetPlex(request.PlexMediaSourceId)
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Plex media source {request.PlexMediaSourceId} does not exist."));
|
||||
// check all connections for a working one
|
||||
Option<PlexConnection> maybeConnection = await FindConnectionToActivate(mediaSource, token);
|
||||
foreach (PlexConnection connection in maybeConnection)
|
||||
{
|
||||
var p = new PlexConnectionParametersViewModel(new Uri(connection.Uri), token.AuthToken);
|
||||
_memoryCache.Set(request, p, TimeSpan.FromMinutes(30));
|
||||
return p;
|
||||
}
|
||||
|
||||
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
|
||||
PlexMediaSource plexMediaSource)
|
||||
{
|
||||
Option<PlexConnection> maybeConnection =
|
||||
plexMediaSource.Connections.SingleOrDefault(c => c.IsActive);
|
||||
return maybeConnection.Map(connection => new ConnectionParameters(plexMediaSource, connection))
|
||||
.ToValidation<BaseError>("Plex media source requires an active connection");
|
||||
}
|
||||
return BaseError.New($"Plex media source {request.PlexMediaSourceId} requires an active connection");
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveToken(
|
||||
ConnectionParameters connectionParameters)
|
||||
{
|
||||
Option<PlexServerAuthToken> maybeToken = await
|
||||
_plexSecretStore.GetServerAuthToken(connectionParameters.PlexMediaSource.ClientIdentifier);
|
||||
return maybeToken.Map(token => connectionParameters with { PlexServerAuthToken = token })
|
||||
.ToValidation<BaseError>("Plex media source requires a token");
|
||||
}
|
||||
return BaseError.New($"Plex media source {request.PlexMediaSourceId} requires a token");
|
||||
}
|
||||
|
||||
private sealed record ConnectionParameters(PlexMediaSource PlexMediaSource, PlexConnection ActiveConnection)
|
||||
{
|
||||
public PlexServerAuthToken PlexServerAuthToken { get; set; }
|
||||
return BaseError.New($"Plex media source {request.PlexMediaSourceId} does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -7,6 +8,7 @@ public record AddProgramScheduleItem(
|
||||
int ProgramScheduleId,
|
||||
StartType StartType,
|
||||
TimeSpan? StartTime,
|
||||
FixedStartTimeBehavior? FixedStartTimeBehavior,
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
int? CollectionId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -7,4 +8,5 @@ public record CreateProgramSchedule(
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems,
|
||||
bool RandomStartPoint) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
|
||||
bool RandomStartPoint,
|
||||
FixedStartTimeBehavior FixedStartTimeBehavior) : IRequest<Either<BaseError, CreateProgramScheduleResult>>;
|
||||
|
||||
@@ -5,19 +5,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
public class CreateProgramScheduleHandler :
|
||||
public class CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) :
|
||||
IRequestHandler<CreateProgramSchedule, Either<BaseError, CreateProgramScheduleResult>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public CreateProgramScheduleHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<Either<BaseError, CreateProgramScheduleResult>> Handle(
|
||||
CreateProgramSchedule request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
Validation<BaseError, ProgramSchedule> validation = await Validate(dbContext, request);
|
||||
return await validation.Apply(ps => PersistProgramSchedule(dbContext, ps));
|
||||
@@ -45,7 +40,8 @@ public class CreateProgramScheduleHandler :
|
||||
KeepMultiPartEpisodesTogether = keepMultiPartEpisodesTogether,
|
||||
TreatCollectionsAsShows = keepMultiPartEpisodesTogether && request.TreatCollectionsAsShows,
|
||||
ShuffleScheduleItems = request.ShuffleScheduleItems,
|
||||
RandomStartPoint = request.RandomStartPoint
|
||||
RandomStartPoint = request.RandomStartPoint,
|
||||
FixedStartTimeBehavior = request.FixedStartTimeBehavior
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
public interface IProgramScheduleItemRequest
|
||||
{
|
||||
TimeSpan? StartTime { get; }
|
||||
FixedStartTimeBehavior? FixedStartTimeBehavior { get; }
|
||||
ProgramScheduleItemCollectionType CollectionType { get; }
|
||||
int? CollectionId { get; }
|
||||
int? MultiCollectionId { get; }
|
||||
|
||||
@@ -185,6 +185,7 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -211,6 +212,7 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -237,6 +239,7 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
@@ -264,6 +267,7 @@ public abstract class ProgramScheduleItemCommandBase
|
||||
ProgramScheduleId = programSchedule.Id,
|
||||
Index = index,
|
||||
StartTime = FixStartTime(item.StartTime),
|
||||
FixedStartTimeBehavior = item.FixedStartTimeBehavior,
|
||||
CollectionType = item.CollectionType,
|
||||
CollectionId = item.CollectionId,
|
||||
MultiCollectionId = item.MultiCollectionId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -7,6 +8,7 @@ public record ReplaceProgramScheduleItem(
|
||||
int Index,
|
||||
StartType StartType,
|
||||
TimeSpan? StartTime,
|
||||
FixedStartTimeBehavior? FixedStartTimeBehavior,
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
int? CollectionId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -8,4 +9,5 @@ public record UpdateProgramSchedule(
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems,
|
||||
bool RandomStartPoint) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
|
||||
bool RandomStartPoint,
|
||||
FixedStartTimeBehavior FixedStartTimeBehavior) : IRequest<Either<BaseError, UpdateProgramScheduleResult>>;
|
||||
|
||||
@@ -42,7 +42,8 @@ public class UpdateProgramScheduleHandler :
|
||||
programSchedule.KeepMultiPartEpisodesTogether != request.KeepMultiPartEpisodesTogether ||
|
||||
programSchedule.TreatCollectionsAsShows != request.TreatCollectionsAsShows ||
|
||||
programSchedule.ShuffleScheduleItems != request.ShuffleScheduleItems ||
|
||||
programSchedule.RandomStartPoint != request.RandomStartPoint;
|
||||
programSchedule.RandomStartPoint != request.RandomStartPoint ||
|
||||
programSchedule.FixedStartTimeBehavior != request.FixedStartTimeBehavior;
|
||||
|
||||
programSchedule.Name = request.Name;
|
||||
programSchedule.KeepMultiPartEpisodesTogether = request.KeepMultiPartEpisodesTogether;
|
||||
@@ -50,6 +51,7 @@ public class UpdateProgramScheduleHandler :
|
||||
request.TreatCollectionsAsShows;
|
||||
programSchedule.ShuffleScheduleItems = request.ShuffleScheduleItems;
|
||||
programSchedule.RandomStartPoint = request.RandomStartPoint;
|
||||
programSchedule.FixedStartTimeBehavior = request.FixedStartTimeBehavior;
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ internal static class Mapper
|
||||
programSchedule.KeepMultiPartEpisodesTogether,
|
||||
programSchedule.TreatCollectionsAsShows,
|
||||
programSchedule.ShuffleScheduleItems,
|
||||
programSchedule.RandomStartPoint);
|
||||
programSchedule.RandomStartPoint,
|
||||
programSchedule.FixedStartTimeBehavior);
|
||||
|
||||
internal static ProgramScheduleItemViewModel ProjectToViewModel(ProgramScheduleItem programScheduleItem) =>
|
||||
programScheduleItem switch
|
||||
@@ -22,6 +23,7 @@ internal static class Mapper
|
||||
duration.Index,
|
||||
duration.StartType,
|
||||
duration.StartTime,
|
||||
duration.FixedStartTimeBehavior,
|
||||
duration.CollectionType,
|
||||
duration.Collection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(duration.Collection)
|
||||
@@ -77,6 +79,7 @@ internal static class Mapper
|
||||
flood.Index,
|
||||
flood.StartType,
|
||||
flood.StartTime,
|
||||
flood.FixedStartTimeBehavior,
|
||||
flood.CollectionType,
|
||||
flood.Collection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(flood.Collection)
|
||||
@@ -129,6 +132,7 @@ internal static class Mapper
|
||||
multiple.Index,
|
||||
multiple.StartType,
|
||||
multiple.StartTime,
|
||||
multiple.FixedStartTimeBehavior,
|
||||
multiple.CollectionType,
|
||||
multiple.Collection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(multiple.Collection)
|
||||
@@ -182,6 +186,7 @@ internal static class Mapper
|
||||
one.Index,
|
||||
one.StartType,
|
||||
one.StartTime,
|
||||
one.FixedStartTimeBehavior,
|
||||
one.CollectionType,
|
||||
one.Collection != null
|
||||
? MediaCollections.Mapper.ProjectToViewModel(one.Collection)
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -13,6 +14,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
||||
int index,
|
||||
StartType startType,
|
||||
TimeSpan? startTime,
|
||||
FixedStartTimeBehavior? fixedStartTimeBehavior,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
MultiCollectionViewModel multiCollection,
|
||||
@@ -40,6 +42,7 @@ public record ProgramScheduleItemDurationViewModel : ProgramScheduleItemViewMode
|
||||
index,
|
||||
startType,
|
||||
startTime,
|
||||
fixedStartTimeBehavior,
|
||||
PlayoutMode.Duration,
|
||||
collectionType,
|
||||
collection,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -13,6 +14,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
|
||||
int index,
|
||||
StartType startType,
|
||||
TimeSpan? startTime,
|
||||
FixedStartTimeBehavior? fixedStartTimeBehavior,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
MultiCollectionViewModel multiCollection,
|
||||
@@ -37,6 +39,7 @@ public record ProgramScheduleItemFloodViewModel : ProgramScheduleItemViewModel
|
||||
index,
|
||||
startType,
|
||||
startTime,
|
||||
fixedStartTimeBehavior,
|
||||
PlayoutMode.Flood,
|
||||
collectionType,
|
||||
collection,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -13,6 +14,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
|
||||
int index,
|
||||
StartType startType,
|
||||
TimeSpan? startTime,
|
||||
FixedStartTimeBehavior? fixedStartTimeBehavior,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
MultiCollectionViewModel multiCollection,
|
||||
@@ -38,6 +40,7 @@ public record ProgramScheduleItemMultipleViewModel : ProgramScheduleItemViewMode
|
||||
index,
|
||||
startType,
|
||||
startTime,
|
||||
fixedStartTimeBehavior,
|
||||
PlayoutMode.Multiple,
|
||||
collectionType,
|
||||
collection,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -13,6 +14,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
|
||||
int index,
|
||||
StartType startType,
|
||||
TimeSpan? startTime,
|
||||
FixedStartTimeBehavior? fixedStartTimeBehavior,
|
||||
ProgramScheduleItemCollectionType collectionType,
|
||||
MediaCollectionViewModel collection,
|
||||
MultiCollectionViewModel multiCollection,
|
||||
@@ -37,6 +39,7 @@ public record ProgramScheduleItemOneViewModel : ProgramScheduleItemViewModel
|
||||
index,
|
||||
startType,
|
||||
startTime,
|
||||
fixedStartTimeBehavior,
|
||||
PlayoutMode.One,
|
||||
collectionType,
|
||||
collection,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ErsatzTV.Application.MediaCollections;
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Application.Watermarks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
@@ -11,6 +12,7 @@ public abstract record ProgramScheduleItemViewModel(
|
||||
int Index,
|
||||
StartType StartType,
|
||||
TimeSpan? StartTime,
|
||||
FixedStartTimeBehavior? FixedStartTimeBehavior,
|
||||
PlayoutMode PlayoutMode,
|
||||
ProgramScheduleItemCollectionType CollectionType,
|
||||
MediaCollectionViewModel Collection,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
public record ProgramScheduleViewModel(
|
||||
int Id,
|
||||
@@ -6,4 +8,5 @@ public record ProgramScheduleViewModel(
|
||||
bool KeepMultiPartEpisodesTogether,
|
||||
bool TreatCollectionsAsShows,
|
||||
bool ShuffleScheduleItems,
|
||||
bool RandomStartPoint);
|
||||
bool RandomStartPoint,
|
||||
FixedStartTimeBehavior FixedStartTimeBehavior);
|
||||
|
||||
@@ -3,18 +3,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.ProgramSchedules;
|
||||
|
||||
public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
|
||||
public class GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
: IRequestHandler<GetAllProgramSchedules, List<ProgramScheduleViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllProgramSchedulesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<List<ProgramScheduleViewModel>> Handle(
|
||||
GetAllProgramSchedules request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.ProgramSchedules
|
||||
.Map(
|
||||
ps => new ProgramScheduleViewModel(
|
||||
@@ -23,7 +19,8 @@ public class GetAllProgramSchedulesHandler : IRequestHandler<GetAllProgramSchedu
|
||||
ps.KeepMultiPartEpisodesTogether,
|
||||
ps.TreatCollectionsAsShows,
|
||||
ps.ShuffleScheduleItems,
|
||||
ps.RandomStartPoint))
|
||||
ps.RandomStartPoint,
|
||||
ps.FixedStartTimeBehavior))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -116,5 +116,5 @@ internal static class Mapper
|
||||
Playouts.Mapper.GetDisplayTitle(playoutItem),
|
||||
playoutItem.StartOffset.TimeOfDay,
|
||||
playoutItem.FinishOffset.TimeOfDay,
|
||||
Playouts.Mapper.GetDisplayDuration(playoutItem.FinishOffset - playoutItem.StartOffset));
|
||||
playoutItem.GetDisplayDuration());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ public class GetErrorProcessHandler : FFmpegProcessHandler<GetErrorProcess>
|
||||
request.ErrorMessage,
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
|
||||
+30
-3
@@ -16,6 +16,7 @@ using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Streaming;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.FFmpeg.State;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -256,6 +257,28 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
cancellationToken);
|
||||
|
||||
// override watermark as song_progress_overlay.png
|
||||
if (videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true })
|
||||
{
|
||||
double ratio = channel.FFmpegProfile.Resolution.Width / (double)channel.FFmpegProfile.Resolution.Height;
|
||||
bool is43 = Math.Abs(ratio - 4.0 / 3.0) < 0.01;
|
||||
string image = is43 ? "song_progress_overlay_43.png" : "song_progress_overlay.png";
|
||||
|
||||
disableWatermarks = false;
|
||||
playoutItemWatermark = new ChannelWatermark
|
||||
{
|
||||
Mode = ChannelWatermarkMode.Permanent,
|
||||
Size = WatermarkSize.Scaled,
|
||||
WidthPercent = 100,
|
||||
HorizontalMarginPercent = 0,
|
||||
VerticalMarginPercent = 0,
|
||||
Opacity = 100,
|
||||
Location = WatermarkLocation.TopLeft,
|
||||
ImageSource = ChannelWatermarkImageSource.Resource,
|
||||
Image = image
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (playoutItemWithPath.PlayoutItem.MediaItem is Image)
|
||||
@@ -293,6 +316,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
request.StartAtZero ? start : now,
|
||||
playoutItemWatermark,
|
||||
maybeGlobalWatermark,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
@@ -338,6 +362,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
@@ -351,6 +376,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
error.Value,
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
@@ -364,6 +390,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset,
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
@@ -591,7 +618,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
|
||||
return new PlayoutItemWithPath(
|
||||
playoutItem,
|
||||
$"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}");
|
||||
$"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{pmf.Key}");
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -609,7 +636,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
{
|
||||
return new PlayoutItemWithPath(
|
||||
playoutItem,
|
||||
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}");
|
||||
$"http://localhost:{Settings.StreamingPort}/media/jellyfin/{itemId}");
|
||||
}
|
||||
|
||||
// attempt to remotely stream emby
|
||||
@@ -624,7 +651,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
{
|
||||
return new PlayoutItemWithPath(
|
||||
playoutItem,
|
||||
$"http://localhost:{Settings.ListenPort}/media/emby/{itemId}");
|
||||
$"http://localhost:{Settings.StreamingPort}/media/emby/{itemId}");
|
||||
}
|
||||
|
||||
return new PlayoutItemDoesNotExistOnDisk(path);
|
||||
|
||||
@@ -135,7 +135,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
|
||||
|
||||
foreach (int playoutId in playoutIdsToCheck)
|
||||
{
|
||||
_entityLocker.LockPlayout(playoutId);
|
||||
await _entityLocker.LockPlayout(playoutId);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Checking playouts {PlayoutIds} for text subtitles to extract", playoutIdsToCheck);
|
||||
@@ -193,7 +193,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
|
||||
|
||||
foreach (int playoutId in playoutIdsToCheck)
|
||||
{
|
||||
_entityLocker.UnlockPlayout(playoutId);
|
||||
await _entityLocker.UnlockPlayout(playoutId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
|
||||
@@ -505,7 +505,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
|
||||
|
||||
foreach (int plexMediaSourceId in maybeId)
|
||||
{
|
||||
return $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}";
|
||||
return $"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{pmf.Key}";
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -514,11 +514,11 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
|
||||
return mediaItem switch
|
||||
{
|
||||
JellyfinMovie jellyfinMovie =>
|
||||
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinMovie.ItemId}",
|
||||
$"http://localhost:{Settings.StreamingPort}/media/jellyfin/{jellyfinMovie.ItemId}",
|
||||
JellyfinEpisode jellyfinEpisode =>
|
||||
$"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinEpisode.ItemId}",
|
||||
EmbyMovie embyMovie => $"http://localhost:{Settings.ListenPort}/media/emby/{embyMovie.ItemId}",
|
||||
EmbyEpisode embyEpisode => $"http://localhost:{Settings.ListenPort}/media/emby/{embyEpisode.ItemId}",
|
||||
$"http://localhost:{Settings.StreamingPort}/media/jellyfin/{jellyfinEpisode.ItemId}",
|
||||
EmbyMovie embyMovie => $"http://localhost:{Settings.StreamingPort}/media/emby/{embyMovie.ItemId}",
|
||||
EmbyEpisode embyEpisode => $"http://localhost:{Settings.StreamingPort}/media/emby/{embyEpisode.ItemId}",
|
||||
_ => file.Path
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -79,7 +79,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
|
||||
{
|
||||
foreach (string subtitlePath in maybeSubtitle.Map(s => s.Path))
|
||||
{
|
||||
return $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{subtitlePath}";
|
||||
return $"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{subtitlePath}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
|
||||
string extension = Subtitle.ExtensionForCodec(subtitle.Codec);
|
||||
var subtitlePath =
|
||||
$"Videos/{jellyfinItemId}/{jellyfinItemId}/Subtitles/{index}/{index}/Stream.{extension}";
|
||||
return $"http://localhost:{Settings.ListenPort}/media/jellyfin/{subtitlePath}";
|
||||
return $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{subtitlePath}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
|
||||
string extension = Subtitle.ExtensionForCodec(subtitle.Codec);
|
||||
var subtitlePath =
|
||||
$"Videos/{embyItemId}/{subtitle.Path}/Subtitles/{subtitle.StreamIndex}/Stream.{extension}";
|
||||
return $"http://localhost:{Settings.ListenPort}/media/emby/{subtitlePath}";
|
||||
return $"http://localhost:{Settings.StreamingPort}/media/emby/{subtitlePath}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -92,7 +93,12 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
|
||||
if (!_memoryCache.TryGetValue("ffmpeg.render_devices", out List<string> vaapiDevices))
|
||||
{
|
||||
vaapiDevices = new List<string> { "/dev/dri/renderD128" };
|
||||
vaapiDevices = ["/dev/dri/renderD128"];
|
||||
}
|
||||
|
||||
if (!_memoryCache.TryGetValue("ffmpeg.vaapi_displays", out List<string> vaapiDisplays))
|
||||
{
|
||||
vaapiDisplays = ["drm"];
|
||||
}
|
||||
|
||||
foreach (string qsvDevice in vaapiDevices)
|
||||
@@ -109,27 +115,37 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
var allDrivers = new List<VaapiDriver>
|
||||
{ VaapiDriver.iHD, VaapiDriver.i965, VaapiDriver.RadeonSI, VaapiDriver.Nouveau };
|
||||
|
||||
foreach (string display in vaapiDisplays)
|
||||
foreach (VaapiDriver activeDriver in allDrivers)
|
||||
foreach (string vaapiDevice in vaapiDevices)
|
||||
{
|
||||
foreach (string vaapiDevice in vaapiDevices)
|
||||
foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput(
|
||||
display,
|
||||
Optional(GetDriverName(activeDriver)),
|
||||
vaapiDevice))
|
||||
{
|
||||
foreach (string output in await _hardwareCapabilitiesFactory.GetVaapiOutput(
|
||||
Optional(GetDriverName(activeDriver)),
|
||||
vaapiDevice))
|
||||
{
|
||||
vaapiCapabilities +=
|
||||
$"Checking driver {activeDriver} device {vaapiDevice}{Environment.NewLine}{Environment.NewLine}";
|
||||
vaapiCapabilities += output;
|
||||
vaapiCapabilities += Environment.NewLine + Environment.NewLine;
|
||||
}
|
||||
vaapiCapabilities +=
|
||||
$"Checking display [{display}] driver [{activeDriver}] device [{vaapiDevice}]{Environment.NewLine}{Environment.NewLine}";
|
||||
vaapiCapabilities += output;
|
||||
vaapiCapabilities += Environment.NewLine + Environment.NewLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var environment = new Dictionary<string, string>();
|
||||
foreach (DictionaryEntry de in Environment.GetEnvironmentVariables())
|
||||
{
|
||||
if (de is { Key: string key, Value: string value } && key.StartsWith("ETV_", 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,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Shouldly;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Domain;
|
||||
|
||||
[TestFixture]
|
||||
public class PlayoutItemTests
|
||||
{
|
||||
[Test]
|
||||
public void GetDisplayDuration_ShortDuration()
|
||||
{
|
||||
var item = new PlayoutItem
|
||||
{
|
||||
Start = DateTime.UtcNow.Date,
|
||||
Finish = DateTime.UtcNow.Date.AddHours(3).AddMinutes(5).AddSeconds(4)
|
||||
};
|
||||
|
||||
string actual = item.GetDisplayDuration();
|
||||
|
||||
actual.ShouldBe("3:05:04");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetDisplayDuration_LongDuration()
|
||||
{
|
||||
var item = new PlayoutItem
|
||||
{
|
||||
Start = DateTime.UtcNow.Date,
|
||||
Finish = DateTime.UtcNow.Date.AddHours(27).AddMinutes(5).AddSeconds(4)
|
||||
};
|
||||
|
||||
string actual = item.GetDisplayDuration();
|
||||
|
||||
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="3.1.0" />
|
||||
<PackageReference Include="CliWrap" Version="3.6.6" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<PackageReference Include="Bugsnag" Version="4.0.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.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.11.20">
|
||||
<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.1.0" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="Serilog" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<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.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>
|
||||
|
||||
@@ -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;
|
||||
@@ -32,7 +32,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.FormatFlags.Should().NotContain("+genpts");
|
||||
actual.FormatFlags.ShouldNotContain("+genpts");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -57,7 +57,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ThreadCount.Should().Be(7);
|
||||
actual.ThreadCount.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -78,7 +78,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ThreadCount.Should().Be(7);
|
||||
actual.ThreadCount.ShouldBe(7);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -100,8 +100,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]
|
||||
@@ -123,8 +123,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]
|
||||
@@ -145,7 +145,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.RealtimeOutput.Should().BeTrue();
|
||||
actual.RealtimeOutput.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -166,7 +166,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.RealtimeOutput.Should().BeTrue();
|
||||
actual.RealtimeOutput.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -189,8 +189,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]
|
||||
@@ -213,8 +213,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]
|
||||
@@ -241,7 +241,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -268,7 +268,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -295,8 +295,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -323,8 +323,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeTrue();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -351,9 +351,69 @@ 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]
|
||||
public void Should_ScaleBeyondMinSize_ForCrop_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
Resolution = new Resolution { Width = 1280, Height = 720 },
|
||||
ScalingBehavior = ScalingBehavior.Crop
|
||||
};
|
||||
|
||||
var version = new MediaVersion { Width = 944, Height = 720, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
false,
|
||||
None);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.ShouldBe(1280);
|
||||
scaledSize.Height.ShouldBe(976);
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_ScaleDownToMinSize_ForCrop_ForTransportStream()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile() with
|
||||
{
|
||||
Resolution = new Resolution { Width = 1280, Height = 720 },
|
||||
ScalingBehavior = ScalingBehavior.Crop
|
||||
};
|
||||
|
||||
var version = new MediaVersion { Width = 1920, Height = 816, SampleAspectRatio = "1:1" };
|
||||
|
||||
FFmpegPlaybackSettings actual = FFmpegPlaybackSettingsCalculator.CalculateSettings(
|
||||
StreamingMode.TransportStream,
|
||||
ffmpegProfile,
|
||||
version,
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
false,
|
||||
None);
|
||||
|
||||
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
|
||||
scaledSize.Width.ShouldBe(1694);
|
||||
scaledSize.Height.ShouldBe(720);
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -380,8 +440,8 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.ScaledSize.IsNone.Should().BeTrue();
|
||||
actual.PadToDesiredResolution.Should().BeFalse();
|
||||
actual.ScaledSize.IsNone.ShouldBeTrue();
|
||||
actual.PadToDesiredResolution.ShouldBeFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -410,9 +470,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]
|
||||
@@ -442,9 +502,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]
|
||||
@@ -473,9 +533,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]
|
||||
@@ -504,9 +564,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]
|
||||
@@ -534,9 +594,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]
|
||||
@@ -565,9 +625,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]
|
||||
@@ -595,9 +655,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]
|
||||
@@ -626,9 +686,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]
|
||||
@@ -652,7 +712,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Aac);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -676,7 +736,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Aac);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Aac);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -700,7 +760,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioFormat.Should().Be(FFmpegProfileAudioFormat.Copy);
|
||||
actual.AudioFormat.ShouldBe(FFmpegProfileAudioFormat.Copy);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -725,7 +785,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioBitrate.IfNone(0).Should().Be(2424);
|
||||
actual.AudioBitrate.IfNone(0).ShouldBe(2424);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -750,7 +810,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioBufferSize.IfNone(0).Should().Be(2424);
|
||||
actual.AudioBufferSize.IfNone(0).ShouldBe(2424);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -775,7 +835,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioChannels.IfNone(0).Should().Be(6);
|
||||
actual.AudioChannels.IfNone(0).ShouldBe(6);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -800,7 +860,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||
actual.AudioSampleRate.IfNone(0).ShouldBe(48);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -824,7 +884,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioChannels.IfNone(0).Should().Be(6);
|
||||
actual.AudioChannels.IfNone(0).ShouldBe(6);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -848,7 +908,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.AudioSampleRate.IfNone(0).Should().Be(48);
|
||||
actual.AudioSampleRate.IfNone(0).ShouldBe(48);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -878,7 +938,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]
|
||||
@@ -902,7 +962,7 @@ public class FFmpegPlaybackSettingsCalculatorTests
|
||||
false,
|
||||
None);
|
||||
|
||||
actual.NormalizeLoudnessMode.Should().Be(NormalizeLoudnessMode.LoudNorm);
|
||||
actual.NormalizeLoudnessMode.ShouldBe(NormalizeLoudnessMode.LoudNorm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,7 +988,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,65 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Should_Select_Audio_Stream_With_Preferred_Title()
|
||||
{
|
||||
// skip movie/episode script paths by using other video
|
||||
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 = 6,
|
||||
Language = "eng",
|
||||
Title = "Another Title",
|
||||
Default = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var audioVersion = new MediaItemAudioVersion(mediaItem, mediaVersion);
|
||||
var channel = new Channel(Guid.NewGuid())
|
||||
{
|
||||
PreferredAudioTitle = "Some"
|
||||
};
|
||||
|
||||
ISearchRepository searchRepository = Substitute.For<ISearchRepository>();
|
||||
searchRepository.GetAllThreeLetterLanguageCodes(Arg.Any<List<string>>())
|
||||
.Returns(Task.FromResult(new List<string> { "jpn", "eng" }));
|
||||
|
||||
var selector = new FFmpegStreamSelector(
|
||||
new ScriptEngine(Substitute.For<ILogger<ScriptEngine>>()),
|
||||
Substitute.For<IStreamSelectorRepository>(),
|
||||
searchRepository,
|
||||
Substitute.For<IConfigElementRepository>(),
|
||||
Substitute.For<ILocalFileSystem>(),
|
||||
Substitute.For<ILogger<FFmpegStreamSelector>>());
|
||||
|
||||
Option<MediaStream> selectedStream = await selector.SelectAudioStream(audioVersion, StreamingMode.TransportStream, channel, null, channel.PreferredAudioTitle);
|
||||
selectedStream.IsSome.ShouldBeTrue();
|
||||
foreach (MediaStream stream in selectedStream)
|
||||
{
|
||||
stream.Language.ShouldBe("ja");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user