Compare commits
281
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1f294c1f | ||
|
|
ea08453913 | ||
|
|
87deaa6f3a | ||
|
|
9d99c19ea4 | ||
|
|
49d14b05f6 | ||
|
|
a8ba9edf2b | ||
|
|
89811a1203 | ||
|
|
534e2c4512 | ||
|
|
c1e148633d | ||
|
|
a9dff5eff7 | ||
|
|
a2da043f4b | ||
|
|
252c185562 | ||
|
|
a47987a9d7 | ||
|
|
5937211bb8 | ||
|
|
e32dbd0474 | ||
|
|
6bcc1ede2b | ||
|
|
6c9764a51e | ||
|
|
ff5438459c | ||
|
|
0c53a4509c | ||
|
|
5fd315ead8 | ||
|
|
f02b0ac345 | ||
|
|
fd83007296 | ||
|
|
70ca5bf050 | ||
|
|
eed9f60273 | ||
|
|
0e2e6cd52e | ||
|
|
c9b557f2e6 | ||
|
|
cde869f3eb | ||
|
|
90d6a59d3f | ||
|
|
b972947747 | ||
|
|
17bc988b49 | ||
|
|
749eea836b | ||
|
|
37c52c4cb4 | ||
|
|
33ba58aa68 | ||
|
|
5f6043e593 | ||
|
|
96e95a21fb | ||
|
|
9168fd6bf2 | ||
|
|
14413f62a7 | ||
|
|
34c71a0c12 | ||
|
|
a487e7fe15 | ||
|
|
cd4ea42597 | ||
|
|
a3d42145f7 | ||
|
|
261cf5052a | ||
|
|
de9af2f0f6 | ||
|
|
8d4e18ed2f | ||
|
|
1ee01c1d78 | ||
|
|
7de50dd916 | ||
|
|
744fd3beaa | ||
|
|
861c95e1bd | ||
|
|
bb5b9f9be4 | ||
|
|
135628441a | ||
|
|
4aa7204984 | ||
|
|
1af59a0337 | ||
|
|
c4c97fcc8c | ||
|
|
9c46e42792 | ||
|
|
efa803aab6 | ||
|
|
6ea02a2d77 | ||
|
|
631f7d2d5e | ||
|
|
e44a4cb2e1 | ||
|
|
f4b95419a6 | ||
|
|
1a5cf49563 | ||
|
|
efef0b0fee | ||
|
|
ee7b8a71ab | ||
|
|
e7c9a51e96 | ||
|
|
78a954f365 | ||
|
|
355c0b7be9 | ||
|
|
3bcb2d36f9 | ||
|
|
b240de9d4a | ||
|
|
f5001837cb | ||
|
|
6ea916b1f0 | ||
|
|
db6fd22215 | ||
|
|
691842008d | ||
|
|
685f78bef8 | ||
|
|
3ce267863b | ||
|
|
e4231cb57d | ||
|
|
03946b13ca | ||
|
|
f1a81bf086 | ||
|
|
7a88374362 | ||
|
|
663a62431b | ||
|
|
1d4acc284d | ||
|
|
0440f7643b | ||
|
|
0f4219f731 | ||
|
|
cbe5d47611 | ||
|
|
afa52ccc89 | ||
|
|
7d1163c68f | ||
|
|
883492bd33 | ||
|
|
a4eac4feea | ||
|
|
dab58f5840 | ||
|
|
176f136c23 | ||
|
|
816d77e15b | ||
|
|
7c4d47a211 | ||
|
|
d9d2cfa8be | ||
|
|
8036e46966 | ||
|
|
594ce437fb | ||
|
|
004c43f895 | ||
|
|
257384ea9b | ||
|
|
637f3a0c8b | ||
|
|
7346808059 | ||
|
|
4210d97ee2 | ||
|
|
6a8ecd2532 | ||
|
|
9b834f7cbe | ||
|
|
7b73677bad | ||
|
|
85b2a46353 | ||
|
|
6f40f2cbd6 | ||
|
|
b62ee4dee9 | ||
|
|
a6e7f192cc | ||
|
|
59a1a4a8dc | ||
|
|
85a9afb51c | ||
|
|
246b4d7591 | ||
|
|
ae2c6350e1 | ||
|
|
ce228604e8 | ||
|
|
3656e932d3 | ||
|
|
73887706ed | ||
|
|
abc103308b | ||
|
|
3773bbec19 | ||
|
|
e223d6a43f | ||
|
|
8369111e31 | ||
|
|
35ba2bab2c | ||
|
|
094ed71ad0 | ||
|
|
89e24b2b78 | ||
|
|
848795af32 | ||
|
|
56f94f489a | ||
|
|
475dc7660b | ||
|
|
db3dfbd446 | ||
|
|
b4c9cdbbfa | ||
|
|
7f84933c0b | ||
|
|
1e35e9a5b0 | ||
|
|
7edf6f5d13 | ||
|
|
919325033d | ||
|
|
2cb5252320 | ||
|
|
015232fad6 | ||
|
|
af51b790b6 | ||
|
|
9195ef7878 | ||
|
|
dfc4c7a284 | ||
|
|
a6b15f68c9 | ||
|
|
0edfb71f8d | ||
|
|
21b90a1b6c | ||
|
|
1582f5dd15 | ||
|
|
fd3b72525d | ||
|
|
55d1871d94 | ||
|
|
a90eb2d4de | ||
|
|
ed3f1b1dad | ||
|
|
8e08ff059f | ||
|
|
fb8c3a0453 | ||
|
|
e45fb67769 | ||
|
|
3a40d6ce77 | ||
|
|
ac048b72ae | ||
|
|
852728c816 | ||
|
|
096f2d42e8 | ||
|
|
1b29e252ff | ||
|
|
a4dc9bfb31 | ||
|
|
184c21a91b | ||
|
|
6ea3191cf8 | ||
|
|
d487bbca08 | ||
|
|
05034b47e2 | ||
|
|
b0c85b6478 | ||
|
|
f1356563da | ||
|
|
c0aad028a8 | ||
|
|
dae06ec0ef | ||
|
|
72f452fd36 | ||
|
|
aaf832c0b6 | ||
|
|
08a18daf23 | ||
|
|
90c1c61a09 | ||
|
|
053db71d44 | ||
|
|
11f90f5d44 | ||
|
|
bda4117655 | ||
|
|
3240703840 | ||
|
|
53a7570ba3 | ||
|
|
0e789fd6d8 | ||
|
|
0136de700c | ||
|
|
2ea0e64ac1 | ||
|
|
5993f23ec5 | ||
|
|
417f35a834 | ||
|
|
a74547997d | ||
|
|
a2f74dd284 | ||
|
|
373daf9ce6 | ||
|
|
68693cffa0 | ||
|
|
6d147de2f3 | ||
|
|
f4a63a1a1a | ||
|
|
bc9d17ca25 | ||
|
|
42e13cbbaf | ||
|
|
6cc61f3212 | ||
|
|
4cf44616a8 | ||
|
|
33aaadae68 | ||
|
|
fe3f8e391e | ||
|
|
1a68dd040a | ||
|
|
67761c1a14 | ||
|
|
1802f9d797 | ||
|
|
69354c9296 | ||
|
|
0021e21b50 | ||
|
|
cdf7765059 | ||
|
|
71658c448f | ||
|
|
3ecdd741a5 | ||
|
|
0daeb844b9 | ||
|
|
22da19845b | ||
|
|
3a6d9e9f39 | ||
|
|
7ed4b8ae3c | ||
|
|
be7311e620 | ||
|
|
03be372070 | ||
|
|
d196308ee9 | ||
|
|
3d68b0f055 | ||
|
|
37e32f06ad | ||
|
|
c43ca2837d | ||
|
|
992121f308 | ||
|
|
04adbfeffa | ||
|
|
1fc905c6ad | ||
|
|
4b5dff2159 | ||
|
|
2a5edf8214 | ||
|
|
69912c8cae | ||
|
|
fd3de2d82a | ||
|
|
6ba9404752 | ||
|
|
db080375c5 | ||
|
|
9abc7ad8b7 | ||
|
|
9e531a82d7 | ||
|
|
d84bd2b948 | ||
|
|
d7d3ec1235 | ||
|
|
742ac21ad7 | ||
|
|
819b55e21f | ||
|
|
cf5718c288 | ||
|
|
adc7982955 | ||
|
|
67a6f554d0 | ||
|
|
609df217ae | ||
|
|
d3086264c7 | ||
|
|
8cd9b23787 | ||
|
|
dc5c9e42ff | ||
|
|
2dd267e4db | ||
|
|
b069a21473 | ||
|
|
6c8813ce22 | ||
|
|
b5de5e2b7f | ||
|
|
4b7da4e468 | ||
|
|
ae8e795228 | ||
|
|
334781485d | ||
|
|
27fefa1b38 | ||
|
|
fc3175591e | ||
|
|
3363d2c9d7 | ||
|
|
1d5217fa84 | ||
|
|
904cdb8780 | ||
|
|
85fee64565 | ||
|
|
13cfb9728f | ||
|
|
60b82876ea | ||
|
|
a99249c375 | ||
|
|
36e6ef4c18 | ||
|
|
21e53532c1 | ||
|
|
a864d53327 | ||
|
|
e6446f9983 | ||
|
|
ad40213f90 | ||
|
|
45c6d20fd0 | ||
|
|
5439db89a7 | ||
|
|
a39231bb5a | ||
|
|
4c8584b517 | ||
|
|
ca8bcacbd3 | ||
|
|
f27286d1dd | ||
|
|
23870b75f7 | ||
|
|
7f5a91c643 | ||
|
|
f1f50e883c | ||
|
|
7506f49f5b | ||
|
|
944f1e4307 | ||
|
|
f7de9ac5ea | ||
|
|
1eb51ad2f4 | ||
|
|
c3e0aaf0b7 | ||
|
|
b9912b47df | ||
|
|
55fb2624e7 | ||
|
|
8ced20dc39 | ||
|
|
e718cb0faf | ||
|
|
e218ff9a6d | ||
|
|
c2a49cbaea | ||
|
|
17e74f7314 | ||
|
|
2032bb4777 | ||
|
|
7877ec641e | ||
|
|
767a9779bb | ||
|
|
bb9127e546 | ||
|
|
c932577cb8 | ||
|
|
ad2685fb2e | ||
|
|
96bc2c28f2 | ||
|
|
a076b3eb30 | ||
|
|
fc360602ad | ||
|
|
d8b4d00a73 | ||
|
|
0638ac8a5e | ||
|
|
f1f09bd4cb | ||
|
|
f6680f29e7 | ||
|
|
1c0413452b | ||
|
|
77308a9ac5 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"jetbrains.resharper.globaltools": {
|
"jetbrains.resharper.globaltools": {
|
||||||
"version": "2021.1.3",
|
"version": "2021.2.2",
|
||||||
"commands": [
|
"commands": [
|
||||||
"jb"
|
"jb"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,3 +6,21 @@ updates:
|
|||||||
interval: daily
|
interval: daily
|
||||||
assignees:
|
assignees:
|
||||||
- jasongdove
|
- jasongdove
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/docker"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
assignees:
|
||||||
|
- jasongdove
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/docker/nvidia"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
assignees:
|
||||||
|
- jasongdove
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/docker/vaapi"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
assignees:
|
||||||
|
- jasongdove
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
name: Build Artifacts
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
description: 'Release tag'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
release_version:
|
||||||
|
description: 'Release version number (e.g. v0.3.7-alpha)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
info_version:
|
||||||
|
description: 'Informational version number (e.g. 0.3.7-alpha)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
apple_developer_certificate_p12_base64:
|
||||||
|
required: true
|
||||||
|
apple_developer_certificate_password:
|
||||||
|
required: true
|
||||||
|
ac_username:
|
||||||
|
required: true
|
||||||
|
ac_password:
|
||||||
|
required: true
|
||||||
|
gh_token:
|
||||||
|
required: true
|
||||||
|
jobs:
|
||||||
|
build_and_upload_mac:
|
||||||
|
name: Mac Build & Upload
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-latest
|
||||||
|
kind: macOS
|
||||||
|
target: osx-x64
|
||||||
|
- os: macos-latest
|
||||||
|
kind: macOS
|
||||||
|
target: osx-arm64
|
||||||
|
steps:
|
||||||
|
- name: Get the sources
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dotnet restore -r "${{ matrix.target}}"
|
||||||
|
|
||||||
|
- name: Import Code-Signing Certificates
|
||||||
|
uses: Apple-Actions/import-codesign-certs@v1
|
||||||
|
with:
|
||||||
|
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
|
||||||
|
p12-password: ${{ secrets.apple_developer_certificate_password }}
|
||||||
|
|
||||||
|
- name: Calculate Release Name
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
|
||||||
|
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
|
|
||||||
|
- name: Bundle
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
brew install coreutils
|
||||||
|
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||||
|
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||||
|
scripts/macOS/bundle.sh
|
||||||
|
|
||||||
|
- name: Sign
|
||||||
|
shell: bash
|
||||||
|
run: scripts/macOS/sign.sh
|
||||||
|
|
||||||
|
- name: Create DMG
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
brew install create-dmg
|
||||||
|
create-dmg \
|
||||||
|
--volname "ErsatzTV" \
|
||||||
|
--volicon "artwork/ErsatzTV.icns" \
|
||||||
|
--window-pos 200 120 \
|
||||||
|
--window-size 800 400 \
|
||||||
|
--icon-size 100 \
|
||||||
|
--icon "ErsatzTV.app" 200 190 \
|
||||||
|
--hide-extension "ErsatzTV.app" \
|
||||||
|
--app-drop-link 600 185 \
|
||||||
|
"ErsatzTV.dmg" \
|
||||||
|
"ErsatzTV.app/"
|
||||||
|
|
||||||
|
- name: Notarize
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
brew tap mitchellh/gon
|
||||||
|
brew install mitchellh/gon/gon
|
||||||
|
gon -log-level=debug -log-json ./gon.json
|
||||||
|
env:
|
||||||
|
AC_USERNAME: ${{ secrets.ac_username }}
|
||||||
|
AC_PASSWORD: ${{ secrets.ac_password }}
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
|
||||||
|
rm -r publish
|
||||||
|
rm -r ErsatzTV.app
|
||||||
|
|
||||||
|
- name: Delete old release assets
|
||||||
|
uses: mknejp/delete-release-assets@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.gh_token }}
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
fail-if-no-assets: false
|
||||||
|
assets: |
|
||||||
|
*${{ matrix.target }}.dmg
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ inputs.release_tag }}
|
||||||
|
files: |
|
||||||
|
${{ env.RELEASE_NAME }}.dmg
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.gh_token }}
|
||||||
|
build_and_upload:
|
||||||
|
name: Build & Upload
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
kind: linux
|
||||||
|
target: linux-x64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
kind: linux
|
||||||
|
target: linux-arm
|
||||||
|
- os: ubuntu-latest
|
||||||
|
kind: linux
|
||||||
|
target: linux-arm64
|
||||||
|
- os: windows-latest
|
||||||
|
kind: windows
|
||||||
|
target: win-x64
|
||||||
|
steps:
|
||||||
|
- name: Get the sources
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dotnet restore -r "${{ matrix.target}}"
|
||||||
|
|
||||||
|
- uses: suisei-cn/actions-download-file@v1
|
||||||
|
if: ${{ matrix.kind }} == "windows"
|
||||||
|
id: downloadffmpeg
|
||||||
|
name: Download ffmpeg
|
||||||
|
with:
|
||||||
|
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build.7z"
|
||||||
|
target: ffmpeg/
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Define some variables for things we need
|
||||||
|
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
|
||||||
|
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Build everything
|
||||||
|
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
|
|
||||||
|
# Build Windows launcher
|
||||||
|
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||||
|
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download ffmpeg
|
||||||
|
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||||
|
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
|
||||||
|
rm -f "$release_name/ffplay.exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pack files
|
||||||
|
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||||
|
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
||||||
|
else
|
||||||
|
tar czvf "${release_name}.tar.gz" "$release_name"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete output directory
|
||||||
|
rm -r "$release_name"
|
||||||
|
env:
|
||||||
|
AC_USERNAME: ${{ secrets.ac_username }}
|
||||||
|
AC_PASSWORD: ${{ secrets.ac_password }}
|
||||||
|
|
||||||
|
- name: Delete old release assets
|
||||||
|
uses: mknejp/delete-release-assets@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.gh_token }}
|
||||||
|
tag: ${{ inputs.release_tag }}
|
||||||
|
fail-if-no-assets: false
|
||||||
|
assets: |
|
||||||
|
*${{ matrix.target }}.zip
|
||||||
|
*${{ matrix.target }}.tar.gz
|
||||||
|
- name: Publish
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ inputs.release_tag }}
|
||||||
|
files: |
|
||||||
|
${{ env.RELEASE_NAME }}.zip
|
||||||
|
${{ env.RELEASE_NAME }}.tar.gz
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.gh_token }}
|
||||||
+40
-92
@@ -1,110 +1,58 @@
|
|||||||
name: Build
|
name: Build
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
build_and_test:
|
calculate_version:
|
||||||
runs-on: ${{ matrix.os }}
|
name: Calculate version information
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ windows-latest, ubuntu-latest, macos-latest ]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup .NET Core
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: 5.0.x
|
|
||||||
|
|
||||||
- name: Clean
|
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build --configuration Release --no-restore
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test --no-restore --verbosity normal
|
|
||||||
build_and_push:
|
|
||||||
name: Build & Publish to Docker Hub
|
|
||||||
needs: build_and_test
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Extract Docker Tag
|
||||||
- name: Extract Git Tag
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(git describe --tags --abbrev=0)
|
tag=$(git describe --tags --abbrev=0)
|
||||||
tag2="${tag:1}"
|
tag2="${tag:1}"
|
||||||
short=$(git rev-parse --short HEAD)
|
short=$(git rev-parse --short HEAD)
|
||||||
final="${tag2/prealpha/$short}"
|
final="${tag2/alpha/$short}"
|
||||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||||
|
- name: Extract Artifacts Version
|
||||||
- name: Set up Docker Buildx Base
|
shell: bash
|
||||||
uses: docker/setup-buildx-action@v1
|
run: |
|
||||||
id: builder-base
|
tag=$(git describe --tags --abbrev=0)
|
||||||
|
short=$(git rev-parse --short HEAD)
|
||||||
- name: Set up Docker Buildx NVIDIA
|
final="${tag/alpha/$short}"
|
||||||
uses: docker/setup-buildx-action@v1
|
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||||
id: builder-nvidia
|
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||||
|
outputs:
|
||||||
- name: Set up Docker Buildx VAAPI
|
git_tag: ${{ env.GIT_TAG }}
|
||||||
uses: docker/setup-buildx-action@v1
|
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||||
id: builder-vaapi
|
info_version: ${{ env.INFO_VERSION }}
|
||||||
|
build_and_upload:
|
||||||
- name: Login to DockerHub
|
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
|
||||||
uses: docker/login-action@v1
|
needs: calculate_version
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
release_tag: develop
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||||
|
info_version: ${{ needs.calculate_version.outputs.info_version }}
|
||||||
- name: Build and push base
|
secrets:
|
||||||
uses: docker/build-push-action@v2
|
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||||
with:
|
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||||
builder: ${{ steps.builder-base.outputs.name }}
|
ac_username: ${{ secrets.AC_USERNAME }}
|
||||||
context: .
|
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||||
file: ./docker/Dockerfile
|
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
push: true
|
build_and_push:
|
||||||
build-args: |
|
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker
|
needs: calculate_version
|
||||||
tags: |
|
with:
|
||||||
jasongdove/ersatztv:develop
|
base_version: develop
|
||||||
jasongdove/ersatztv:${{ github.sha }}
|
info_version: ${{ needs.calculate_version.outputs.git_tag }}
|
||||||
|
tag_version: ${{ github.sha }}
|
||||||
- name: Build and push nvidia
|
secrets:
|
||||||
uses: docker/build-push-action@v2
|
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
with:
|
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
|
||||||
context: .
|
|
||||||
file: ./docker/nvidia/Dockerfile
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
|
|
||||||
tags: |
|
|
||||||
jasongdove/ersatztv:develop-nvidia
|
|
||||||
jasongdove/ersatztv:${{ github.sha }}-nvidia
|
|
||||||
|
|
||||||
- name: Build and push vaapi
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
builder: ${{ steps.builder-vaapi.outputs.name }}
|
|
||||||
context: .
|
|
||||||
file: ./docker/vaapi/Dockerfile
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
|
|
||||||
tags: |
|
|
||||||
jasongdove/ersatztv:develop-vaapi
|
|
||||||
jasongdove/ersatztv:${{ github.sha }}-vaapi
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ main ]
|
|
||||||
schedule:
|
|
||||||
- cron: '30 3 * * 0'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'csharp' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: Build & Publish to Docker Hub
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
base_version:
|
||||||
|
description: 'Base version (latest or develop)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
info_version:
|
||||||
|
description: 'Informational version number (e.g. 0.3.7-alpha)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
tag_version:
|
||||||
|
description: 'Docker tag version (e.g. v0.3.7)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
docker_hub_username:
|
||||||
|
required: true
|
||||||
|
docker_hub_access_token:
|
||||||
|
required: true
|
||||||
|
jobs:
|
||||||
|
build_and_push:
|
||||||
|
name: Build & Publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx Base
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: builder-base
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx NVIDIA
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: builder-nvidia
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx VAAPI
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
id: builder-vaapi
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.docker_hub_username }}
|
||||||
|
password: ${{ secrets.docker_hub_access_token }}
|
||||||
|
|
||||||
|
- name: Build and push base
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.builder-base.outputs.name }}
|
||||||
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
INFO_VERSION=${{ inputs.info_version }}-docker
|
||||||
|
tags: |
|
||||||
|
jasongdove/ersatztv:${{ inputs.base_version }}
|
||||||
|
jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||||
|
|
||||||
|
- name: Build and push nvidia
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||||
|
context: .
|
||||||
|
file: ./docker/nvidia/Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
|
||||||
|
tags: |
|
||||||
|
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
|
||||||
|
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
|
||||||
|
|
||||||
|
- name: Build and push vaapi
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||||
|
context: .
|
||||||
|
file: ./docker/vaapi/Dockerfile
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
INFO_VERSION=${{ inputs.info_version }}-docker-vaapi
|
||||||
|
tags: |
|
||||||
|
jasongdove/ersatztv:${{ inputs.base_version }}-vaapi
|
||||||
|
jasongdove/ersatztv:${{ inputs.tag_version }}-vaapi
|
||||||
@@ -3,14 +3,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Deploy docs
|
name: Deploy docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master
|
- name: Checkout master
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Deploy docs
|
- name: Deploy docs
|
||||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Pull Request
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ windows-latest, ubuntu-latest, macos-latest ]
|
||||||
|
steps:
|
||||||
|
- name: Get the sources
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: 6.0.x
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-restore --verbosity normal
|
||||||
+40
-129
@@ -1,142 +1,53 @@
|
|||||||
name: Publish
|
name: Release
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ published ]
|
types: [ published ]
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
calculate_version:
|
||||||
name: Release
|
name: Calculate version information
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
kind: linux
|
|
||||||
target: linux-x64
|
|
||||||
- os: ubuntu-latest
|
|
||||||
kind: linux
|
|
||||||
target: linux-arm
|
|
||||||
- os: windows-latest
|
|
||||||
kind: windows
|
|
||||||
target: win-x64
|
|
||||||
- os: macos-latest
|
|
||||||
kind: maxOS
|
|
||||||
target: osx-x64
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Get the sources
|
- name: Get the sources
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Setup .NET Core
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: 5.0.x
|
|
||||||
|
|
||||||
- name: Clean
|
|
||||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: dotnet restore
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
# Define some variables for things we need
|
|
||||||
tag=$(git describe --tags --abbrev=0)
|
|
||||||
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
|
||||||
|
|
||||||
# Build everything
|
|
||||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
|
|
||||||
|
|
||||||
# Pack files
|
|
||||||
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
|
||||||
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
|
||||||
elif [ "${{ matrix.target }}" == "linux-arm" ]; then
|
|
||||||
cp lib/linux-arm/* "$release_name/"
|
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
|
||||||
else
|
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete output directory
|
|
||||||
rm -r "$release_name"
|
|
||||||
|
|
||||||
- name: Publish
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
files: |
|
|
||||||
ErsatzTV*.zip
|
|
||||||
ErsatzTV*.tar.gz
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
build_and_push:
|
|
||||||
name: Build & Publish to Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Extract Docker Tag
|
||||||
- name: Extract Git Tag
|
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
tag=$(git describe --tags --abbrev=0)
|
tag=$(git describe --tags --abbrev=0)
|
||||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||||
echo "DOCKER_TAG=${tag/-prealpha/}" >> $GITHUB_ENV
|
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
|
||||||
|
- name: Extract Artifacts Version
|
||||||
- name: Set up Docker Buildx Base
|
shell: bash
|
||||||
uses: docker/setup-buildx-action@v1
|
run: |
|
||||||
id: builder-base
|
tag=$(git describe --tags --abbrev=0)
|
||||||
|
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
|
||||||
- name: Set up Docker Buildx NVIDIA
|
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||||
uses: docker/setup-buildx-action@v1
|
outputs:
|
||||||
id: builder-nvidia
|
git_tag: ${{ env.GIT_TAG }}
|
||||||
|
docker_tag: ${{ env.DOCKER_TAG }}
|
||||||
- name: Set up Docker Buildx VAAPI
|
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
|
||||||
uses: docker/setup-buildx-action@v1
|
info_version: ${{ env.INFO_VERSION }}
|
||||||
id: builder-vaapi
|
build_and_upload:
|
||||||
|
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
|
||||||
- name: Login to DockerHub
|
needs: calculate_version
|
||||||
uses: docker/login-action@v1
|
with:
|
||||||
with:
|
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
info_version: ${{ needs.calculate_version.outputs.info_version }}
|
||||||
|
secrets:
|
||||||
- name: Build and push base
|
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
|
||||||
uses: docker/build-push-action@v2
|
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
|
||||||
with:
|
ac_username: ${{ secrets.AC_USERNAME }}
|
||||||
builder: ${{ steps.builder-base.outputs.name }}
|
ac_password: ${{ secrets.AC_PASSWORD }}
|
||||||
context: .
|
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: ./docker/Dockerfile
|
build_and_push:
|
||||||
push: true
|
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
|
||||||
build-args: |
|
needs: calculate_version
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker
|
with:
|
||||||
tags: |
|
base_version: latest
|
||||||
jasongdove/ersatztv:latest
|
info_version: ${{ needs.calculate_version.outputs.git_tag }}
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}
|
tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
|
||||||
|
secrets:
|
||||||
- name: Build and push nvidia
|
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
uses: docker/build-push-action@v2
|
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
with:
|
|
||||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
|
||||||
context: .
|
|
||||||
file: ./docker/nvidia/Dockerfile
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
|
|
||||||
tags: |
|
|
||||||
jasongdove/ersatztv:latest-nvidia
|
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
|
|
||||||
|
|
||||||
- name: Build and push vaapi
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
builder: ${{ steps.builder-vaapi.outputs.name }}
|
|
||||||
context: .
|
|
||||||
file: ./docker/vaapi/Dockerfile
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
|
|
||||||
tags: |
|
|
||||||
jasongdove/ersatztv:latest-vaapi
|
|
||||||
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "ErsatzTV-macOS"]
|
||||||
|
path = ErsatzTV-macOS
|
||||||
|
url = git@github.com:jasongdove/ErsatzTV-macOS.git
|
||||||
+448
-1
@@ -5,6 +5,423 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.1-alpha] - 2022-02-10
|
||||||
|
### Fixed
|
||||||
|
- Normalize smart quotes in search queries as they are unsupported by the search library
|
||||||
|
- Fix incorrect watermark time calculations caused by working ahead in `HLS Segmenter`
|
||||||
|
- Fix ui crash adding empty path to local library
|
||||||
|
- Fix ui crash loading collection editor
|
||||||
|
- Properly flag items as `File Not Found` when local library path (folder) is missing from disk
|
||||||
|
- Fix playback bug with unknown pixel format
|
||||||
|
- Fix playback of interlaced mpeg2video on NVIDIA, VAAPI
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Include `Series` category tag for all episodes in XMLTV
|
||||||
|
- Include movie, episode (show), music video (artist) genres as `category` tags in XMLTV
|
||||||
|
- Add framerate normalization to `HLS Segmenter` and `MPEG-TS` streaming modes
|
||||||
|
- Add `HLS Segmenter Initial Segment Count` setting to allow segmenter to work ahead before allowing client playback
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Intermittent watermarks will now fade in and out
|
||||||
|
- Show collection name in some playout build error messages
|
||||||
|
- Use hardware-accelerated filter for watermarks on NVIDIA
|
||||||
|
- Use hardware-accelerated deinterlace for some content on NVIDIA
|
||||||
|
|
||||||
|
## [0.4.0-alpha] - 2022-01-29
|
||||||
|
### Fixed
|
||||||
|
- Fix m3u `mode` query param to properly override streaming mode for all channels
|
||||||
|
- `segmenter` for `HLS Segmenter`
|
||||||
|
- `hls-direct` for `HLS Direct`
|
||||||
|
- `ts` for `MPEG-TS`
|
||||||
|
- `ts-legacy` for `MPEG-TS (Legacy)`
|
||||||
|
- omitting the `mode` parameter returns each channel as configured
|
||||||
|
- Link `File Not Found` health check to `Trash` page to allow deletion
|
||||||
|
- Fix `HLS Segmenter` streaming mode with multiple ffmpeg-based clients
|
||||||
|
- Jellyfin (web) and TiviMate (Android) were specifically tested
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
|
||||||
|
- Also write logs to text files in the `logs` config subfolder
|
||||||
|
- Add `added_date` to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
- Add `added_inthelast`, `added_notinthelast` search field for relative added date queries
|
||||||
|
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
|
||||||
|
|
||||||
|
## [0.3.8-alpha] - 2022-01-23
|
||||||
|
### Fixed
|
||||||
|
- Fix issue preventing some versions of ffmpeg (usually 4.4.x) from streaming MPEG-TS (Legacy) channels at all
|
||||||
|
- The issue appears to be caused by using a thread count other than `1`
|
||||||
|
- Thread count is now forced to `1` for all streaming modes other than HLS Segmenter
|
||||||
|
- Fix bug with HLS Segmenter in cultures where `.` is a group/thousands separator
|
||||||
|
- Fix search results page crashing with some media kinds
|
||||||
|
- Always use MPEG-TS or MPEG-TS (Legacy) streaming mode with HDHR (Plex)
|
||||||
|
- Other configured modes will fall back to MPEG-TS when accessed by Plex
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgrade ffmpeg from 4.4 to 5.0 in all docker images
|
||||||
|
- Upgrading from 4.4 to 5.0 is recommended for all installations
|
||||||
|
|
||||||
|
## [0.3.7-alpha] - 2022-01-17
|
||||||
|
### Fixed
|
||||||
|
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
|
||||||
|
- Fix double-click startup on mac
|
||||||
|
- Fix trakt list sync when show does not contain a year
|
||||||
|
- Properly unlock libraries when a scan is unable to be performed because ffmpeg or ffprobe have not been found
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add trash system for local libraries to maintain collection and schedule integrity through media share outages
|
||||||
|
- When items are missing from disk, they will be flagged and present in the `Media` > `Trash` page
|
||||||
|
- The trash page can be used to permanently remove missing items from the database
|
||||||
|
- When items reappear at the expected location on disk, they will be unflagged and removed from the trash
|
||||||
|
- Add basic Mac hardware acceleration using VideoToolbox
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Local libraries only: when items are missing from disk, they will be added to the trash and no longer removed from collections, etc.
|
||||||
|
- Show song thumbnail in song list
|
||||||
|
|
||||||
|
## [0.3.6-alpha] - 2022-01-10
|
||||||
|
### Fixed
|
||||||
|
- Properly index `minutes` field when adding new items during scan (vs when rebuilding index)
|
||||||
|
- Fix some nvenc edge cases where only padding is needed for normalization
|
||||||
|
- Properly overwrite environment variables for ffmpeg processes (`LIBVA_DRIVER_NAME`, `FFREPORT`)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add music video `artist` to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
|
||||||
|
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
|
||||||
|
- This improves compatibility with many clients and also improves performance at program boundaries
|
||||||
|
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
|
||||||
|
- This mode will be removed in a future release
|
||||||
|
|
||||||
|
## [0.3.5-alpha] - 2022-01-05
|
||||||
|
### Fixed
|
||||||
|
- Fix bundled ffmpeg version in base docker image (NOT nvidia or vaapi) which prevented playback since v0.3.0-alpha
|
||||||
|
- Use software decoding for mpeg4 content when VAAPI acceleration is enabled
|
||||||
|
- Fix hardware acceleration health check to recognize QSV on non-Windows platforms
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Treat `setsar` as a hardware filter, avoiding unneeded `hwdownload` and `hwupload` steps when padding isn't required
|
||||||
|
|
||||||
|
## [0.3.4-alpha] - 2021-12-21
|
||||||
|
### Fixed
|
||||||
|
- Fix other video and song scanners to include videos contained directly in top-level folders that are added to a library
|
||||||
|
- Allow saving ffmpeg troubleshooting reports on Windows
|
||||||
|
|
||||||
|
## [0.3.3-alpha] - 2021-12-12
|
||||||
|
### Fixed
|
||||||
|
- Fix bug with saving multiple blurhash versions for cover art; all cover art will be automatically rescanned
|
||||||
|
- Fix song detail margin when no cover art exists and no watermark exists
|
||||||
|
- Fix synchronizing virtual shows and seasons from Jellyfin
|
||||||
|
- Properly sort channels in M3U
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use blurhash of ErsatzTV colors instead of solid colors for default song backgrounds
|
||||||
|
- Use select control instead of autocomplete control in many places
|
||||||
|
- The autocomplete control is not intuitive to use and has focus bugs
|
||||||
|
|
||||||
|
## [0.3.2-alpha] - 2021-12-03
|
||||||
|
### Fixed
|
||||||
|
- Fix artwork upload on Windows
|
||||||
|
- Fix unicode song metadata on Windows
|
||||||
|
- Fix unicode console output on Windows
|
||||||
|
- Fix TV Show NFO metadata processing when `year` is missing
|
||||||
|
- Fix song detail outline to help legibility on white backgrounds
|
||||||
|
- Optimize song artwork scanning to prevent re-processing album artwork for each song
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use custom log database backend which should be more portable (i.e. work in osx-arm64)
|
||||||
|
- Use cover art blurhashes for song backgrounds instead of solid colors or box blur
|
||||||
|
|
||||||
|
## [0.3.1-alpha] - 2021-11-30
|
||||||
|
### Fixed
|
||||||
|
- Fix song page links in UI
|
||||||
|
- Show song artist in playout detail
|
||||||
|
- Include song artist and cover art in channel guide (xmltv)
|
||||||
|
- Use subtitles to display errors, which fixes many edge cases of unescaped characters
|
||||||
|
- Properly split song genre tags
|
||||||
|
- Properly display all songs that have an identical album and title
|
||||||
|
- Fix channel logo and watermark uploads
|
||||||
|
- Fix regression introduced with `v0.2.4-alpha` that caused some filler edge cases to crash the playout builder
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add song genres to search index
|
||||||
|
- Use embedded song cover art when sidecar cover art is unavailable
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Randomly place song cover art on left or right side of screen
|
||||||
|
- Randomly use a solid color from the cover art instead of blurred cover art for song background
|
||||||
|
- Randomly select song detail layout (large title/small artist or small artist/title/album)
|
||||||
|
|
||||||
|
## [0.3.0-alpha] - 2021-11-25
|
||||||
|
### Fixed
|
||||||
|
- Properly fix database incompatibility introduced with `v0.2.4-alpha` and partially fixed with `v0.2.5-alpha`
|
||||||
|
- The proper fix requires rebuilding all playouts, which will happen on startup after upgrading
|
||||||
|
- Fix local library locking/progress display when adding paths
|
||||||
|
- Fix grouping duration items in EPG when custom title is configured
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add *experimental* `Songs` local libraries
|
||||||
|
- Like `Other Videos`, `Songs` require no metadata or particular folder layout, and will have tags added for each containing folder
|
||||||
|
- For Example, a song at `rock/band/1990 - Album/01 whatever.flac` will have the tags `rock`, `band` and `1990 - Album`, and the title `01 whatever`
|
||||||
|
- Songs will also have basic metadata read from embedded tags (album, artist, title)
|
||||||
|
- Video will be automatically generated for songs using metadata and cover art or watermarks if available
|
||||||
|
- Add support for `.webm` video files
|
||||||
|
|
||||||
|
## [0.2.5-alpha] - 2021-11-21
|
||||||
|
### Fixed
|
||||||
|
- Include other video title in channel guide (xmltv)
|
||||||
|
- Fix bug introduced with 0.2.4-alpha that caused some playouts to build from year 0
|
||||||
|
- Use less memory matching Trakt list items
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Build osx-arm64 packages on release
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- No longer warn about local Plex guids; they aren't used for Trakt matching and can be ignored
|
||||||
|
|
||||||
|
## [0.2.4-alpha] - 2021-11-13
|
||||||
|
### Changed
|
||||||
|
- Upgrade to dotnet 6
|
||||||
|
- Use `scale_cuda` instead of `scale_npp` for NVIDIA scaling in all cases
|
||||||
|
|
||||||
|
## [0.2.3-alpha] - 2021-11-03
|
||||||
|
### Fixed
|
||||||
|
- Fix bug with audio filter in cultures where `.` is a group/thousands separator
|
||||||
|
- Fix bug where flood playout mode would only schedule one item
|
||||||
|
- This would happen if the flood was followed by another flood with a fixed start time
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support empty `.etvignore` file to instruct local movie scanner to ignore the containing folder
|
||||||
|
|
||||||
|
## [0.2.2-alpha] - 2021-10-30
|
||||||
|
### Fixed
|
||||||
|
- Fix EPG entries for Duration schedule items that play multiple items
|
||||||
|
- Fix EPG entries for Multiple schedule items that play more than one item
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add fallback filler settings to Channel and global FFmpeg Settings
|
||||||
|
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
|
||||||
|
- Channel fallback filler
|
||||||
|
- Global fallback filler
|
||||||
|
- Generated `Channel Is Offline` error message video
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow per-episode folders for local show libraries
|
||||||
|
- e.g. `Show Name\Season #\Episode #\Show Name - s#e#.mkv`
|
||||||
|
|
||||||
|
## [0.2.1-alpha] - 2021-10-24
|
||||||
|
### Fixed
|
||||||
|
- Fix saving dynamic start time on schedule items
|
||||||
|
|
||||||
|
## [0.2.0-alpha] - 2021-10-23
|
||||||
|
### Fixed
|
||||||
|
- Fix generated streams with mpeg2video
|
||||||
|
- Fix incorrect row count in playout detail table
|
||||||
|
- Fix deleting movies that have been removed from Jellyfin and Emby
|
||||||
|
- Fix bug that caused large unscheduled gaps in playouts
|
||||||
|
- This was caused by schedule items with a fixed start of midnight
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add new filler system
|
||||||
|
- `Pre-Roll Filler` plays before each media item
|
||||||
|
- `Mid-Roll Filler` plays between media item chapters
|
||||||
|
- `Post-Roll Filler` plays after each media item
|
||||||
|
- `Tail Filler` plays after all media items, until the next media item
|
||||||
|
- `Fallback Filler` loops instead of default offline image to fill any remaining gaps
|
||||||
|
- Store chapter details with media statistics; this is needed to support mid-roll filler
|
||||||
|
- This requires re-ingesting statistics for all media items the first time this version is launched
|
||||||
|
- Add switch to show/hide filler in playout detail table
|
||||||
|
- Add `minutes` field to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Change some debug log messages to info so they show by default again
|
||||||
|
- Remove tail collection options from `Duration` playout mode
|
||||||
|
- Show localized start time in schedule items tables
|
||||||
|
|
||||||
|
## [0.1.5-alpha] - 2021-10-18
|
||||||
|
### Fixed
|
||||||
|
- Fix double scheduling; this could happen if the app was shutdown during a playout build
|
||||||
|
- Fix updating Jellyfin and Emby TV seasons
|
||||||
|
- Fix updating Jellyfin and Emby artwork
|
||||||
|
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
|
||||||
|
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Include music video thumbnails in channel guide (xmltv)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Automatically find working Plex address on startup
|
||||||
|
- Automatically select schedule item in schedules that contain only one item
|
||||||
|
- Change default log level from `Debug` to `Information`
|
||||||
|
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
|
||||||
|
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
|
||||||
|
|
||||||
|
## [0.1.4-alpha] - 2021-10-14
|
||||||
|
### Fixed
|
||||||
|
- Fix error message/offline stream continuity with channels that use HLS Segmenter
|
||||||
|
- Fix removing items from search index when folders are removed from local libraries
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add `Other Video` local libraries
|
||||||
|
- Other video items require no metadata or particular folder layout, and will have tags added for each containing folder
|
||||||
|
- For Example, a video at `commercials/sd/1990/whatever.mkv` will have the tags `commercials`, `sd` and `1990`, and the title `whatever`
|
||||||
|
- Add filler `Tail Mode` option to `Duration` playout mode (in addition to existing `Offline` option)
|
||||||
|
- Filler collection will always be randomized (to fill as much time as possible)
|
||||||
|
- Filler will be hidden from channel guide, but visible in playout details in ErsatzTV
|
||||||
|
- Unfilled time will show offline image
|
||||||
|
- Add `Guide Mode` option to all schedule items
|
||||||
|
- `Normal` guide mode will show all scheduled items in the channel guide (xmltv)
|
||||||
|
- `Filler` guide mode will hide all scheduled items from the channel guide, and extend the end time for the previous item in the guide
|
||||||
|
|
||||||
|
## [0.1.3-alpha] - 2021-10-13
|
||||||
|
### Fixed
|
||||||
|
- Fix startup bug for some docker installations
|
||||||
|
|
||||||
|
## [0.1.2-alpha] - 2021-10-12
|
||||||
|
### Added
|
||||||
|
- Include more cuda (nvidia) filters in docker image
|
||||||
|
- Enable deinterlacing with nvidia using new `yadif_cuda` filter
|
||||||
|
- Add two HLS Segmenter settings: idle timeout and work-ahead limit
|
||||||
|
- `HLS Segmenter Idle Timeout` - the number of seconds to keep transcoding a channel while no requests have been received from any client
|
||||||
|
- This setting must be greater than or equal to 30 (seconds)
|
||||||
|
- `Work-Ahead HLS Segmenter Limit` - the number of segmenters (channels) that will work-ahead simultaneously (if multiple channels are being watched)
|
||||||
|
- "working ahead" means transcoding at full speed, which can take a lot of resources
|
||||||
|
- This setting must be greater than or equal to 0
|
||||||
|
- Add more watermark locations ("middle" of each side)
|
||||||
|
- Add `VAAPI Device` setting to ffmpeg profile to support installations with multiple video cards
|
||||||
|
- Add *experimental* `RadeonSI` option for `VAAPI Driver` and include mesa drivers in vaapi docker image
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Upgrade ffmpeg from 4.3 to 4.4 in all docker images
|
||||||
|
- Upgrading from 4.3 to 4.4 is recommended for all installations
|
||||||
|
- Move `VAAPI Driver` from settings page to ffmpeg profile to support installations with multiple video cards
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix some transcoding edge cases with nvidia and pixel format `yuv420p10le`
|
||||||
|
|
||||||
|
## [0.1.1-alpha] - 2021-10-10
|
||||||
|
### Added
|
||||||
|
- Add music video album to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove forced initial delay from `HLS Segmenter` streaming mode
|
||||||
|
- Upgrade nvidia docker image from 18.04 to 20.04
|
||||||
|
|
||||||
|
## [0.1.0-alpha] - 2021-10-08
|
||||||
|
### Added
|
||||||
|
- Add *experimental* streaming mode `HLS Segmenter` (most similar to `HLS Hybrid`)
|
||||||
|
- This mode is intended to increase client compatibility and reduce issues at program boundaries
|
||||||
|
- If you want the temporary transcode files to be located on a particular drive, the docker path is `/root/.local/share/etv-transcode`
|
||||||
|
- Store frame rate with media statistics; this is needed to support HLS Segmenter
|
||||||
|
- This requires re-ingesting statistics for all media items the first time this version is launched
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use latest iHD driver (21.2.3 vs 20.1.1) in vaapi docker images
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add downsampling to support transcoding 10-bit HEVC content with the h264_vaapi encoder
|
||||||
|
- Fix updating statistics when media items are replaced
|
||||||
|
- Fix XMLTV generation when scheduled episode is missing metadata
|
||||||
|
|
||||||
|
## [0.0.62-alpha] - 2021-10-05
|
||||||
|
### Added
|
||||||
|
- Support IMDB ids from Plex libraries, which may improve Trakt matching for some items
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Include Specials/Season 0 `episode-num` entry in XMLTV
|
||||||
|
- Fix some transcoding edge cases with VAAPI and pixel formats `yuv420p10le`, `yuv444p10le` and `yuv444p`
|
||||||
|
- Update Plex movie and episode paths when they are changed within Plex
|
||||||
|
- Always use `libx264` software encoder for error messages
|
||||||
|
|
||||||
|
## [0.0.61-alpha] - 2021-09-30
|
||||||
|
### Fixed
|
||||||
|
- Revert nvenc/cuda filter change from v60
|
||||||
|
|
||||||
|
## [0.0.60-alpha] - 2021-09-25
|
||||||
|
### Added
|
||||||
|
- Add Trakt list support under `Lists` > `Trakt Lists`
|
||||||
|
- Trakt lists can be added by url or by `user/list`
|
||||||
|
- To re-download a Trakt list, simply add it again (no need to delete)
|
||||||
|
- See `Logs` for unmatched item details
|
||||||
|
- Trakt lists can only be scheduled by using Smart Collections
|
||||||
|
- Add seasons to search index
|
||||||
|
- This is needed because Trakt lists can contain seasons
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix local television scanner to properly update episode metadata when NFO files have been added/changed
|
||||||
|
- Properly detect ffmpeg nvenc (cuda) support in Hardware Acceleration health check
|
||||||
|
- Fix nvenc/cuda filter for some yuv420p content
|
||||||
|
|
||||||
|
## [0.0.59-alpha] - 2021-09-18
|
||||||
|
### Added
|
||||||
|
- Add `Health Checks` table to home page to identify and surface common misconfigurations
|
||||||
|
- `FFmpeg Version` checks `ffmpeg` and `ffprobe` versions
|
||||||
|
- `FFmpeg Reports` checks whether ffmpeg troubleshooting reports are enabled since they can use a lot of disk space over time
|
||||||
|
- `Hardware Acceleration` checks whether channels that transcode are using acceleration methods that ffmpeg claims to support
|
||||||
|
- `Movie Metadata` checks whether all movies have metadata (fallback metadata counts as metadata)
|
||||||
|
- `Episode Metadata` checks whether all episodes have metadata (fallback metadata counts as metadata)
|
||||||
|
- `Zero Duration` checks whether all movies and episodes have a valid (non-zero) duration
|
||||||
|
- `VAAPI Driver` checks whether a vaapi driver preference is configured when using the vaapi docker image
|
||||||
|
- Add setting to each playout to schedule an automatic daily rebuild
|
||||||
|
- This is useful if the playout uses a smart collection with `released_onthisday`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix docker vaapi support for newer Intel platforms (Gen 8+)
|
||||||
|
- This includes a new setting to force a particular vaapi driver (`iHD` or `i965`), as some Gen 8 or 9 hardware that is supported by both drivers will perform better with one or the other
|
||||||
|
- Fix scanning and indexing local movies and episodes without NFO metadata
|
||||||
|
- Fix displaying seasons for shows with no year (in metadata or in folder name)
|
||||||
|
- Fix "direct play" in MPEG-TS mode (copy audio and video stream when `Transcode` is unchecked)
|
||||||
|
|
||||||
|
## [0.0.58-alpha] - 2021-09-15
|
||||||
|
### Added
|
||||||
|
- Add `released_notinthelast` search field for relative release date queries
|
||||||
|
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
|
||||||
|
- Add `released_onthisday` search field for historical queries
|
||||||
|
- Syntax is `released_onthisday:1` and will search for items released on this month number and day number in prior years
|
||||||
|
- Add tooltip explaining `Keep Multi-Part Episodes Together`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Properly display watermark when no other video filters (like scaling or padding) are required
|
||||||
|
- Fix building some playouts in timezones with positive offsets (like UTC+2)
|
||||||
|
- Fix `Shuffle In Order` so all collections/shows start from the earliest episode
|
||||||
|
- You may need to rebuild playouts to see this fixed behavior more quickly
|
||||||
|
|
||||||
|
## [0.0.57-alpha] - 2021-09-10
|
||||||
|
### Added
|
||||||
|
- Add `released_inthelast` search field for relative release date queries
|
||||||
|
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
|
||||||
|
- Allow adding smart collections to multi collections
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix loading artwork in Kodi
|
||||||
|
- Use fake image extension (`.jpg`) for artwork in M3U and XMLTV since Kodi detects MIME type from URL
|
||||||
|
- Enable HEAD requests for IPTV image paths since Kodi requires those
|
||||||
|
|
||||||
|
## [0.0.56-alpha] - 2021-09-10
|
||||||
|
### Added
|
||||||
|
- Add Smart Collections
|
||||||
|
- Smart Collections use search queries and can be created from the search result page
|
||||||
|
- Smart Collections are re-evaluated every time playouts are extended or rebuilt to automatically include newly-matching items
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
- Allow `Shuffle In Order` with Collections and Smart Collections
|
||||||
|
- Episodes will be grouped by show, and music videos will be grouped by artist
|
||||||
|
- All movies will be a single group (multi-collections are probably better if `Shuffle In Order` is desired for movies)
|
||||||
|
- All groups will be be ordered chronologically (custom ordering is only supported in multi-collections)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Generate XMLTV that validates successfully
|
||||||
|
- Properly order elements
|
||||||
|
- Omit channels with no programmes
|
||||||
|
- Properly identify channels using the format number.etv like `15.etv`
|
||||||
|
- Fix building playouts when multi-part episode grouping is enabled and episodes are missing metadata
|
||||||
|
- Fix incorrect total items count in `Multi Collections` table
|
||||||
|
|
||||||
## [0.0.55-alpha] - 2021-09-03
|
## [0.0.55-alpha] - 2021-09-03
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fix all local library scanners to ignore dot underscore files (`._`)
|
- Fix all local library scanners to ignore dot underscore files (`._`)
|
||||||
@@ -550,7 +967,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Initial release to facilitate testing outside of Docker.
|
- Initial release to facilitate testing outside of Docker.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...HEAD
|
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...HEAD
|
||||||
|
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
|
||||||
|
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
|
||||||
|
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
|
||||||
|
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
|
||||||
|
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
|
||||||
|
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
|
||||||
|
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
|
||||||
|
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
|
||||||
|
[0.3.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.1-alpha...v0.3.2-alpha
|
||||||
|
[0.3.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.0-alpha...v0.3.1-alpha
|
||||||
|
[0.3.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
|
||||||
|
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
|
||||||
|
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
|
||||||
|
[0.2.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.2-alpha...v0.2.3-alpha
|
||||||
|
[0.2.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.1-alpha...v0.2.2-alpha
|
||||||
|
[0.2.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.0-alpha...v0.2.1-alpha
|
||||||
|
[0.2.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...v0.2.0-alpha
|
||||||
|
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
|
||||||
|
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
|
||||||
|
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
|
||||||
|
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
|
||||||
|
[0.1.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
|
||||||
|
[0.1.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.62-alpha...v0.1.0-alpha
|
||||||
|
[0.0.62-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.61-alpha...v0.0.62-alpha
|
||||||
|
[0.0.61-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.60-alpha...v0.0.61-alpha
|
||||||
|
[0.0.60-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.59-alpha...v0.0.60-alpha
|
||||||
|
[0.0.59-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.58-alpha...v0.0.59-alpha
|
||||||
|
[0.0.58-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...v0.0.58-alpha
|
||||||
|
[0.0.57-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha
|
||||||
|
[0.0.56-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha
|
||||||
[0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
|
[0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
|
||||||
[0.0.54-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.53-alpha...v0.0.54-alpha
|
[0.0.54-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.53-alpha...v0.0.54-alpha
|
||||||
[0.0.53-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.52-alpha...v0.0.53-alpha
|
[0.0.53-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.52-alpha...v0.0.53-alpha
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
|
<RootNamespace>ErsatzTV_Windows</RootNamespace>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<ApplicationIcon>Ersatztv.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Ersatztv.ico">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Asmichi.ChildProcess" Version="0.11.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Program.cs">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ErsatzTV_Windows;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The main entry point for the application.
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
public static void Main()
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new TrayApplicationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Asmichi.ProcessManagement;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ErsatzTV_Windows;
|
||||||
|
|
||||||
|
public class TrayApplicationContext : ApplicationContext
|
||||||
|
{
|
||||||
|
private readonly NotifyIcon _trayIcon;
|
||||||
|
private readonly IChildProcess? _childProcess;
|
||||||
|
|
||||||
|
public TrayApplicationContext()
|
||||||
|
{
|
||||||
|
_trayIcon = new NotifyIcon
|
||||||
|
{
|
||||||
|
Icon = new Icon("./Ersatztv.ico"),
|
||||||
|
ContextMenuStrip = new ContextMenuStrip(),
|
||||||
|
Visible = true
|
||||||
|
};
|
||||||
|
|
||||||
|
AddMenuItem("Launch Web UI", LaunchWebUI);
|
||||||
|
AddMenuItem("Show Logs", ShowLogs);
|
||||||
|
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||||
|
AddMenuItem("Exit", Exit);
|
||||||
|
|
||||||
|
string folder = AppContext.BaseDirectory;
|
||||||
|
string exe = Path.Combine(folder, "ErsatzTV.exe");
|
||||||
|
|
||||||
|
if (File.Exists(exe))
|
||||||
|
{
|
||||||
|
var si = new ChildProcessStartInfo(exe);
|
||||||
|
_childProcess = ChildProcess.Start(si);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddMenuItem(string name, EventHandler action)
|
||||||
|
{
|
||||||
|
var item = new ToolStripMenuItem(name);
|
||||||
|
item.Click += action;
|
||||||
|
_trayIcon.ContextMenuStrip.Items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LaunchWebUI(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var process = new Process();
|
||||||
|
process.StartInfo.UseShellExecute = true;
|
||||||
|
process.StartInfo.FileName = "http://localhost:8409";
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowLogs(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(FileSystemLayout.LogsFolder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = new Process();
|
||||||
|
process.StartInfo.UseShellExecute = true;
|
||||||
|
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
|
||||||
|
process.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
_childProcess?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Exit(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Hide tray icon, otherwise it will remain shown until user mouses over it
|
||||||
|
_trayIcon.Visible = false;
|
||||||
|
Application.Exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
Submodule
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
@@ -10,5 +10,7 @@ namespace ErsatzTV.Application.Channels
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId);
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId,
|
||||||
|
int PlayoutCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -42,9 +43,10 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
(ValidateName(request), await ValidateNumber(dbContext, request),
|
(ValidateName(request), await ValidateNumber(dbContext, request),
|
||||||
await FFmpegProfileMustExist(dbContext, request),
|
await FFmpegProfileMustExist(dbContext, request),
|
||||||
ValidatePreferredLanguage(request),
|
ValidatePreferredLanguage(request),
|
||||||
await WatermarkMustExist(dbContext, request))
|
await WatermarkMustExist(dbContext, request),
|
||||||
|
await FillerPresetMustExist(dbContext, request))
|
||||||
.Apply(
|
.Apply(
|
||||||
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
|
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
|
||||||
{
|
{
|
||||||
var artwork = new List<Artwork>();
|
var artwork = new List<Artwork>();
|
||||||
if (!string.IsNullOrWhiteSpace(request.Logo))
|
if (!string.IsNullOrWhiteSpace(request.Logo))
|
||||||
@@ -74,6 +76,11 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
channel.WatermarkId = id;
|
channel.WatermarkId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (int id in fillerPresetId)
|
||||||
|
{
|
||||||
|
channel.FallbackFillerId = id;
|
||||||
|
}
|
||||||
|
|
||||||
return channel;
|
return channel;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,5 +138,25 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
.MapT(_ => Optional(createChannel.WatermarkId))
|
.MapT(_ => Optional(createChannel.WatermarkId))
|
||||||
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
CreateChannel createChannel)
|
||||||
|
{
|
||||||
|
if (createChannel.FallbackFillerId is null)
|
||||||
|
{
|
||||||
|
return Option<int>.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.FillerPresets
|
||||||
|
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
|
||||||
|
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
|
||||||
|
.Map(Optional)
|
||||||
|
.Filter(c => c > 0)
|
||||||
|
.MapT(_ => Optional(createChannel.FallbackFillerId))
|
||||||
|
.Map(
|
||||||
|
o => o.ToValidation<BaseError>(
|
||||||
|
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,6 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
string Logo,
|
string Logo,
|
||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
int? WatermarkId,
|
||||||
|
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace ErsatzTV.Application.Channels.Commands
|
|||||||
|
|
||||||
c.StreamingMode = update.StreamingMode;
|
c.StreamingMode = update.StreamingMode;
|
||||||
c.WatermarkId = update.WatermarkId;
|
c.WatermarkId = update.WatermarkId;
|
||||||
|
c.FallbackFillerId = update.FallbackFillerId;
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
return ProjectToViewModel(c);
|
return ProjectToViewModel(c);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ namespace ErsatzTV.Application.Channels
|
|||||||
GetLogo(channel),
|
GetLogo(channel),
|
||||||
channel.PreferredLanguageCode,
|
channel.PreferredLanguageCode,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.WatermarkId);
|
channel.WatermarkId,
|
||||||
|
channel.FallbackFillerId,
|
||||||
|
channel.Playouts?.Count ?? 0);
|
||||||
|
|
||||||
private static string GetLogo(Channel channel) =>
|
private static string GetLogo(Channel channel) =>
|
||||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries;
|
||||||
|
|
||||||
|
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Channels.Queries;
|
||||||
|
|
||||||
|
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly ILogger<GetChannelFramerateHandler> _logger;
|
||||||
|
|
||||||
|
public GetChannelFramerateHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
ILogger<GetChannelFramerateHandler> logger)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// TODO: expand to check everything in collection rather than what's scheduled?
|
||||||
|
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
|
||||||
|
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
List<Playout> playouts = await dbContext.Playouts
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as Movie).MediaVersions)
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as Episode).MediaVersions)
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as Song).MediaVersions)
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
|
||||||
|
.Include(p => p.Items)
|
||||||
|
.ThenInclude(pi => pi.MediaItem)
|
||||||
|
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
|
||||||
|
.Filter(p => p.Channel.Number == request.ChannelNumber)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
|
||||||
|
.Flatten()
|
||||||
|
.Map(mv => mv.RFrameRate)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
|
var distinct = frameRates.Distinct().ToList();
|
||||||
|
if (distinct.Count > 1)
|
||||||
|
{
|
||||||
|
// TODO: something more intelligent than minimum framerate?
|
||||||
|
int result = frameRates.Map(ParseFrameRate).Min();
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
|
||||||
|
request.ChannelNumber,
|
||||||
|
distinct,
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
|
||||||
|
request.ChannelNumber,
|
||||||
|
distinct[0]);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int ParseFrameRate(string frameRate)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(frameRate, out int fr))
|
||||||
|
{
|
||||||
|
string[] split = (frameRate ?? string.Empty).Split("/");
|
||||||
|
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
|
||||||
|
{
|
||||||
|
fr = (int)Math.Round(left / (double)right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fr = 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,14 +28,22 @@ namespace ErsatzTV.Application.Channels.Queries
|
|||||||
{
|
{
|
||||||
switch (mode.ToLowerInvariant())
|
switch (mode.ToLowerInvariant())
|
||||||
{
|
{
|
||||||
|
case "segmenter":
|
||||||
|
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
|
||||||
|
result.Add(channel);
|
||||||
|
break;
|
||||||
case "hls-direct":
|
case "hls-direct":
|
||||||
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
||||||
result.Add(channel);
|
result.Add(channel);
|
||||||
break;
|
break;
|
||||||
case "ts":
|
case "ts-legacy":
|
||||||
channel.StreamingMode = StreamingMode.TransportStream;
|
channel.StreamingMode = StreamingMode.TransportStream;
|
||||||
result.Add(channel);
|
result.Add(channel);
|
||||||
break;
|
break;
|
||||||
|
case "ts":
|
||||||
|
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
|
||||||
|
result.Add(channel);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result.Add(channel);
|
result.Add(channel);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||||
Optional(request.LibraryRefreshInterval)
|
Optional(request.LibraryRefreshInterval)
|
||||||
.Filter(lri => lri > 0)
|
.Where(lri => lri > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ namespace ErsatzTV.Application.Configuration.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
||||||
Optional(request.DaysToBuild)
|
Optional(request.DaysToBuild)
|
||||||
.Filter(days => days > 0)
|
.Where(days => days > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Days to build must be greater than zero")
|
.ToValidation<BaseError>("Days to build must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
|||||||
{
|
{
|
||||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
|||||||
|
|
||||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||||
{
|
{
|
||||||
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
|
||||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
||||||
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
||||||
{
|
{
|
||||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Emby.Commands
|
|||||||
{
|
{
|
||||||
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Emby media source requires an api key");
|
.ToValidation<BaseError>("Emby media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncFixer" Version="1.5.1">
|
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.10.56">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.FFmpeg;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -10,6 +11,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
bool Transcode,
|
bool Transcode,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
VaapiDriver VaapiDriver,
|
||||||
|
string VaapiDevice,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
bool NormalizeVideo,
|
bool NormalizeVideo,
|
||||||
string VideoCodec,
|
string VideoCodec,
|
||||||
@@ -21,5 +24,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
bool NormalizeLoudness,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
|
bool NormalizeAudio,
|
||||||
|
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
ThreadCount = threadCount,
|
ThreadCount = threadCount,
|
||||||
Transcode = request.Transcode,
|
Transcode = request.Transcode,
|
||||||
HardwareAcceleration = request.HardwareAcceleration,
|
HardwareAcceleration = request.HardwareAcceleration,
|
||||||
|
VaapiDriver = request.VaapiDriver,
|
||||||
|
VaapiDevice = request.VaapiDevice,
|
||||||
ResolutionId = resolutionId,
|
ResolutionId = resolutionId,
|
||||||
NormalizeVideo = request.NormalizeVideo,
|
NormalizeVideo = request.NormalizeVideo,
|
||||||
VideoCodec = request.VideoCodec,
|
VideoCodec = request.VideoCodec,
|
||||||
@@ -56,7 +58,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
NormalizeLoudness = request.NormalizeLoudness,
|
NormalizeLoudness = request.NormalizeLoudness,
|
||||||
AudioChannels = request.AudioChannels,
|
AudioChannels = request.AudioChannels,
|
||||||
AudioSampleRate = request.AudioSampleRate,
|
AudioSampleRate = request.AudioSampleRate,
|
||||||
NormalizeAudio = request.NormalizeAudio
|
NormalizeAudio = request.NormalizeAudio,
|
||||||
|
NormalizeFramerate = request.NormalizeFramerate
|
||||||
});
|
});
|
||||||
|
|
||||||
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.FFmpeg;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
bool Transcode,
|
bool Transcode,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
VaapiDriver VaapiDriver,
|
||||||
|
string VaapiDevice,
|
||||||
int ResolutionId,
|
int ResolutionId,
|
||||||
bool NormalizeVideo,
|
bool NormalizeVideo,
|
||||||
string VideoCodec,
|
string VideoCodec,
|
||||||
@@ -22,5 +25,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
bool NormalizeLoudness,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
|
bool NormalizeAudio,
|
||||||
|
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,18 +36,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
p.ThreadCount = update.ThreadCount;
|
p.ThreadCount = update.ThreadCount;
|
||||||
p.Transcode = update.Transcode;
|
p.Transcode = update.Transcode;
|
||||||
p.HardwareAcceleration = update.HardwareAcceleration;
|
p.HardwareAcceleration = update.HardwareAcceleration;
|
||||||
|
p.VaapiDriver = update.VaapiDriver;
|
||||||
|
p.VaapiDevice = update.VaapiDevice;
|
||||||
p.ResolutionId = update.ResolutionId;
|
p.ResolutionId = update.ResolutionId;
|
||||||
p.NormalizeVideo = update.NormalizeVideo;
|
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
|
||||||
p.VideoCodec = update.VideoCodec;
|
p.VideoCodec = update.VideoCodec;
|
||||||
p.VideoBitrate = update.VideoBitrate;
|
p.VideoBitrate = update.VideoBitrate;
|
||||||
p.VideoBufferSize = update.VideoBufferSize;
|
p.VideoBufferSize = update.VideoBufferSize;
|
||||||
p.AudioCodec = update.AudioCodec;
|
p.AudioCodec = update.AudioCodec;
|
||||||
p.AudioBitrate = update.AudioBitrate;
|
p.AudioBitrate = update.AudioBitrate;
|
||||||
p.AudioBufferSize = update.AudioBufferSize;
|
p.AudioBufferSize = update.AudioBufferSize;
|
||||||
p.NormalizeLoudness = update.NormalizeLoudness;
|
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;
|
||||||
p.AudioChannels = update.AudioChannels;
|
p.AudioChannels = update.AudioChannels;
|
||||||
p.AudioSampleRate = update.AudioSampleRate;
|
p.AudioSampleRate = update.AudioSampleRate;
|
||||||
p.NormalizeAudio = update.NormalizeAudio;
|
p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
|
||||||
|
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
return new UpdateFFmpegProfileResult(p.Id);
|
return new UpdateFFmpegProfileResult(p.Id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
@@ -16,16 +14,13 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
{
|
{
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
|
||||||
|
|
||||||
public UpdateFFmpegSettingsHandler(
|
public UpdateFFmpegSettingsHandler(
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem)
|
||||||
IRuntimeInfo runtimeInfo)
|
|
||||||
{
|
{
|
||||||
_configElementRepository = configElementRepository;
|
_configElementRepository = configElementRepository;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
_runtimeInfo = runtimeInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
@@ -36,8 +31,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
||||||
(await FFmpegMustExist(request), await FFprobeMustExist(request), ReportsAreNotSupportedOnWindows(request))
|
(await FFmpegMustExist(request), await FFprobeMustExist(request))
|
||||||
.Apply((_, _, _) => Unit.Default);
|
.Apply((_, _) => Unit.Default);
|
||||||
|
|
||||||
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
||||||
@@ -45,16 +40,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
||||||
|
|
||||||
private Validation<BaseError, Unit> ReportsAreNotSupportedOnWindows(UpdateFFmpegSettings request)
|
|
||||||
{
|
|
||||||
if (request.Settings.SaveReports && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return BaseError.New("FFmpeg reports are not supported on Windows");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unit.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
@@ -115,6 +100,29 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.Settings.GlobalFallbackFillerId is not null)
|
||||||
|
{
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegGlobalFallbackFillerId,
|
||||||
|
request.Settings.GlobalFallbackFillerId.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegSegmenterTimeout,
|
||||||
|
request.Settings.HlsSegmenterIdleTimeout);
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegWorkAheadSegmenters,
|
||||||
|
request.Settings.WorkAheadSegmenterLimit);
|
||||||
|
|
||||||
|
await _configElementRepository.Upsert(
|
||||||
|
ConfigElementKey.FFmpegInitialSegmentCount,
|
||||||
|
request.Settings.InitialSegmentCount);
|
||||||
|
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using ErsatzTV.Application.Resolutions;
|
using ErsatzTV.Application.Resolutions;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.FFmpeg;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles
|
namespace ErsatzTV.Application.FFmpegProfiles
|
||||||
{
|
{
|
||||||
@@ -9,6 +10,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
int ThreadCount,
|
int ThreadCount,
|
||||||
bool Transcode,
|
bool Transcode,
|
||||||
HardwareAccelerationKind HardwareAcceleration,
|
HardwareAccelerationKind HardwareAcceleration,
|
||||||
|
VaapiDriver VaapiDriver,
|
||||||
|
string VaapiDevice,
|
||||||
ResolutionViewModel Resolution,
|
ResolutionViewModel Resolution,
|
||||||
bool NormalizeVideo,
|
bool NormalizeVideo,
|
||||||
string VideoCodec,
|
string VideoCodec,
|
||||||
@@ -20,5 +23,6 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
bool NormalizeLoudness,
|
bool NormalizeLoudness,
|
||||||
int AudioChannels,
|
int AudioChannels,
|
||||||
int AudioSampleRate,
|
int AudioSampleRate,
|
||||||
bool NormalizeAudio);
|
bool NormalizeAudio,
|
||||||
|
bool NormalizeFramerate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,9 @@
|
|||||||
public string PreferredLanguageCode { get; set; }
|
public string PreferredLanguageCode { get; set; }
|
||||||
public bool SaveReports { get; set; }
|
public bool SaveReports { get; set; }
|
||||||
public int? GlobalWatermarkId { get; set; }
|
public int? GlobalWatermarkId { get; set; }
|
||||||
|
public int? GlobalFallbackFillerId { get; set; }
|
||||||
|
public int HlsSegmenterIdleTimeout { get; set; }
|
||||||
|
public int WorkAheadSegmenterLimit { get; set; }
|
||||||
|
public int InitialSegmentCount { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
profile.ThreadCount,
|
profile.ThreadCount,
|
||||||
profile.Transcode,
|
profile.Transcode,
|
||||||
profile.HardwareAcceleration,
|
profile.HardwareAcceleration,
|
||||||
|
profile.VaapiDriver,
|
||||||
|
profile.VaapiDevice,
|
||||||
Project(profile.Resolution),
|
Project(profile.Resolution),
|
||||||
profile.NormalizeVideo,
|
profile.NormalizeVideo,
|
||||||
profile.VideoCodec,
|
profile.VideoCodec,
|
||||||
@@ -23,7 +25,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
|
|||||||
profile.NormalizeLoudness,
|
profile.NormalizeLoudness,
|
||||||
profile.AudioChannels,
|
profile.AudioChannels,
|
||||||
profile.AudioSampleRate,
|
profile.AudioSampleRate,
|
||||||
profile.NormalizeAudio);
|
profile.NormalizeAudio,
|
||||||
|
profile.NormalizeVideo && profile.NormalizeFramerate);
|
||||||
|
|
||||||
private static ResolutionViewModel Project(Resolution resolution) =>
|
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
|
||||||
Option<int> watermark =
|
Option<int> watermark =
|
||||||
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
|
||||||
|
Option<int> fallbackFiller =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
|
||||||
|
Option<int> hlsSegmenterIdleTimeout =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
|
||||||
|
Option<int> workAheadSegmenterLimit =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
|
||||||
|
Option<int> initialSegmentCount =
|
||||||
|
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
|
||||||
|
|
||||||
var result = new FFmpegSettingsViewModel
|
var result = new FFmpegSettingsViewModel
|
||||||
{
|
{
|
||||||
@@ -36,6 +44,9 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
|
||||||
SaveReports = await saveReports.IfNoneAsync(false),
|
SaveReports = await saveReports.IfNoneAsync(false),
|
||||||
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
|
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
|
||||||
|
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
|
||||||
|
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
|
||||||
|
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (int watermarkId in watermark)
|
foreach (int watermarkId in watermark)
|
||||||
@@ -43,6 +54,11 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
|
|||||||
result.GlobalWatermarkId = watermarkId;
|
result.GlobalWatermarkId = watermarkId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (int fallbackFillerId in fallbackFiller)
|
||||||
|
{
|
||||||
|
result.GlobalFallbackFillerId = fallbackFillerId;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record CreateFillerPreset(
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId
|
||||||
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public CreateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(CreateFillerPreset request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
var fillerPreset = new FillerPreset
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
FillerKind = request.FillerKind,
|
||||||
|
FillerMode = request.FillerMode,
|
||||||
|
Duration = request.Duration,
|
||||||
|
Count = request.Count,
|
||||||
|
PadToNearestMinute = request.PadToNearestMinute,
|
||||||
|
CollectionType = request.CollectionType,
|
||||||
|
CollectionId = request.CollectionId,
|
||||||
|
MediaItemId = request.MediaItemId,
|
||||||
|
MultiCollectionId = request.MultiCollectionId,
|
||||||
|
SmartCollectionId = request.SmartCollectionId
|
||||||
|
};
|
||||||
|
|
||||||
|
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record DeleteFillerPreset(int FillerPresetId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class DeleteFillerPresetHandler : IRequestHandler<DeleteFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public DeleteFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
DeleteFillerPreset request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
|
return await validation.Apply(ps => DoDeletion(dbContext, ps));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Unit> DoDeletion(TvContext dbContext, FillerPreset fillerPreset)
|
||||||
|
{
|
||||||
|
dbContext.FillerPresets.Remove(fillerPreset);
|
||||||
|
return dbContext.SaveChangesAsync().ToUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
DeleteFillerPreset request) =>
|
||||||
|
dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(fp => fp.Id, ps => ps.Id == request.FillerPresetId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>($"FillerPreset {request.FillerPresetId} does not exist."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public record UpdateFillerPreset(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId
|
||||||
|
) : IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Commands
|
||||||
|
{
|
||||||
|
public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public UpdateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(UpdateFillerPreset request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
Validation<BaseError, FillerPreset> validation = await FillerPresetMustExist(dbContext, request);
|
||||||
|
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyUpdateRequest(
|
||||||
|
TvContext dbContext,
|
||||||
|
FillerPreset existing,
|
||||||
|
UpdateFillerPreset request)
|
||||||
|
{
|
||||||
|
existing.Name = request.Name;
|
||||||
|
existing.FillerKind = request.FillerKind;
|
||||||
|
existing.FillerMode = request.FillerMode;
|
||||||
|
existing.Duration = request.Duration;
|
||||||
|
existing.Count = request.Count;
|
||||||
|
existing.PadToNearestMinute = request.PadToNearestMinute;
|
||||||
|
existing.CollectionType = request.CollectionType;
|
||||||
|
existing.CollectionId = request.CollectionId;
|
||||||
|
existing.MediaItemId = request.MediaItemId;
|
||||||
|
existing.MultiCollectionId = request.MultiCollectionId;
|
||||||
|
existing.SmartCollectionId = request.SmartCollectionId;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, FillerPreset>> FillerPresetMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
UpdateFillerPreset request) =>
|
||||||
|
dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(ps => ps.Id, ps => ps.Id == request.Id)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("FillerPreset does not exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
public record FillerPresetViewModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
FillerKind FillerKind,
|
||||||
|
FillerMode FillerMode,
|
||||||
|
TimeSpan? Duration,
|
||||||
|
int? Count,
|
||||||
|
int? PadToNearestMinute,
|
||||||
|
ProgramScheduleItemCollectionType CollectionType,
|
||||||
|
int? CollectionId,
|
||||||
|
int? MediaItemId,
|
||||||
|
int? MultiCollectionId,
|
||||||
|
int? SmartCollectionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ErsatzTV.Core.Domain.Filler;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
internal static class Mapper
|
||||||
|
{
|
||||||
|
internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) =>
|
||||||
|
new(
|
||||||
|
fillerPreset.Id,
|
||||||
|
fillerPreset.Name,
|
||||||
|
fillerPreset.FillerKind,
|
||||||
|
fillerPreset.FillerMode,
|
||||||
|
fillerPreset.Duration,
|
||||||
|
fillerPreset.Count,
|
||||||
|
fillerPreset.PadToNearestMinute,
|
||||||
|
fillerPreset.CollectionType,
|
||||||
|
fillerPreset.CollectionId,
|
||||||
|
fillerPreset.MediaItemId,
|
||||||
|
fillerPreset.MultiCollectionId,
|
||||||
|
fillerPreset.SmartCollectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler
|
||||||
|
{
|
||||||
|
public record PagedFillerPresetsViewModel(int TotalCount, List<FillerPresetViewModel> Page);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetAllFillerPresets : IRequest<List<FillerPresetViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using LanguageExt;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetAllFillerPresetsHandler : IRequestHandler<GetAllFillerPresets, List<FillerPresetViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetAllFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<List<FillerPresetViewModel>> Handle(
|
||||||
|
GetAllFillerPresets request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
return await dbContext.FillerPresets.ToListAsync(cancellationToken)
|
||||||
|
.Map(presets => presets.Map(ProjectToViewModel).ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetFillerPresetById(int Id) : IRequest<Option<FillerPresetViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetFillerPresetByIdHandler : IRequestHandler<GetFillerPresetById, Option<FillerPresetViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetFillerPresetByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Option<FillerPresetViewModel>> Handle(
|
||||||
|
GetFillerPresetById request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
return await dbContext.FillerPresets
|
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||||
|
.MapT(ProjectToViewModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public record GetPagedFillerPresets(int PageNum, int PageSize) : IRequest<PagedFillerPresetsViewModel>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using LanguageExt;
|
||||||
|
using static ErsatzTV.Application.Filler.Mapper;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Filler.Queries
|
||||||
|
{
|
||||||
|
public class GetPagedFillerPresetsHandler : IRequestHandler<GetPagedFillerPresets, PagedFillerPresetsViewModel>
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _dbConnection;
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public GetPagedFillerPresetsHandler(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_dbConnection = dbConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PagedFillerPresetsViewModel> Handle(
|
||||||
|
GetPagedFillerPresets request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int count = await _dbConnection.QuerySingleAsync<int>(@"SELECT COUNT (*) FROM FillerPreset");
|
||||||
|
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
List<FillerPresetViewModel> page = await dbContext.FillerPresets.FromSqlRaw(
|
||||||
|
@"SELECT * FROM FillerPreset
|
||||||
|
ORDER BY Name
|
||||||
|
COLLATE NOCASE
|
||||||
|
LIMIT {0} OFFSET {1}",
|
||||||
|
request.PageSize,
|
||||||
|
request.PageNum * request.PageSize)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||||
|
|
||||||
|
return new PagedFillerPresetsViewModel(count, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace ErsatzTV.Application.HDHR.Commands
|
|||||||
|
|
||||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||||
Optional(request.TunerCount)
|
Optional(request.TunerCount)
|
||||||
.Filter(tc => tc > 0)
|
.Where(tc => tc > 0)
|
||||||
.Map(_ => Unit.Default)
|
.Map(_ => Unit.Default)
|
||||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||||
.AsTask();
|
.AsTask();
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health.Queries
|
||||||
|
{
|
||||||
|
public record GetAllHealthCheckResults : IRequest<List<HealthCheckResult>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Health.Queries
|
||||||
|
{
|
||||||
|
public class GetAllHealthCheckResultsHandler : IRequestHandler<GetAllHealthCheckResults, List<HealthCheckResult>>
|
||||||
|
{
|
||||||
|
private readonly IHealthCheckService _healthCheckService;
|
||||||
|
|
||||||
|
public GetAllHealthCheckResultsHandler(IHealthCheckService healthCheckService) =>
|
||||||
|
_healthCheckService = healthCheckService;
|
||||||
|
|
||||||
|
public async Task<List<HealthCheckResult>> Handle(
|
||||||
|
GetAllHealthCheckResults request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<HealthCheckResult> results = await _healthCheckService.PerformHealthChecks();
|
||||||
|
return results.Filter(r => r.Status != HealthCheckStatus.NotApplicable).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ErsatzTV.Application
|
||||||
|
{
|
||||||
|
public interface IFFmpegWorkerRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using ErsatzTV.Core;
|
using System.IO;
|
||||||
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
@@ -6,5 +7,5 @@ using MediatR;
|
|||||||
namespace ErsatzTV.Application.Images.Commands
|
namespace ErsatzTV.Application.Images.Commands
|
||||||
{
|
{
|
||||||
// ReSharper disable once SuggestBaseTypeForParameter
|
// ReSharper disable once SuggestBaseTypeForParameter
|
||||||
public record SaveArtworkToDisk(byte[] Buffer, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
|
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ namespace ErsatzTV.Application.Images.Commands
|
|||||||
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
|
public SaveArtworkToDiskHandler(IImageCache imageCache) => _imageCache = imageCache;
|
||||||
|
|
||||||
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) =>
|
public Task<Either<BaseError, string>> Handle(SaveArtworkToDisk request, CancellationToken cancellationToken) =>
|
||||||
_imageCache.SaveArtworkToCache(request.Buffer, request.ArtworkKind);
|
_imageCache.SaveArtworkToCache(request.Stream, request.ArtworkKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
|
|
||||||
private async Task<Unit> Synchronize(RequestParameters parameters)
|
private async Task<Unit> Synchronize(RequestParameters parameters)
|
||||||
{
|
{
|
||||||
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
|
||||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
|
||||||
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
|
||||||
{
|
{
|
||||||
@@ -142,7 +142,7 @@ namespace ErsatzTV.Application.Jellyfin.Commands
|
|||||||
{
|
{
|
||||||
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
JellyfinSecrets secrets = await _jellyfinSecretStore.ReadSecrets();
|
||||||
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
|
||||||
.Filter(match => match)
|
.Where(match => match)
|
||||||
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
|
||||||
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
.ToValidation<BaseError>("Jellyfin media source requires an api key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -37,7 +36,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
CreateLocalLibrary request,
|
CreateLocalLibrary request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
|
Validation<BaseError, LocalLibrary> validation = await Validate(dbContext, request);
|
||||||
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
|
return await validation.Apply(localLibrary => PersistLocalLibrary(dbContext, localLibrary));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.Map(list => list.Map(c => c.Path).ToList());
|
.Map(list => list.Map(c => c.Path).ToList());
|
||||||
|
|
||||||
return Optional(request.Path)
|
return Optional(request.Path)
|
||||||
.Filter(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
.Where(folder => allPaths.ForAll(f => !AreSubPaths(f, folder)))
|
||||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
.Map(list => list.SelectMany(ll => ll.Paths).Map(lp => lp.Path).ToList());
|
||||||
|
|
||||||
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
return Optional(localLibrary.Paths.Count(folder => allPaths.Any(f => AreSubPaths(f, folder.Path))))
|
||||||
.Filter(length => length == 0)
|
.Where(length => length == 0)
|
||||||
.Map(_ => localLibrary)
|
.Map(_ => localLibrary)
|
||||||
.ToValidation<BaseError>("Path must not belong to another library path");
|
.ToValidation<BaseError>("Path must not belong to another library path");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -8,6 +9,7 @@ using ErsatzTV.Application.MediaSources.Commands;
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Locking;
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -22,15 +24,18 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
{
|
{
|
||||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||||
private readonly IEntityLocker _entityLocker;
|
private readonly IEntityLocker _entityLocker;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
public UpdateLocalLibraryHandler(
|
public UpdateLocalLibraryHandler(
|
||||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||||
IEntityLocker entityLocker,
|
IEntityLocker entityLocker,
|
||||||
|
ISearchIndex searchIndex,
|
||||||
IDbContextFactory<TvContext> dbContextFactory)
|
IDbContextFactory<TvContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_workerChannel = workerChannel;
|
_workerChannel = workerChannel;
|
||||||
_entityLocker = entityLocker;
|
_entityLocker = entityLocker;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +43,7 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
UpdateLocalLibrary request,
|
UpdateLocalLibrary request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||||
return await validation.Apply(parameters => UpdateLocalLibrary(dbContext, parameters));
|
return await validation.Apply(parameters => UpdateLocalLibrary(dbContext, parameters));
|
||||||
}
|
}
|
||||||
@@ -48,7 +53,6 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
(LocalLibrary existing, LocalLibrary incoming) = parameters;
|
(LocalLibrary existing, LocalLibrary incoming) = parameters;
|
||||||
existing.Name = incoming.Name;
|
existing.Name = incoming.Name;
|
||||||
|
|
||||||
// toAdd
|
|
||||||
var toAdd = incoming.Paths
|
var toAdd = incoming.Paths
|
||||||
.Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path)))
|
.Filter(p => existing.Paths.All(ep => NormalizePath(ep.Path) != NormalizePath(p.Path)))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -56,12 +60,23 @@ namespace ErsatzTV.Application.Libraries.Commands
|
|||||||
.Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path)))
|
.Filter(ep => incoming.Paths.All(p => NormalizePath(p.Path) != NormalizePath(ep.Path)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var toRemoveIds = toRemove.Map(lp => lp.Id).ToList();
|
||||||
|
|
||||||
|
List<int> itemsToRemove = await dbContext.MediaItems
|
||||||
|
.Filter(mi => toRemoveIds.Contains(mi.LibraryPathId))
|
||||||
|
.Map(mi => mi.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
existing.Paths.RemoveAll(toRemove.Contains);
|
existing.Paths.RemoveAll(toRemove.Contains);
|
||||||
existing.Paths.AddRange(toAdd);
|
existing.Paths.AddRange(toAdd);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
if (await dbContext.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
await _searchIndex.RemoveItems(itemsToRemove);
|
||||||
|
_searchIndex.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
if (toAdd.Count > 0 || toRemove.Count > 0 && _entityLocker.LockLibrary(existing.Id))
|
if ((toAdd.Count > 0 || toRemove.Count > 0) && _entityLocker.LockLibrary(existing.Id))
|
||||||
{
|
{
|
||||||
await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
|
await _workerChannel.WriteAsync(new ForceScanLocalLibrary(existing.Id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands;
|
||||||
|
|
||||||
|
public record DeleteItemsFromDatabase(List<int> MediaItemIds) : IRequest<Either<BaseError, Unit>>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler<DeleteItemsFromDatabase, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public DeleteItemsFromDatabaseHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
DeleteItemsFromDatabase request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds);
|
||||||
|
if (deleteResult.IsRight)
|
||||||
|
{
|
||||||
|
await _searchIndex.RemoveItems(request.MediaItemIds);
|
||||||
|
_searchIndex.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) :
|
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) :
|
||||||
MediaCardViewModel(Id, Name, Role, Name, Thumb);
|
MediaCardViewModel(Id, Name, Role, Name, Thumb, State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ArtistCardViewModel
|
public record ArtistCardViewModel
|
||||||
(int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
ArtistId,
|
int ArtistId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster);
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
ArtistId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||||
List<ArtistCardViewModel> ArtistCards,
|
List<ArtistCardViewModel> ArtistCards,
|
||||||
List<MusicVideoCardViewModel> MusicVideoCards)
|
List<MusicVideoCardViewModel> MusicVideoCards,
|
||||||
|
List<OtherVideoCardViewModel> OtherVideoCards,
|
||||||
|
List<SongCardViewModel> SongCards)
|
||||||
{
|
{
|
||||||
public bool UseCustomPlaybackOrder { get; set; }
|
public bool UseCustomPlaybackOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System;
|
using System.Linq;
|
||||||
using System.Linq;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using static LanguageExt.Prelude;
|
using static LanguageExt.Prelude;
|
||||||
@@ -19,7 +20,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
showMetadata.Title,
|
showMetadata.Title,
|
||||||
showMetadata.Year?.ToString(),
|
showMetadata.Year?.ToString(),
|
||||||
showMetadata.SortTitle,
|
showMetadata.SortTitle,
|
||||||
GetPoster(showMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(showMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
showMetadata.Show.State);
|
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
Season season,
|
Season season,
|
||||||
@@ -34,7 +36,29 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
GetSeasonName(season.SeasonNumber),
|
GetSeasonName(season.SeasonNumber),
|
||||||
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
||||||
.IfNone(string.Empty),
|
.IfNone(string.Empty),
|
||||||
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
|
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(),
|
||||||
|
season.State);
|
||||||
|
|
||||||
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
|
SeasonMetadata seasonMetadata,
|
||||||
|
Option<JellyfinMediaSource> maybeJellyfin,
|
||||||
|
Option<EmbyMediaSource> maybeEmby)
|
||||||
|
{
|
||||||
|
string showTitle = seasonMetadata.Season.Show.ShowMetadata.HeadOrNone().Match(
|
||||||
|
m => m.Title ?? string.Empty,
|
||||||
|
() => string.Empty);
|
||||||
|
|
||||||
|
return new TelevisionSeasonCardViewModel(
|
||||||
|
showTitle,
|
||||||
|
seasonMetadata.SeasonId,
|
||||||
|
seasonMetadata.Season.SeasonNumber,
|
||||||
|
showTitle,
|
||||||
|
GetSeasonName(seasonMetadata.Season.SeasonNumber),
|
||||||
|
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
|
||||||
|
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(),
|
||||||
|
seasonMetadata.Season.State);
|
||||||
|
}
|
||||||
|
|
||||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
||||||
EpisodeMetadata episodeMetadata,
|
EpisodeMetadata episodeMetadata,
|
||||||
@@ -43,7 +67,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
bool isSearchResult) =>
|
bool isSearchResult) =>
|
||||||
new(
|
new(
|
||||||
episodeMetadata.EpisodeId,
|
episodeMetadata.EpisodeId,
|
||||||
episodeMetadata.ReleaseDate ?? DateTime.MinValue,
|
episodeMetadata.ReleaseDate ?? SystemTime.MinValueUtc,
|
||||||
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match(
|
episodeMetadata.Episode.Season.Show.ShowMetadata.HeadOrNone().Match(
|
||||||
m => m.Title ?? string.Empty,
|
m => m.Title ?? string.Empty,
|
||||||
() => string.Empty),
|
() => string.Empty),
|
||||||
@@ -60,7 +84,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
||||||
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
||||||
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
||||||
episodeMetadata.Writers.Map(w => w.Name).ToList());
|
episodeMetadata.Writers.Map(w => w.Name).ToList(),
|
||||||
|
episodeMetadata.Episode.State,
|
||||||
|
episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(
|
internal static MovieCardViewModel ProjectToViewModel(
|
||||||
MovieMetadata movieMetadata,
|
MovieMetadata movieMetadata,
|
||||||
@@ -71,7 +97,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
movieMetadata.Title,
|
movieMetadata.Title,
|
||||||
movieMetadata.Year?.ToString(),
|
movieMetadata.Year?.ToString(),
|
||||||
movieMetadata.SortTitle,
|
movieMetadata.SortTitle,
|
||||||
GetPoster(movieMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(movieMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
movieMetadata.Movie.State);
|
||||||
|
|
||||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||||
new(
|
new(
|
||||||
@@ -80,7 +107,30 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title,
|
musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title,
|
||||||
musicVideoMetadata.SortTitle,
|
musicVideoMetadata.SortTitle,
|
||||||
musicVideoMetadata.Plot,
|
musicVideoMetadata.Plot,
|
||||||
GetThumbnail(musicVideoMetadata, None, None));
|
musicVideoMetadata.Album,
|
||||||
|
GetThumbnail(musicVideoMetadata, None, None),
|
||||||
|
musicVideoMetadata.MusicVideo.State,
|
||||||
|
musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
|
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
||||||
|
new(
|
||||||
|
otherVideoMetadata.OtherVideoId,
|
||||||
|
otherVideoMetadata.Title,
|
||||||
|
otherVideoMetadata.OriginalTitle,
|
||||||
|
otherVideoMetadata.SortTitle,
|
||||||
|
otherVideoMetadata.OtherVideo.State);
|
||||||
|
|
||||||
|
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata)
|
||||||
|
{
|
||||||
|
string album = string.IsNullOrWhiteSpace(songMetadata.Album) ? "" : $" - {songMetadata.Album}";
|
||||||
|
return new SongCardViewModel(
|
||||||
|
songMetadata.SongId,
|
||||||
|
songMetadata.Title,
|
||||||
|
songMetadata.Artist + album,
|
||||||
|
songMetadata.SortTitle,
|
||||||
|
GetThumbnail(songMetadata, None, None),
|
||||||
|
songMetadata.Song.State);
|
||||||
|
}
|
||||||
|
|
||||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||||
new(
|
new(
|
||||||
@@ -88,7 +138,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
artistMetadata.Title,
|
artistMetadata.Title,
|
||||||
artistMetadata.Disambiguation,
|
artistMetadata.Disambiguation,
|
||||||
artistMetadata.SortTitle,
|
artistMetadata.SortTitle,
|
||||||
GetThumbnail(artistMetadata, None, None));
|
GetThumbnail(artistMetadata, None, None),
|
||||||
|
artistMetadata.Artist.State);
|
||||||
|
|
||||||
internal static CollectionCardResultsViewModel
|
internal static CollectionCardResultsViewModel
|
||||||
ProjectToViewModel(
|
ProjectToViewModel(
|
||||||
@@ -112,6 +163,10 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
.ToList(),
|
.ToList(),
|
||||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||||
|
.ToList(),
|
||||||
|
collection.MediaItems.OfType<OtherVideo>().Map(ov => ProjectToViewModel(ov.OtherVideoMetadata.Head()))
|
||||||
|
.ToList(),
|
||||||
|
collection.MediaItems.OfType<Song>().Map(s => ProjectToViewModel(s.SongMetadata.Head()))
|
||||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||||
|
|
||||||
internal static ActorCardViewModel ProjectToViewModel(
|
internal static ActorCardViewModel ProjectToViewModel(
|
||||||
@@ -132,7 +187,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
.SetQueryParam("maxHeight", 440);
|
.SetQueryParam("maxHeight", 440);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork);
|
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster);
|
public record MediaCardViewModel(
|
||||||
|
int MediaItemId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MovieCardViewModel
|
public record MovieCardViewModel
|
||||||
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
MovieId,
|
int MovieId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster)
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
MovieId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MusicVideoCardViewModel
|
public record MusicVideoCardViewModel
|
||||||
(
|
(
|
||||||
@@ -7,12 +9,16 @@
|
|||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Poster) : MediaCardViewModel(
|
string Album,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
MusicVideoId,
|
MusicVideoId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record OtherVideoCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<OtherVideoCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record OtherVideoCardViewModel
|
||||||
|
(
|
||||||
|
int OtherVideoId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
OtherVideoId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
null,
|
||||||
|
State)
|
||||||
|
{
|
||||||
|
public int CustomIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
GetCollectionCards request,
|
GetCollectionCards request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||||
.Map(list => list.HeadOrNone());
|
.Map(list => list.HeadOrNone());
|
||||||
@@ -47,6 +47,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
.ThenInclude(i => (i as Movie).MovieMetadata)
|
.ThenInclude(i => (i as Movie).MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Movie).MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
.ThenInclude(i => (i as Artist).ArtistMetadata)
|
||||||
.ThenInclude(mvm => mvm.Artwork)
|
.ThenInclude(mvm => mvm.Artwork)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
@@ -56,6 +59,9 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
.ThenInclude(i => (i as MusicVideo).Artist)
|
.ThenInclude(i => (i as MusicVideo).Artist)
|
||||||
.ThenInclude(a => a.ArtistMetadata)
|
.ThenInclude(a => a.ArtistMetadata)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as MusicVideo).MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => (i as Show).ShowMetadata)
|
.ThenInclude(i => (i as Show).ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
@@ -80,6 +86,21 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
.Include(c => c.MediaItems)
|
.Include(c => c.MediaItems)
|
||||||
.ThenInclude(i => (i as Episode).Season)
|
.ThenInclude(i => (i as Episode).Season)
|
||||||
.ThenInclude(s => s.SeasonMetadata)
|
.ThenInclude(s => s.SeasonMetadata)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Episode).MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
|
||||||
|
.ThenInclude(ovm => ovm.Artwork)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as OtherVideo).MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Song).SongMetadata)
|
||||||
|
.ThenInclude(ovm => ovm.Artwork)
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.ThenInclude(i => (i as Song).MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
|
||||||
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
.Map(c => c.ToEither(BaseError.New("Unable to load collection")))
|
||||||
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
.MapT(c => ProjectToViewModel(c, maybeJellyfin, maybeEmby));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
|||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards.Queries
|
namespace ErsatzTV.Application.MediaCards.Queries
|
||||||
{
|
{
|
||||||
@@ -41,7 +42,7 @@ namespace ErsatzTV.Application.MediaCards.Queries
|
|||||||
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
|
.GetPagedSeasons(request.TelevisionShowId, request.PageNumber, request.PageSize)
|
||||||
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
|
.Map(list => list.Map(s => ProjectToViewModel(s, maybeJellyfin, maybeEmby)).ToList());
|
||||||
|
|
||||||
return new TelevisionSeasonCardResultsViewModel(count, results);
|
return new TelevisionSeasonCardResultsViewModel(count, results, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record SongCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<SongCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
|
{
|
||||||
|
public record SongCardViewModel
|
||||||
|
(
|
||||||
|
int SongId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
SongId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State)
|
||||||
|
{
|
||||||
|
public int CustomIndex { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
@@ -17,10 +18,13 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string Plot,
|
string Plot,
|
||||||
string Poster,
|
string Poster,
|
||||||
List<string> Directors,
|
List<string> Directors,
|
||||||
List<string> Writers) : MediaCardViewModel(
|
List<string> Writers,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionSeasonCardResultsViewModel(int Count, List<TelevisionSeasonCardViewModel> Cards);
|
public record TelevisionSeasonCardResultsViewModel(
|
||||||
|
int Count,
|
||||||
|
List<TelevisionSeasonCardViewModel> Cards,
|
||||||
|
Option<SearchPageMap> PageMap);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionSeasonCardViewModel
|
public record TelevisionSeasonCardViewModel
|
||||||
(
|
(
|
||||||
@@ -9,10 +11,12 @@
|
|||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Poster,
|
string Poster,
|
||||||
string Placeholder) : MediaCardViewModel(
|
string Placeholder,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
TelevisionSeasonId,
|
TelevisionSeasonId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionShowCardViewModel
|
public record TelevisionShowCardViewModel
|
||||||
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
TelevisionShowId,
|
int TelevisionShowId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster);
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
TelevisionShowId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
int CollectionId,
|
int CollectionId,
|
||||||
List<int> MovieIds,
|
List<int> MovieIds,
|
||||||
List<int> ShowIds,
|
List<int> ShowIds,
|
||||||
|
List<int> SeasonIds,
|
||||||
List<int> EpisodeIds,
|
List<int> EpisodeIds,
|
||||||
List<int> ArtistIds,
|
List<int> ArtistIds,
|
||||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
List<int> MusicVideoIds,
|
||||||
|
List<int> OtherVideoIds,
|
||||||
|
List<int> SongIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,9 +52,12 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
{
|
{
|
||||||
var allItems = request.MovieIds
|
var allItems = request.MovieIds
|
||||||
.Append(request.ShowIds)
|
.Append(request.ShowIds)
|
||||||
|
.Append(request.SeasonIds)
|
||||||
.Append(request.EpisodeIds)
|
.Append(request.EpisodeIds)
|
||||||
.Append(request.ArtistIds)
|
.Append(request.ArtistIds)
|
||||||
.Append(request.MusicVideoIds)
|
.Append(request.MusicVideoIds)
|
||||||
|
.Append(request.OtherVideoIds)
|
||||||
|
.Append(request.SongIds)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
var toAddIds = allItems.Where(item => collection.MediaItems.All(mi => mi.Id != item)).ToList();
|
||||||
@@ -77,12 +80,15 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Collection>> Validate(TvContext dbContext, AddItemsToCollection request) =>
|
private async Task<Validation<BaseError, Collection>> Validate(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddItemsToCollection request) =>
|
||||||
(await CollectionMustExist(dbContext, request),
|
(await CollectionMustExist(dbContext, request),
|
||||||
await ValidateMovies(request),
|
await ValidateMovies(request),
|
||||||
await ValidateShows(request),
|
await ValidateShows(request),
|
||||||
|
await ValidateSeasons(request),
|
||||||
await ValidateEpisodes(request))
|
await ValidateEpisodes(request))
|
||||||
.Apply((collection, _, _, _) => collection);
|
.Apply((collection, _, _, _, _) => collection);
|
||||||
|
|
||||||
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
TvContext dbContext,
|
TvContext dbContext,
|
||||||
@@ -106,6 +112,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.MapT(_ => Unit.Default)
|
.MapT(_ => Unit.Default)
|
||||||
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
.Map(v => v.ToValidation<BaseError>("Show does not exist"));
|
||||||
|
|
||||||
|
private Task<Validation<BaseError, Unit>> ValidateSeasons(AddItemsToCollection request) =>
|
||||||
|
_televisionRepository.AllSeasonsExist(request.SeasonIds)
|
||||||
|
.Map(Optional)
|
||||||
|
.Filter(v => v == true)
|
||||||
|
.MapT(_ => Unit.Default)
|
||||||
|
.Map(v => v.ToValidation<BaseError>("Season does not exist"));
|
||||||
|
|
||||||
private Task<Validation<BaseError, Unit>> ValidateEpisodes(AddItemsToCollection request) =>
|
private Task<Validation<BaseError, Unit>> ValidateEpisodes(AddItemsToCollection request) =>
|
||||||
_televisionRepository.AllEpisodesExist(request.EpisodeIds)
|
_televisionRepository.AllEpisodesExist(request.EpisodeIds)
|
||||||
.Map(Optional)
|
.Map(Optional)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record AddOtherVideoToCollection
|
||||||
|
(int CollectionId, int OtherVideoId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class AddOtherVideoToCollectionHandler :
|
||||||
|
MediatR.IRequestHandler<AddOtherVideoToCollection, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public AddOtherVideoToCollectionHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
AddOtherVideoToCollection request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||||
|
return await validation.Apply(parameters => ApplyAddOtherVideoRequest(dbContext, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyAddOtherVideoRequest(TvContext dbContext, Parameters parameters)
|
||||||
|
{
|
||||||
|
parameters.Collection.MediaItems.Add(parameters.OtherVideo);
|
||||||
|
if (await dbContext.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository
|
||||||
|
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddOtherVideoToCollection request) =>
|
||||||
|
(await CollectionMustExist(dbContext, request), await ValidateOtherVideo(dbContext, request))
|
||||||
|
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddOtherVideoToCollection request) =>
|
||||||
|
dbContext.Collections
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, OtherVideo>> ValidateOtherVideo(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddOtherVideoToCollection request) =>
|
||||||
|
dbContext.OtherVideos
|
||||||
|
.SelectOneAsync(m => m.Id, e => e.Id == request.OtherVideoId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("OtherVideo does not exist"));
|
||||||
|
|
||||||
|
private record Parameters(Collection Collection, OtherVideo OtherVideo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record AddSongToCollection
|
||||||
|
(int CollectionId, int SongId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Application.Playouts.Commands;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class AddSongToCollectionHandler :
|
||||||
|
MediatR.IRequestHandler<AddSongToCollection, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||||
|
|
||||||
|
public AddSongToCollectionHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
|
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
|
_channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
AddSongToCollection request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
Validation<BaseError, Parameters> validation = await Validate(dbContext, request);
|
||||||
|
return await validation.Apply(parameters => ApplyAddSongRequest(dbContext, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Unit> ApplyAddSongRequest(TvContext dbContext, Parameters parameters)
|
||||||
|
{
|
||||||
|
parameters.Collection.MediaItems.Add(parameters.Song);
|
||||||
|
if (await dbContext.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
// rebuild all playouts that use this collection
|
||||||
|
foreach (int playoutId in await _mediaCollectionRepository
|
||||||
|
.PlayoutIdsUsingCollection(parameters.Collection.Id))
|
||||||
|
{
|
||||||
|
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, Parameters>> Validate(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
(await CollectionMustExist(dbContext, request), await ValidateSong(dbContext, request))
|
||||||
|
.Apply((collection, episode) => new Parameters(collection, episode));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, Collection>> CollectionMustExist(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
dbContext.Collections
|
||||||
|
.Include(c => c.MediaItems)
|
||||||
|
.SelectOneAsync(c => c.Id, c => c.Id == request.CollectionId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("Collection does not exist."));
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, Song>> ValidateSong(
|
||||||
|
TvContext dbContext,
|
||||||
|
AddSongToCollection request) =>
|
||||||
|
dbContext.Songs
|
||||||
|
.SelectOneAsync(m => m.Id, e => e.Id == request.SongId)
|
||||||
|
.Map(o => o.ToValidation<BaseError>("Song does not exist"));
|
||||||
|
|
||||||
|
private record Parameters(Collection Collection, Song Song);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record AddTraktList(string TraktListUrl) : IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Locking;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using ErsatzTV.Core.Interfaces.Trakt;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class AddTraktListHandler : TraktCommandBase, IRequestHandler<AddTraktList, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
private readonly IEntityLocker _entityLocker;
|
||||||
|
|
||||||
|
public AddTraktListHandler(
|
||||||
|
ITraktApiClient traktApiClient,
|
||||||
|
ISearchRepository searchRepository,
|
||||||
|
ISearchIndex searchIndex,
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
ILogger<AddTraktListHandler> logger,
|
||||||
|
IEntityLocker entityLocker)
|
||||||
|
: base(traktApiClient, searchRepository, searchIndex, logger)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
_entityLocker = entityLocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(AddTraktList request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Validation<BaseError, Parameters> validation = ValidateUrl(request);
|
||||||
|
return await validation.Match(
|
||||||
|
DoAdd,
|
||||||
|
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_entityLocker.UnlockTrakt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Validation<BaseError, Parameters> ValidateUrl(AddTraktList request)
|
||||||
|
{
|
||||||
|
const string PATTERN = @"(?:https:\/\/trakt\.tv\/users\/)?([\w\-_]+)\/(?:lists\/)?([\w\-_]+)";
|
||||||
|
Match match = Regex.Match(request.TraktListUrl, PATTERN);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
string user = match.Groups[1].Value;
|
||||||
|
string list = match.Groups[2].Value;
|
||||||
|
return new Parameters(user, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BaseError.New("Invalid Trakt list url");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Either<BaseError, Unit>> DoAdd(Parameters parameters)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
return await TraktApiClient.GetUserList(parameters.User, parameters.List)
|
||||||
|
.BindT(list => SaveList(dbContext, list))
|
||||||
|
.BindT(list => SaveListItems(dbContext, list))
|
||||||
|
.BindT(list => MatchListItems(dbContext, list))
|
||||||
|
.MapT(_ => Unit.Default);
|
||||||
|
|
||||||
|
// match list items (and update in search index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Parameters(string User, string List);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(createCollection.Name)
|
var result2 = Optional(createCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("Collection name must be unique");
|
.ToValidation<BaseError>("Collection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createCollection.Name);
|
return (result1, result2).Apply((_, _) => createCollection.Name);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using MediatR;
|
|||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
{
|
{
|
||||||
public record CreateMultiCollectionItem(int CollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder);
|
public record CreateMultiCollectionItem(int? CollectionId, int? SmartCollectionId, bool ScheduleAsGroup, PlaybackOrder PlaybackOrder);
|
||||||
|
|
||||||
public record CreateMultiCollection
|
public record CreateMultiCollection
|
||||||
(string Name, List<CreateMultiCollectionItem> Items) : IRequest<Either<BaseError, MultiCollectionViewModel>>;
|
(string Name, List<CreateMultiCollectionItem> Items) : IRequest<Either<BaseError, MultiCollectionViewModel>>;
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Query()
|
.Query()
|
||||||
.Include(i => i.Collection)
|
.Include(i => i.Collection)
|
||||||
.LoadAsync();
|
.LoadAsync();
|
||||||
|
await dbContext.Entry(multiCollection)
|
||||||
|
.Collection(c => c.MultiCollectionSmartItems)
|
||||||
|
.Query()
|
||||||
|
.Include(i => i.SmartCollection)
|
||||||
|
.LoadAsync();
|
||||||
return ProjectToViewModel(multiCollection);
|
return ProjectToViewModel(multiCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +56,40 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
name => new MultiCollection
|
name => new MultiCollection
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
MultiCollectionItems = request.Items.Map(i => new MultiCollectionItem
|
MultiCollectionItems = request.Items.Bind(
|
||||||
{
|
i =>
|
||||||
CollectionId = i.CollectionId,
|
{
|
||||||
ScheduleAsGroup = i.ScheduleAsGroup,
|
if (i.CollectionId.HasValue)
|
||||||
PlaybackOrder = i.PlaybackOrder
|
{
|
||||||
}).ToList()
|
return Some(
|
||||||
|
new MultiCollectionItem
|
||||||
|
{
|
||||||
|
CollectionId = i.CollectionId.Value,
|
||||||
|
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||||
|
PlaybackOrder = i.PlaybackOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Option<MultiCollectionItem>.None;
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
MultiCollectionSmartItems = request.Items.Bind(
|
||||||
|
i =>
|
||||||
|
{
|
||||||
|
if (i.SmartCollectionId.HasValue)
|
||||||
|
{
|
||||||
|
return Some(
|
||||||
|
new MultiCollectionSmartItem
|
||||||
|
{
|
||||||
|
SmartCollectionId = i.SmartCollectionId.Value,
|
||||||
|
ScheduleAsGroup = i.ScheduleAsGroup,
|
||||||
|
PlaybackOrder = i.PlaybackOrder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Option<MultiCollectionSmartItem>.None;
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
});
|
});
|
||||||
|
|
||||||
private static async Task<Validation<BaseError, string>> ValidateName(
|
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||||
@@ -71,7 +104,7 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
|||||||
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
|
.Bind(_ => createMultiCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
var result2 = Optional(createMultiCollection.Name)
|
var result2 = Optional(createMultiCollection.Name)
|
||||||
.Filter(name => !allNames.Contains(name))
|
.Where(name => !allNames.Contains(name))
|
||||||
.ToValidation<BaseError>("MultiCollection name must be unique");
|
.ToValidation<BaseError>("MultiCollection name must be unique");
|
||||||
|
|
||||||
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
|
return (result1, result2).Apply((_, _) => createMultiCollection.Name);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record CreateSmartCollection
|
||||||
|
(string Query, string Name) : IRequest<Either<BaseError, SmartCollectionViewModel>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public class CreateSmartCollectionHandler :
|
||||||
|
IRequestHandler<CreateSmartCollection, Either<BaseError, SmartCollectionViewModel>>
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public CreateSmartCollectionHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, SmartCollectionViewModel>> Handle(
|
||||||
|
CreateSmartCollection request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||||
|
Validation<BaseError, SmartCollection> validation = await Validate(dbContext, request);
|
||||||
|
return await validation.Apply(c => PersistCollection(dbContext, c));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<SmartCollectionViewModel> PersistCollection(
|
||||||
|
TvContext dbContext,
|
||||||
|
SmartCollection smartCollection)
|
||||||
|
{
|
||||||
|
await dbContext.SmartCollections.AddAsync(smartCollection);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return ProjectToViewModel(smartCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task<Validation<BaseError, SmartCollection>> Validate(
|
||||||
|
TvContext dbContext,
|
||||||
|
CreateSmartCollection request) =>
|
||||||
|
ValidateName(dbContext, request).MapT(
|
||||||
|
name => new SmartCollection
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Query = request.Query
|
||||||
|
});
|
||||||
|
|
||||||
|
private static async Task<Validation<BaseError, string>> ValidateName(
|
||||||
|
TvContext dbContext,
|
||||||
|
CreateSmartCollection createSmartCollection)
|
||||||
|
{
|
||||||
|
List<string> allNames = await dbContext.SmartCollections
|
||||||
|
.Map(c => c.Name)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
Validation<BaseError, string> result1 = createSmartCollection.NotEmpty(c => c.Name)
|
||||||
|
.Bind(_ => createSmartCollection.NotLongerThan(50)(c => c.Name));
|
||||||
|
|
||||||
|
var result2 = Optional(createSmartCollection.Name)
|
||||||
|
.Where(name => !allNames.Contains(name))
|
||||||
|
.ToValidation<BaseError>("SmartCollection name must be unique");
|
||||||
|
|
||||||
|
return (result1, result2).Apply((_, _) => createSmartCollection.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||||
|
{
|
||||||
|
public record DeleteSmartCollection(int SmartCollectionId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user