Compare commits
295 Commits
v0.5.0-bet
...
v0.7.4-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c309ab430e | ||
|
|
13e21bbcce | ||
|
|
0eb36f0ce1 | ||
|
|
6429f0f064 | ||
|
|
7412ac6fc9 | ||
|
|
e58e3c786d | ||
|
|
93fc1e4eb4 | ||
|
|
cacde26796 | ||
|
|
0a3db92c60 | ||
|
|
8bb0cd5ab5 | ||
|
|
e497dc4e36 | ||
|
|
2689a67eb8 | ||
|
|
3d821043bb | ||
|
|
e69c58e615 | ||
|
|
a21b6f9f4e | ||
|
|
99b8038852 | ||
|
|
ef8ca9f8c6 | ||
|
|
d9186df157 | ||
|
|
aca6bfb0bb | ||
|
|
587fc3a98f | ||
|
|
ab1c67e60e | ||
|
|
e271f43066 | ||
|
|
6bf8feb26e | ||
|
|
ffd66f6a21 | ||
|
|
3b135df4c1 | ||
|
|
4369d04940 | ||
|
|
faaa78fed7 | ||
|
|
6bea1660ea | ||
|
|
8d46676c25 | ||
|
|
4c75e638a2 | ||
|
|
dd73a3803a | ||
|
|
f6c345d7cf | ||
|
|
585b56a668 | ||
|
|
f18f3b4f35 | ||
|
|
eb7871a048 | ||
|
|
000fc78fd3 | ||
|
|
ba676ef956 | ||
|
|
36ea88e2d6 | ||
|
|
5237e6fa50 | ||
|
|
99bde1819c | ||
|
|
f5d7ec2890 | ||
|
|
13c65435d3 | ||
|
|
315420f1a5 | ||
|
|
ab7051f075 | ||
|
|
a43e5bbe9d | ||
|
|
b7bd4541b1 | ||
|
|
648f25e9cc | ||
|
|
ccbe85a46a | ||
|
|
d168d79fe0 | ||
|
|
d37dde2477 | ||
|
|
8e13b07c84 | ||
|
|
927e7724f0 | ||
|
|
6558c5bd69 | ||
|
|
5f7efbb69c | ||
|
|
b79795af50 | ||
|
|
9479806cb0 | ||
|
|
6e49ea78ec | ||
|
|
7b1edd9c54 | ||
|
|
aeaafd2964 | ||
|
|
622fa01602 | ||
|
|
e2b3c1ce8e | ||
|
|
6c5db650e7 | ||
|
|
731072425b | ||
|
|
0f817308a8 | ||
|
|
0fc1e15cac | ||
|
|
acf30384b7 | ||
|
|
d2040eaac9 | ||
|
|
93673fce03 | ||
|
|
d7a432068b | ||
|
|
cb9215980a | ||
|
|
a4fc1f1c6f | ||
|
|
cbbdb11938 | ||
|
|
a2274bca7b | ||
|
|
f84496b09d | ||
|
|
3abf310a3b | ||
|
|
f12e361c2e | ||
|
|
cd0f1e98cc | ||
|
|
325ef80951 | ||
|
|
9a30d7c7da | ||
|
|
25ea75b761 | ||
|
|
32edf77d35 | ||
|
|
47fbb2b1b7 | ||
|
|
e388f81e1f | ||
|
|
f0bea295c4 | ||
|
|
7439ded43d | ||
|
|
6a640d3708 | ||
|
|
776bce9087 | ||
|
|
3c499f9e97 | ||
|
|
114ff7a3e3 | ||
|
|
527cdf523c | ||
|
|
91eb8ab824 | ||
|
|
7a87fb1c2e | ||
|
|
d8cc6b4c22 | ||
|
|
c9bd94d9f8 | ||
|
|
93bf818882 | ||
|
|
723fb3848d | ||
|
|
6a213e2249 | ||
|
|
a6c5c3a317 | ||
|
|
9313d2c8eb | ||
|
|
485a874ab5 | ||
|
|
f2bc884632 | ||
|
|
39d6653f8e | ||
|
|
2ce0fcb264 | ||
|
|
8bf5e18ae5 | ||
|
|
88f4d8074a | ||
|
|
f5aa2fcac8 | ||
|
|
6f892bea6b | ||
|
|
cbf0c9c988 | ||
|
|
393c67213d | ||
|
|
f69de9f071 | ||
|
|
2e400c0d22 | ||
|
|
6035c10550 | ||
|
|
555b156154 | ||
|
|
a0ea2e8910 | ||
|
|
734ca39cbd | ||
|
|
e0e5cfada5 | ||
|
|
7e0c43bc46 | ||
|
|
be1125a9ab | ||
|
|
d21c985a77 | ||
|
|
28f2b9b27e | ||
|
|
9b185e19e9 | ||
|
|
27b923b462 | ||
|
|
357dfee050 | ||
|
|
7f4004c228 | ||
|
|
9b8dc0ed80 | ||
|
|
3cc1286271 | ||
|
|
df281758b7 | ||
|
|
25273c18c8 | ||
|
|
f1be945423 | ||
|
|
9a4f772f53 | ||
|
|
d669e8114b | ||
|
|
3972e3603b | ||
|
|
acc22fcb62 | ||
|
|
2df360d7fb | ||
|
|
46331ed2c6 | ||
|
|
3aee3b0515 | ||
|
|
72c45692b2 | ||
|
|
8edf71ca55 | ||
|
|
612b9e6524 | ||
|
|
7aff65f07b | ||
|
|
5d350fcfad | ||
|
|
5546ad204c | ||
|
|
d66efa0a1d | ||
|
|
36d3d38530 | ||
|
|
8e79141860 | ||
|
|
9b3545f7ca | ||
|
|
56db20faa0 | ||
|
|
b0bd4c9fed | ||
|
|
ba079452e2 | ||
|
|
f0f2b3da4b | ||
|
|
866049543c | ||
|
|
40ed4b8b0e | ||
|
|
b43d08ca67 | ||
|
|
5e7e386108 | ||
|
|
4176df9940 | ||
|
|
de2ef959fe | ||
|
|
b53cfebac1 | ||
|
|
6895b9cc6b | ||
|
|
c60d6e46f1 | ||
|
|
c66d190174 | ||
|
|
5e8da591be | ||
|
|
9c02a6738b | ||
|
|
5ed0184bca | ||
|
|
ae64ca4a93 | ||
|
|
c47099895e | ||
|
|
a2529febba | ||
|
|
521e0ba8b3 | ||
|
|
ee0efac9be | ||
|
|
bfe7635489 | ||
|
|
aa1735f024 | ||
|
|
8deae983c7 | ||
|
|
f349646703 | ||
|
|
5003e80500 | ||
|
|
940d9cd6b5 | ||
|
|
197c166789 | ||
|
|
d114db091e | ||
|
|
3204da8e43 | ||
|
|
100eb14408 | ||
|
|
025017ace5 | ||
|
|
6a690c7c10 | ||
|
|
dd7f77751c | ||
|
|
0c13b8ef1a | ||
|
|
c6ca58ab97 | ||
|
|
0846fc1d96 | ||
|
|
e41dd68ee0 | ||
|
|
0a92996da8 | ||
|
|
082bc6145c | ||
|
|
bf3f16451b | ||
|
|
3cb37003cb | ||
|
|
9acfd2cd06 | ||
|
|
3242e7ebb8 | ||
|
|
7644d628e7 | ||
|
|
b4f19e6de4 | ||
|
|
0388425763 | ||
|
|
ca5d303ac7 | ||
|
|
18e66a92ad | ||
|
|
7d0a56ab98 | ||
|
|
5069792d12 | ||
|
|
c9789458b9 | ||
|
|
777a0d09ed | ||
|
|
4e2ebcc48a | ||
|
|
90fe1d7709 | ||
|
|
1576dd026e | ||
|
|
ee7a64eea9 | ||
|
|
9742e1eef7 | ||
|
|
a61c4b3472 | ||
|
|
ea0d43cf99 | ||
|
|
fd36ea51a7 | ||
|
|
5213b71d62 | ||
|
|
0ba3ac7f50 | ||
|
|
d960fec734 | ||
|
|
f272036c6f | ||
|
|
9fe03b6ef3 | ||
|
|
f895ab5304 | ||
|
|
07c54ff45f | ||
|
|
6a29ce2049 | ||
|
|
d19e95fb38 | ||
|
|
d78daf8735 | ||
|
|
4f6522379d | ||
|
|
9e0972fec0 | ||
|
|
6d564233ed | ||
|
|
47252b1243 | ||
|
|
bd5b52922d | ||
|
|
59c793b9be | ||
|
|
3ad1ba01f8 | ||
|
|
ab10f0ed81 | ||
|
|
44dd68fe59 | ||
|
|
6326189444 | ||
|
|
198c693208 | ||
|
|
1431b33a98 | ||
|
|
e81a8e58ea | ||
|
|
daf7114ce2 | ||
|
|
8542bc20b1 | ||
|
|
9decb91bf7 | ||
|
|
fcfd579b37 | ||
|
|
e9be182bed | ||
|
|
610e261cd7 | ||
|
|
f65818c838 | ||
|
|
1651d2895e | ||
|
|
b90c536dcb | ||
|
|
5c98eb3df5 | ||
|
|
bdff5eba75 | ||
|
|
7d112eda05 | ||
|
|
4f16431ca0 | ||
|
|
69b39c6940 | ||
|
|
fe7181ea1d | ||
|
|
88b287a094 | ||
|
|
7953e3ad85 | ||
|
|
8ba6374165 | ||
|
|
973dd4b53d | ||
|
|
6facd745ec | ||
|
|
32c4c4ec8b | ||
|
|
ecb6ed37f0 | ||
|
|
2a8bf57930 | ||
|
|
1ebc4b62e3 | ||
|
|
4ae671b633 | ||
|
|
87aa69f4cc | ||
|
|
404ea49e35 | ||
|
|
4ed40acfbe | ||
|
|
17f540dc99 | ||
|
|
780ebc01ee | ||
|
|
0a0fb71b94 | ||
|
|
53d6ecae8d | ||
|
|
837f311ec0 | ||
|
|
a9a89d04ea | ||
|
|
2e1073eb53 | ||
|
|
7687278b80 | ||
|
|
392aebd46f | ||
|
|
0a4f6d9b62 | ||
|
|
d879ce0d0d | ||
|
|
558e8acf5f | ||
|
|
89a2ac9455 | ||
|
|
39c05a24d8 | ||
|
|
78383bd5fa | ||
|
|
d67251bfa0 | ||
|
|
e91ec98007 | ||
|
|
097b8c3d1f | ||
|
|
7284ee9fb7 | ||
|
|
fccb9003a0 | ||
|
|
cc9c2f6ae3 | ||
|
|
ec6eab97b2 | ||
|
|
3ede136ff3 | ||
|
|
7c27241ab6 | ||
|
|
3713711864 | ||
|
|
d755d0ae29 | ||
|
|
c02b83d0d6 | ||
|
|
e250e93a8e | ||
|
|
60965d0961 | ||
|
|
ff1a7b376f | ||
|
|
741b00fd52 | ||
|
|
7e55681916 | ||
|
|
210630d299 | ||
|
|
0ddbb898d6 | ||
|
|
d6bf579436 | ||
|
|
765df64555 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2021.2.2",
|
||||
"version": "2022.1.0",
|
||||
"commands": [
|
||||
"jb"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
[*]
|
||||
charset=utf-8-bom
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
trim_trailing_whitespace=false
|
||||
insert_final_newline=false
|
||||
|
||||
60
.github/workflows/artifacts.yml
vendored
60
.github/workflows/artifacts.yml
vendored
@@ -33,31 +33,31 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-latest
|
||||
- os: macos-11
|
||||
kind: macOS
|
||||
target: osx-x64
|
||||
- os: macos-latest
|
||||
- os: macos-11
|
||||
kind: macOS
|
||||
target: osx-arm64
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: bahmutov/npm-install@v1.4.5
|
||||
uses: bahmutov/npm-install@v1.8.28
|
||||
with:
|
||||
working-directory: ErsatzTV/client-app
|
||||
|
||||
@@ -81,7 +81,10 @@ jobs:
|
||||
|
||||
- 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
|
||||
run: |
|
||||
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
|
||||
- name: Bundle
|
||||
shell: bash
|
||||
@@ -108,15 +111,16 @@ jobs:
|
||||
--icon "ErsatzTV.app" 200 190 \
|
||||
--hide-extension "ErsatzTV.app" \
|
||||
--app-drop-link 600 185 \
|
||||
--skip-jenkins \
|
||||
"ErsatzTV.dmg" \
|
||||
"ErsatzTV.app/"
|
||||
|
||||
- name: Notarize
|
||||
shell: bash
|
||||
run: |
|
||||
curl -o gon.zip -L -s "https://github.com/mitchellh/gon/releases/latest/download/gon_macos.zip"
|
||||
unzip -o -q gon.zip
|
||||
./gon -log-level=debug -log-json ./gon.json
|
||||
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 }}
|
||||
@@ -130,6 +134,7 @@ jobs:
|
||||
|
||||
- name: Delete old release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
if: ${{ inputs.release_tag == 'develop' }}
|
||||
with:
|
||||
token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
@@ -167,22 +172,28 @@ jobs:
|
||||
target: win-x64
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
if: ${{ matrix.kind == 'windows' }}
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: bahmutov/npm-install@v1.4.5
|
||||
uses: bahmutov/npm-install@v1.8.28
|
||||
with:
|
||||
working-directory: ErsatzTV/client-app
|
||||
|
||||
@@ -190,14 +201,14 @@ jobs:
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore -r "${{ matrix.target}}"
|
||||
run: dotnet restore -r "${{ matrix.target }}"
|
||||
|
||||
- uses: suisei-cn/actions-download-file@v1
|
||||
if: ${{ matrix.kind }} == "windows"
|
||||
- uses: suisei-cn/actions-download-file@v1.3.0
|
||||
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"
|
||||
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.1/ffmpeg-5.1-full_build.7z"
|
||||
target: ffmpeg/
|
||||
|
||||
- name: Build
|
||||
@@ -208,11 +219,15 @@ jobs:
|
||||
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
|
||||
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.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
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.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
|
||||
cargo build --manifest-path=ErsatzTV-Windows/Cargo.toml --release --all-features
|
||||
ls -l ErsatzTV-Windows/target/release
|
||||
mv ErsatzTV-Windows/target/release/ersatztv_windows.exe "$release_name/ErsatzTV-Windows.exe"
|
||||
fi
|
||||
|
||||
# Download ffmpeg
|
||||
@@ -236,6 +251,7 @@ jobs:
|
||||
|
||||
- name: Delete old release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
if: ${{ inputs.release_tag == 'develop' }}
|
||||
with:
|
||||
token: ${{ secrets.gh_token }}
|
||||
tag: ${{ inputs.release_tag }}
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Extract Docker Tag
|
||||
@@ -19,14 +19,14 @@ jobs:
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
tag2="${tag:1}"
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag2/alpha/$short}"
|
||||
final="${tag2/beta/$short}"
|
||||
echo "GIT_TAG=${final}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
short=$(git rev-parse --short HEAD)
|
||||
final="${tag/alpha/$short}"
|
||||
final="${tag/beta/$short}"
|
||||
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
|
||||
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
|
||||
outputs:
|
||||
|
||||
90
.github/workflows/docker.yml
vendored
90
.github/workflows/docker.yml
vendored
@@ -24,65 +24,89 @@ jobs:
|
||||
name: Build & Publish
|
||||
runs-on: ubuntu-latest
|
||||
if: contains(github.event.head_commit.message, '[no build]') == false
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: base
|
||||
path: ''
|
||||
suffix: ''
|
||||
qemu: false
|
||||
- name: nvidia
|
||||
path: 'nvidia/'
|
||||
suffix: '-nvidia'
|
||||
qemu: false
|
||||
- name: vaapi
|
||||
path: 'vaapi/'
|
||||
suffix: '-vaapi'
|
||||
qemu: false
|
||||
- name: arm32v7
|
||||
path: 'arm32v7/'
|
||||
suffix: '-arm'
|
||||
qemu: true
|
||||
- name: arm64
|
||||
path: 'arm64/'
|
||||
suffix: '-arm64'
|
||||
qemu: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx Base
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: builder-base
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
if: ${{ matrix.qemu == true }}
|
||||
|
||||
- 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: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
id: docker-buildx
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.docker_hub_username }}
|
||||
password: ${{ secrets.docker_hub_access_token }}
|
||||
|
||||
- name: Build and push base
|
||||
uses: docker/build-push-action@v2
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.builder-base.outputs.name }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name != 'arm64' && matrix.name != 'arm32v7' }}
|
||||
|
||||
- name: Build and push nvidia
|
||||
uses: docker/build-push-action@v2
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.builder-nvidia.outputs.name }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/nvidia/Dockerfile
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
platforms: 'linux/arm64'
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm64' }}
|
||||
|
||||
- name: Build and push vaapi
|
||||
uses: docker/build-push-action@v2
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
builder: ${{ steps.builder-vaapi.outputs.name }}
|
||||
builder: ${{ steps.docker-buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/vaapi/Dockerfile
|
||||
file: ./docker/${{ matrix.path }}Dockerfile
|
||||
push: true
|
||||
platforms: 'linux/arm/v7'
|
||||
build-args: |
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker-vaapi
|
||||
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
|
||||
tags: |
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}-vaapi
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}-vaapi
|
||||
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
|
||||
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
|
||||
if: ${{ matrix.name == 'arm32v7' }}
|
||||
|
||||
5
.github/workflows/docs.yml
vendored
5
.github/workflows/docs.yml
vendored
@@ -3,13 +3,16 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- docs/**
|
||||
- mkdocs.yml
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
|
||||
80
.github/workflows/pr.yml
vendored
80
.github/workflows/pr.yml
vendored
@@ -2,20 +2,21 @@
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
build_and_test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ windows-latest, ubuntu-latest, macos-latest ]
|
||||
build_and_test_windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
@@ -23,6 +24,67 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
||||
- name: Build Windows
|
||||
run: |
|
||||
cd ErsatzTV-Windows
|
||||
cargo build --release --all-features
|
||||
build_and_test_linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
build_and_test_mac:
|
||||
runs-on: macos-11
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 7.0.x
|
||||
|
||||
- name: Clean
|
||||
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Prep project file
|
||||
run: sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get the sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Extract Docker Tag
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
run: |
|
||||
tag=$(git describe --tags --abbrev=0)
|
||||
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
|
||||
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
|
||||
- name: Extract Artifacts Version
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
4
.github/workflows/vue-lint.yml
vendored
4
.github/workflows/vue-lint.yml
vendored
@@ -7,10 +7,10 @@ jobs:
|
||||
steps:
|
||||
# Checkout the current repo
|
||||
- name: Checkout current repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
# Setup NodeJS version 14
|
||||
- name: Setup NodeJS V14.x.x
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
# CD into the current client directory and lint and build the client
|
||||
|
||||
486
CHANGELOG.md
486
CHANGELOG.md
@@ -1,10 +1,467 @@
|
||||
# Changelog
|
||||
Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.4-beta] - 2023-02-12
|
||||
### Added
|
||||
- Add button to copy/clone schedule from schedules table
|
||||
- Synchronize episode tags and genres from Jellyfin, Emby and Local show libraries
|
||||
- Add `Deep Scan` button to Jellyfin and Emby libraries
|
||||
- This is now required to update some metadata for existing libraries, when targeted updates are not possible
|
||||
- For example, if you already have tags and genres on your episodes in Jellyfin or Emby, you will need to deep scan each library to update that metadata on existing items in ErsatzTV
|
||||
|
||||
### Fixed
|
||||
- Fix many QSV pipeline bugs
|
||||
- Fix MPEG2 video format with QSV and VAAPI acceleration
|
||||
- Fix playback of content with undefined colorspace
|
||||
- Fix NVIDIA color normalization with VP9 sources
|
||||
- Fix fallback filler looping
|
||||
- Fix bug where some libraries would never scan
|
||||
- Fix filler ordering so post-roll is properly scheduled after padded mid-roll
|
||||
- Fix pre/post-roll filler padding when used with mid-roll
|
||||
- This caused overlapping schedule items, fallback filler that was too long, etc.
|
||||
|
||||
### Changed
|
||||
- Merge generated `Other Video` folder tags with tags from sidecar NFO
|
||||
- Prioritize audio streams that are flagged as "default" when multiple candidate streams are available
|
||||
- For example, a video with a stereo commentary track and a stereo "default" track will now prefer the "default" track
|
||||
|
||||
## [0.7.3-beta] - 2023-01-25
|
||||
### Added
|
||||
- Attempt to release memory periodically
|
||||
- Add OpenID Connect (OIDC) support (e.g. Keycloak, Authelia, Auth0)
|
||||
- This only protects the management UI; all streaming endpoints will continue to allow anonymous access
|
||||
- This can be configured with the following env vars (note the double underscore separator `__`)
|
||||
- `OIDC__AUTHORITY`
|
||||
- `OIDC__CLIENTID`
|
||||
- `OIDC__CLIENTSECRET`
|
||||
- `OIDC__LOGOUTURI` (optional, needed for Auth0, use `https://{auth0-domain}/v2/logout?client_id={auth0-client-id}` with proper values for domain and client-id)
|
||||
- Add *experimental* alternate schedule system
|
||||
- This allows a single playout to dynamically select a schedule based on date criteria, for example:
|
||||
- Weekday vs weekend schedules
|
||||
- Summer vs fall schedules
|
||||
- Shark week schedules
|
||||
- Alternate schedules can be managed by clicking the calendar icon in the playout list
|
||||
- Playouts contain a prioritized (top to bottom) list of alternate schedules
|
||||
- Whenever a playout is built for a given day, ErsatzTV will check for a matching schedule from top to bottom
|
||||
- A given day must match all alternate schedule parameters; wildcards (`*any*`) will always match
|
||||
- Day of week
|
||||
- Day of month
|
||||
- Month
|
||||
- The lowest priority (bottom) item will always match all parameters, and can be considered a "default" or "fallback" schedule
|
||||
|
||||
### Fixed
|
||||
- Fix schedule editor crashing due to bad music video artist data
|
||||
- Fix bug where playouts would not maintain smart collection progress on schedules that use multiple smart collections
|
||||
- Fix library scanning on osx-arm64
|
||||
- Fix ability to remove some media server libraries from ErsatzTV
|
||||
|
||||
### Changed
|
||||
- Always use software pipeline for error display
|
||||
- This ensures errors will display even when hardware acceleration is misconfigured
|
||||
- Call scanner process only when scanning is required based on library refresh interval
|
||||
- Use lower process priority for scanner process with unforced (automatic) library scans
|
||||
- Disable V2 UI and APIs by default
|
||||
- V2 UI can be re-enabled by setting the env var `ETV_UI_V2` to any value
|
||||
|
||||
## [0.7.2-beta] - 2023-01-05
|
||||
### Fixed
|
||||
- Fix VAAPI encoding in docker by switching to non-free driver
|
||||
|
||||
### Changed
|
||||
- Rewrite log page to read directly from log files instead of sqlite
|
||||
|
||||
## [0.7.1-beta] - 2023-01-03
|
||||
### Added
|
||||
- Add new music video credit templates
|
||||
|
||||
### Fixed
|
||||
- Fix many transcoding failures caused by the colorspace filter
|
||||
- Fix song playback with VAAPI and NVENC
|
||||
- Fix edge case where some local movies would not automatically be restored from trash
|
||||
- Fix synchronizing Jellyfin and Emby collection items
|
||||
- Fix saving some external subtitle records to database
|
||||
|
||||
### Changed
|
||||
- Upgrade to dotnet 7
|
||||
- Upgrade all docker images to ubuntu jammy and ffmpeg 5.1.2
|
||||
- Limit library scan interval between 0 and 1,000,000
|
||||
- 0 means do not automatically scan libraries
|
||||
- 1 to 999,999 means scan if it has been that many hours since the last scan
|
||||
- Use new `ErsatzTV.Scanner` process for scanning all libraries
|
||||
- This should reduce the ongoing memory footprint
|
||||
|
||||
## [0.7.0-beta] - 2022-12-11
|
||||
### Fixed
|
||||
- Fix removing Jellyfin and Emby libraries that have been deleted from the source media server
|
||||
- Fix `Work-Ahead HLS Segmenter Limit` setting to properly limit number of channels that can work-ahead at once
|
||||
- Include base path value in generated channel playlist (M3U) and channel guide (XMLTV) links
|
||||
- Fix parsing song metadata from OGG audio files
|
||||
- Properly unlock/re-enable trakt list operations after an operation is canceled
|
||||
|
||||
### Added
|
||||
- Add (required) bit depth normalization option to ffmpeg profile
|
||||
- This can help if your card only supports e.g. h264 encoding, normalizing to 8 bits will allow the hardware encoder to be used
|
||||
- Extract font attachments after extracting text subtitles
|
||||
- This should improve SubStation Alpha subtitle rendering
|
||||
- Detect VAAPI capabilities and fallback to software decoding/encoding as needed
|
||||
- Add audio stream selector scripts for episodes and movies
|
||||
- This will let you customize which audio stream is selected for playback
|
||||
- Episodes are passed the following data:
|
||||
- `channelNumber`
|
||||
- `channelName`
|
||||
- `showTitle`
|
||||
- `showGuids`: array of string ids like `imdb_1234` or `tvdb_1234`
|
||||
- `seasonNumber`
|
||||
- `episodeNumber`
|
||||
- `episodeGuids`: array of string ids like `imdb_1234` or `tvdb_1234`
|
||||
- `preferredLanguageCodes`: array of string preferred language codes configured for the channel
|
||||
- `audioStreams`: array of audio stream data, each containing
|
||||
- `index`: the stream's index number, this is what the function needs to return
|
||||
- `channels`: the number of audio channels
|
||||
- `codec`: the audio codec
|
||||
- `isDefault`: bool indicating whether the stream is flagged as default
|
||||
- `isForced`: bool indicating whether the stream is flagged as forced
|
||||
- `language`: the stream's language
|
||||
- `title`: the stream's title
|
||||
- Movies are passed the following data:
|
||||
- `channelNumber`
|
||||
- `channelName`
|
||||
- `title`
|
||||
- `guids`: array of string ids like `imdb_1234` or `tvdb_1234`
|
||||
- `preferredLanguageCodes`: array of string preferred language codes configured for the channel
|
||||
- `audioStreams`: array of audio stream data, each containing
|
||||
- `index`: the stream's index number, this is what the function needs to return
|
||||
- `channels`: the number of audio channels
|
||||
- `codec`: the audio codec
|
||||
- `isDefault`: bool indicating whether the stream is flagged as default
|
||||
- `isForced`: bool indicating whether the stream is flagged as forced
|
||||
- `language`: the stream's language
|
||||
- `title`: the stream's title
|
||||
- Add new fields to search index
|
||||
- `video_codec`: the video codec
|
||||
- `video_bit_depth`: the number of bits in the video stream's pixel format, e.g. 8 or 10
|
||||
- `video_dynamic_range`: the video's dynamic range, either `sdr` or `hdr`
|
||||
|
||||
### Changed
|
||||
- Change `Multi-Episode Shuffle` scripting system to use Javascript instead of Lua
|
||||
|
||||
## [0.6.9-beta] - 2022-10-21
|
||||
### Fixed
|
||||
- Fix bug where tail or fallback filler would sometimes schedule much longer than expected
|
||||
- This only happened with fixed start schedule items following a schedule item with tail or fallback filler
|
||||
- Fix NFO reader bug that caused inaccurate warning messages about invalid XML and incomplete metadata
|
||||
- Fix reverse proxy SSL termination support by supporting `X-Forwarded-Proto` header
|
||||
- Fix automatic playout reset scheduling
|
||||
- Playouts would reset every 30 minutes between midnight and the configured time, instead of only at the configured time
|
||||
- XMLTV: properly group schedule items with `Custom Title` followed by item(s) with `Guide Mode` set to `Filler`
|
||||
|
||||
### Added
|
||||
- Add music video credits template system
|
||||
- Templates are selected in each channel's settings
|
||||
- Templates should be copied from `_default.ass.sbntxt` which is located in the config subfolder `templates/music-video-credits`
|
||||
- Copy the file, give it any name ending with `.ass.sbntext`, and only make edits to the copied file
|
||||
- The default template will be extracted and overwritten every time ErsatzTV is started
|
||||
- The template is an [Advanced SubStation Alpha](http://www.tcax.org/docs/ass-specs.htm) file using [scribian](https://github.com/scriban/scriban/tree/master/doc) template syntax
|
||||
- The following fields are available for use in the template:
|
||||
- `resolution`: the ffmpeg profile's resolution, which is used for margin calculations
|
||||
- `title`: the title of the music video
|
||||
- `track`: the music video's track number
|
||||
- `album`: the music video's album
|
||||
- `plot`: the music video's plot
|
||||
- `release_date`: the music video's release date
|
||||
- `artist`: the music videos artist (the parent folder)
|
||||
- `all_artists`: a list of additional artists from the music video's sidecar NFO metadata file
|
||||
- `duration`: the timespan duration of the music video, which can be used to calculate timing of additional subtitles
|
||||
- `stream_seek`: the timespan that ffmpeg will seek into the media item before beginning playback
|
||||
- Add `Multi-Episode Shuffle` playout order for `Television Show` schedule items
|
||||
- The purpose of this playout order is to improve randomization for shows that normally have intro, multiple episodes, and outro
|
||||
- This playout order requires splitting the parts into individual files (e.g. splitting `s01e01-03.mkv` into `s01e01.mkv`, `s01e02.mkv` and `s01e03.mkv`)
|
||||
- This playout order requires a lua script in the config subfolder `scripts/multi-episode-shuffle`
|
||||
- The lua script should be named for the television show's guid, e.g. `tvdb_12345.lua` or `imdb_tt123456789.lua`
|
||||
- The script defines the number of parts that each un-split file typically contains
|
||||
- The script also defines a function to map each episode to a part number (or no part number i.e. `nil` if an episode has not been split)
|
||||
- All groups of part numbers (i.e. all part 1s, all part 2s) will be shuffled
|
||||
- The playout order will then schedule a random part 1 followed by a random part 2, etc
|
||||
- Un-split (`nil`) episodes will be randomly placed between re-combined parts (e.g. part1, part2, part3, un-split, part1, part2, part3)
|
||||
- Add `ETV_BASE_URL` environment variable to support reverse proxies that use paths (e.g. `/ersatztv`)
|
||||
|
||||
### Changed
|
||||
- No longer place watermarks within content by default (e.g. within 4:3 content padded to a 16:9 resolution)
|
||||
- This can be re-enabled if desired using the `Place Within Source Content` checkbox in watermark settings
|
||||
|
||||
## [0.6.8-beta] - 2022-10-05
|
||||
### Fixed
|
||||
- Fix typo introduced in `0.6.7-beta` that stopped QSV HEVC encoder from working
|
||||
- Fix scaling logic for `Nvidia` acceleration and software mode
|
||||
- Attempt to position watermarks within content (not over added black padding)
|
||||
- Fix search results for `Other Videos` when NFO metadata is used
|
||||
- Properly synchronize tags from Emby movies and shows
|
||||
- Properly sync updated file paths from Plex
|
||||
- Fix numeric range search queries (e.g. `minutes:[5 TO 10]`, `minutes:[* TO 3]`)
|
||||
|
||||
### Added
|
||||
- Add `QSV Device` option to ffmpeg profile on linux
|
||||
- Add guids to search index (e.g. `imdb:tt000000`, `tvdb:12345`)
|
||||
|
||||
## [0.6.7-beta] - 2022-09-05
|
||||
### Fixed
|
||||
- When all audio streams are selected with `HLS Direct`, explicitly copy them without transcoding
|
||||
- This only happens when the channel does not have a `Preferred Audio Language`
|
||||
- Fix scanner crash caused by invalid mtime
|
||||
- `VAAPI`: Downgrade libva from 2.15 to 2.14
|
||||
- Fix bug with XMLTV that caused some filler to display with primary content details
|
||||
- Multiple fixes for content scaling with `Nvidia`, `Qsv` and `Vaapi` accelerations
|
||||
- Properly scale image-based subtitles
|
||||
- Fix bug where a schedule containing a single item (fixed start and flood) would never finish building a playout
|
||||
- Logic was also added to detect infinite playout build loops in the future and stop them
|
||||
- Fix bug where `Other Videos` wouldn't be included in scheduling mode `Shuffle In Order`
|
||||
|
||||
### Added
|
||||
- Add `Preferred Audio Title` feature
|
||||
- Preference can be configured in channel settings and overridden on schedule items
|
||||
- When a title is specified, audio streams that contain that title (case-insensitive search) will be prioritized
|
||||
- This can be helpful for creating channels that use commentary tracks
|
||||
- External tooling exists to easily update title/name metadata if your audio streams don't already have this metadata
|
||||
- Add `Amf` hardware acceleration option for AMD GPUs on Windows
|
||||
- Add `QSV Extra Hardware Frames` parameter for tuning QSV acceleration
|
||||
- Performance may improve on some systems after doubling or halving the default value of `64`
|
||||
|
||||
## [0.6.6-beta] - 2022-08-17
|
||||
### Fixed
|
||||
- Use MIME Type `application/x-mpegurl` for all playlists instead of `application/vnd.apple.mpegurl`
|
||||
- Replace `setsar` filter with `setdar` filter
|
||||
- `setsar` caused issues scaling between two different aspect ratios
|
||||
- For example, some 4:3 content would appear stretched when scaled to a 16:9 resolution
|
||||
- `setdar` is now only used when aspect ratios match
|
||||
- Prioritize aspect ratio from container when video stream contains conflicting aspect ratio
|
||||
- This is usually caused by bad authoring, but the change should improve scaling behavior for edge cases
|
||||
|
||||
### Added
|
||||
- Support DSD audio file formats (DFF and DSF) in local song libraries
|
||||
- Support OGG audio file formats (OGG, OPUS, OGA, OGX, SPX) in local song libraries
|
||||
|
||||
### Changed
|
||||
- Always return playlist after a maximum of 8 seconds while starting up an HLS Segmenter session
|
||||
- Use multi-variant playlists instead of redirects for HLS Segmenter sessions
|
||||
- Upgrade ffmpeg from 5.0 to 5.1 in most docker images (not ARM variants)
|
||||
- Upgrading from 5.0 to 5.1 is also recommended for other installations (Windows, Linux)
|
||||
|
||||
## [0.6.5-beta] - 2022-08-02
|
||||
### Fixed
|
||||
- Fix database initializer; fresh installs with v0.6.4-beta are missing some config data and should upgrade
|
||||
|
||||
## [0.6.4-beta] - 2022-07-28
|
||||
### Fixed
|
||||
- Fix subtitle stream selection when subtitle language is different than audio language
|
||||
- Fix bug with unsupported AAC channel layouts
|
||||
- Fix NVIDIA second-gen maxwell capabilities detection
|
||||
- Return distinct search results for episodes and other videos that have the same title
|
||||
- For example, two other videos both named `Trailer` would previously have displayed as one item in search results
|
||||
- Fix schedules that would begin to repeat the same content in the same order after a couple of days
|
||||
|
||||
### Added
|
||||
- Add `640x480` resolution
|
||||
|
||||
## [0.6.3-beta] - 2022-07-04
|
||||
### Fixed
|
||||
- Maintain stream continuity when playout is rebuilt for a channel that is actively being streamed
|
||||
- Properly apply changes to episode title, sort title, outline and plot from Plex
|
||||
- Fix search index for other videos and songs
|
||||
- In previous versions, some libraries would incorrectly display only one item
|
||||
- Properly display old versions of renamed items in trash
|
||||
|
||||
### Added
|
||||
- Add `Minimum Log Level` option to `Settings` page
|
||||
- Other methods of configuring the log level will no longer work
|
||||
|
||||
## [0.6.2-beta] - 2022-06-18
|
||||
### Fixed
|
||||
- Fix content repeating for up to a minute near the top of every hour
|
||||
- Check whether hardware-accelerated hevc codecs are supported by the NVIDIA card
|
||||
- Software codecs will be used if they are unsupported by the NVIDIA card
|
||||
- Fix sorting of channel contents in EPG
|
||||
- Fix Jellyfin admin user id sync
|
||||
- Ignore disabled admins and admins who do not have access to all libraries
|
||||
|
||||
### Added
|
||||
- Add 32-bit `arm` docker tags (`develop-arm` and `latest-arm`)
|
||||
|
||||
### Changed
|
||||
- Regularly delete old segments from transcode folder while content is actively transcoding
|
||||
- This should help reduce required disk space
|
||||
- To further minimize required disk space, set `Work-Ahead HLS Segmenter Limit` to `0` in `Settings`
|
||||
|
||||
## [0.6.1-beta] - 2022-06-03
|
||||
### Fixed
|
||||
- Fix Jellyfin show library paging
|
||||
- Properly locate and identify multiple Plex servers
|
||||
- Properly restore `Unavailable`/`File Not Found` items when they are located on disk
|
||||
|
||||
### Added
|
||||
- Add basic music video credits subtitle generation
|
||||
- This can be enabled in channel settings
|
||||
|
||||
## [0.6.0-beta] - 2022-06-01
|
||||
### Fixed
|
||||
- Additional fix for duplicate `Other Videos` entries; trash may need to be emptied one last time after upgrading
|
||||
- Fix watermark opacity in cultures where `,` is a decimal separator
|
||||
- Rework playlist filtering to avoid empty playlist responses
|
||||
- Fix some QSV/VAAPI memory errors by always requesting 64 extra hardware frames
|
||||
|
||||
### Added
|
||||
- Enable QSV hardware acceleration for vaapi docker images
|
||||
|
||||
### Changed
|
||||
- Use paging to synchronize all media from Plex, Jellyfin and Emby
|
||||
- This will reduce memory use and improve reliability of synchronizing large libraries
|
||||
- Disable low power mode for `h264_qsv` and `hevc_qsv` encoders
|
||||
|
||||
## [0.5.8-beta] - 2022-05-20
|
||||
### Fixed
|
||||
- Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes
|
||||
- Remove erroneous log messages about normalizing framerate on channels where framerate normalization is disabled
|
||||
- Fix unscheduled filler gaps that sometimes happen as playouts are automatically extended each hour
|
||||
|
||||
### Added
|
||||
- Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason
|
||||
|
||||
### Changed
|
||||
- Remove thread limitation for scenarios where it is not required
|
||||
- This should give a performance boost to installations that don't use hardware acceleration
|
||||
- Use hardware acceleration to display error messages where configured
|
||||
|
||||
## [0.5.7-beta] - 2022-05-14
|
||||
### Fixed
|
||||
- Reduce memory use due to library scan operations
|
||||
- Fix some instances of filler getting "stuck" when a filler item is encountered that's too long for the gap
|
||||
- Properly ignore Plex `Other Videos` libraries (`movie` libraries where agent is `com.plexapp.agents.none`)
|
||||
- Fix `Custom Title` for schedule items with `One`, `Multiple` and `Flood` playout modes
|
||||
- Fix scheduling bug where flood items would sometimes fail to continue after midnight
|
||||
|
||||
### Added
|
||||
- Add `metadata_kind` field to search index to allow searching for items with a particular metdata source
|
||||
- Valid metadata kinds are `fallback`, `sidecar` (NFO), `external` (from a media server) and `embedded` (songs)
|
||||
- Add autocomplete functionality to search bar to quickly navigate to channels, ffmpeg profiles, collections and schedules by name
|
||||
- Add global setting to skip missing (file-not-found or unavailable) items when building playouts
|
||||
- Add filler preset option to allow watermarks to overlay on top of filler (disabled by default)
|
||||
- This option is applied when new items are added to a playout; rebuilding is needed if you want the change to take effect immediately
|
||||
- Read `track` field from music video NFO metadata and use it for chronological sorting (after release date)
|
||||
- Add `Random Start Point` option to schedules
|
||||
- When this option is enabled, all `Chronological` or `Shuffle In Order` content groups will have their start points randomized
|
||||
- When this option is disabled, all `Chronological` or `Shuffle In Order` content groups will start with the chronologically earliest item
|
||||
|
||||
### Changed
|
||||
- Replace invalid (control) characters in NFO metadata with replacement character `<60>` before parsing
|
||||
- Store partial (incomplete) NFO metadata results when invalid XML is encountered
|
||||
- Previously, no metadata would be stored if the XML within the NFO failed to validate
|
||||
|
||||
## [0.5.6-beta] - 2022-05-06
|
||||
### Fixed
|
||||
- Fix processing local movie NFO metadata without a `year` value
|
||||
- Fix processing local movie fallback metadata
|
||||
- Fix search edge case where very recently added items (hours) would not be returned by relative date queries
|
||||
- Fix search index validation on startup; improper validation was causing a rebuild with every startup
|
||||
- Block library scanning until search index has been recreated/upgraded
|
||||
- Fix occasional erroneous log messages when HLS channel playback times out because all clients have left
|
||||
- Fix fallback filler playback
|
||||
- Fix stream continuity when error messages are displayed
|
||||
- Fix duplicate scanning within `Other Video` libraries (i.e. folders would be scanned multiple times)
|
||||
|
||||
### Added
|
||||
- Add `show_genre` and `show_tag` to search index for seasons and episodes
|
||||
- Use `aired` value to source release date from music video nfo metadata
|
||||
- Add NFO metadata support to `Other Video` libraries
|
||||
- `Other Video` NFO metadata must be in the movie NFO metadata format
|
||||
|
||||
## [0.5.5-beta] - 2022-05-03
|
||||
### Fixed
|
||||
- Fix adding episodes with no title to the search index
|
||||
- This behavior was preventing some items from being removed from the trash
|
||||
- Support combination NFO metadata for movies, shows, artists and music videos
|
||||
- Note that ErsatzTV does not scrape any metadata; any URLs after the XML will be ignored
|
||||
- Fix bug causing some Jellyfin and Emby content to incorrectly show as unavailable
|
||||
- Fix extracting embedded `mov_text` subtitles
|
||||
- Properly extract embedded subtitles on playouts where subtitles are only enabled on schedule items (and not on the channel itself)
|
||||
|
||||
### Added
|
||||
- Add experimental `arm64` docker tags (`develop-arm64` and `latest-arm64`)
|
||||
- Use `Sort Title` from Movie NFO metadata if available
|
||||
- Support multiple `Artist` entries in music video NFO metadata
|
||||
|
||||
## [0.5.4-beta] - 2022-04-29
|
||||
### Fixed
|
||||
- Cleanly stop all library scans when service termination is requested
|
||||
- Fix health check crash when trash contains a show or a season
|
||||
- Fix ability of health check crash to crash home page
|
||||
- Remove and ignore Season 0/Specials from Plex shows that have no specials
|
||||
- Automatically delete and rebuild the search index on startup if it has become corrupt
|
||||
- Automatically scan Jellyfin and Emby libraries on startup and periodically
|
||||
- Properly remove un-synchronized Plex, Jellyfin and Emby items from the database and search index
|
||||
- Fix synchronizing movies within a collection from Jellyfin
|
||||
|
||||
### Changed
|
||||
- Update Plex, Jellyfin and Emby movie and show library scanners to share a significant amount of code
|
||||
- This should help maintain feature parity going forward
|
||||
- Optimize search-index rebuilding to complete 100x faster
|
||||
- **No longer use network paths to source content from Jellyfin and Emby**
|
||||
- **If you previously used path replacements to convert network paths to local paths, you should remove them**
|
||||
|
||||
### Added
|
||||
- Add `unavailable` state for Jellyfin and Emby movie and show libraries
|
||||
- Add `height` and `width` to search index for all videos
|
||||
- Add `season_number` and `episode_number` to search index for all episodes
|
||||
- Add `season_number` to search index for seasons
|
||||
- Add `show_title` to search index for seasons and episodes
|
||||
|
||||
## [0.5.3-beta] - 2022-04-24
|
||||
### Fixed
|
||||
- Cleanly stop Plex library scan when service termination is requested
|
||||
- Fix bug introduced with 0.5.2-beta that prevented some Plex content from being played
|
||||
- Fix spammy subtitle error message
|
||||
- Fix generating blur hashes for song backgrounds in Docker
|
||||
|
||||
### Changed
|
||||
- No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk
|
||||
- Instead, a new `unavailable` media state will be used to indicate this condition
|
||||
- After updating mounts, path replacements, etc - a library scan can be used to resolve this state
|
||||
|
||||
## [0.5.2-beta] - 2022-04-22
|
||||
### Fixed
|
||||
- Fix unlocking libraries when scanning fails for any reason
|
||||
- Fix software overlay of actual size watermark
|
||||
|
||||
### Added
|
||||
- Add support for burning in embedded and external text subtitles
|
||||
- **This requires a one-time full library scan, which may take a long time with large libraries.**
|
||||
- Sync Plex, Jellyfin and Emby collections as tags on movies, shows, seasons and episodes
|
||||
- This allows smart collections that use queries like `tag:"Collection Name"`
|
||||
- Note that Emby has an outstanding collections bug that prevents updates when removing items from a collection
|
||||
- Sync Plex labels as tags on movies and shows
|
||||
- This allows smart collections that use queries like `tag:"Plex Label Name"`
|
||||
- Add `Deep Scan` button for Plex libraries
|
||||
- This scanning mode is *slow* but is required to detect some changes like labels
|
||||
|
||||
### Changed
|
||||
- Improve the speed and change detection of the Plex library scanners
|
||||
|
||||
## [0.5.1-beta] - 2022-04-17
|
||||
### Fixed
|
||||
- Fix subtitles edge case with NVENC
|
||||
- Only select picture subtitles (text subtitles are not yet supported)
|
||||
- Supported picture subtitles are `hdmv_pgs_subtitle` and `dvd_subtitle`
|
||||
- Fix subtitles using software encoders, videotoolbox, VAAPI
|
||||
- Fix setting VAAPI driver name
|
||||
- Fix ffmpeg troubleshooting reports
|
||||
- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected
|
||||
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
|
||||
|
||||
### Added
|
||||
- Add `Empty Trash` button to `Trash` page
|
||||
|
||||
## [0.5.0-beta] - 2022-04-13
|
||||
### Fixed
|
||||
- Fix `HLS Segmenter` bug where it would drift off of the schedule if a playout was changed while the segmenter was running
|
||||
@@ -1068,7 +1525,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.4-beta...HEAD
|
||||
[0.7.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.3-beta...v0.7.4-beta
|
||||
[0.7.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.2-beta...v0.7.3-beta
|
||||
[0.7.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.1-beta...v0.7.2-beta
|
||||
[0.7.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.7.0-beta...v0.7.1-beta
|
||||
[0.7.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.9-beta...v0.7.0-beta
|
||||
[0.6.9-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.8-beta...v0.6.9-beta
|
||||
[0.6.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.7-beta...v0.6.8-beta
|
||||
[0.6.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.6-beta...v0.6.7-beta
|
||||
[0.6.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.5-beta...v0.6.6-beta
|
||||
[0.6.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.4-beta...v0.6.5-beta
|
||||
[0.6.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.3-beta...v0.6.4-beta
|
||||
[0.6.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.2-beta...v0.6.3-beta
|
||||
[0.6.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...v0.6.2-beta
|
||||
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
|
||||
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
|
||||
[0.5.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.7-beta...v0.5.8-beta
|
||||
[0.5.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.6-beta...v0.5.7-beta
|
||||
[0.5.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.5-beta...v0.5.6-beta
|
||||
[0.5.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.4-beta...v0.5.5-beta
|
||||
[0.5.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.3-beta...v0.5.4-beta
|
||||
[0.5.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.2-beta...v0.5.3-beta
|
||||
[0.5.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...v0.5.2-beta
|
||||
[0.5.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
|
||||
[0.5.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
|
||||
[0.4.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
|
||||
[0.4.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.3-alpha...v0.4.4-alpha
|
||||
@@ -1156,4 +1636,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
[0.0.5-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
|
||||
[0.0.4-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
|
||||
[0.0.3-prealpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
|
||||
[0.0.1-prealpha]: https://github.com/jasongdove/ErsatzTV/releases/tag/v0.0.1-prealpha
|
||||
[0.0.1-prealpha]: https://github.com/jasongdove/ErsatzTV/releases/tag/v0.0.1-prealpha
|
||||
|
||||
2
ErsatzTV-Windows/.gitignore
vendored
Normal file
2
ErsatzTV-Windows/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
target/
|
||||
|
||||
1028
ErsatzTV-Windows/Cargo.lock
generated
Normal file
1028
ErsatzTV-Windows/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
ErsatzTV-Windows/Cargo.toml
Normal file
19
ErsatzTV-Windows/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "ersatztv_windows"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tray-item = { git = "https://github.com/olback/tray-item-rs" }
|
||||
special-folder = { git = "https://github.com/masinc/special-folder-rs" }
|
||||
process_path = "0.1.4"
|
||||
|
||||
[dependencies.windows]
|
||||
version = "0.43.0"
|
||||
features = [
|
||||
"Win32_System_Console",
|
||||
"Win32_Foundation"
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
windres = "*"
|
||||
@@ -1,33 +0,0 @@
|
||||
<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="CliWrap" Version="3.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Program.cs">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,14 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
using System.Diagnostics;
|
||||
using CliWrap;
|
||||
|
||||
namespace ErsatzTV_Windows;
|
||||
|
||||
public class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
private readonly NotifyIcon _trayIcon;
|
||||
private readonly CancellationTokenSource _tokenSource;
|
||||
|
||||
public TrayApplicationContext()
|
||||
{
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Icon = new Icon("./Ersatztv.ico"),
|
||||
ContextMenuStrip = new ContextMenuStrip(),
|
||||
Visible = true
|
||||
};
|
||||
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
|
||||
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))
|
||||
{
|
||||
|
||||
Cli.Wrap(exe)
|
||||
.WithWorkingDirectory(folder)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync(_tokenSource.Token);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_tokenSource?.Cancel();
|
||||
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();
|
||||
}
|
||||
}
|
||||
5
ErsatzTV-Windows/build.rs
Normal file
5
ErsatzTV-Windows/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use windres::Build;
|
||||
|
||||
fn main() {
|
||||
Build::new().compile("ersatztv_windows.rc").unwrap();
|
||||
}
|
||||
2
ErsatzTV-Windows/ersatztv_windows.rc
Normal file
2
ErsatzTV-Windows/ersatztv_windows.rc
Normal file
@@ -0,0 +1,2 @@
|
||||
id ICON "ersatztv.ico"
|
||||
ersatztv-icon ICON "ersatztv.ico"
|
||||
112
ErsatzTV-Windows/src/main.rs
Normal file
112
ErsatzTV-Windows/src/main.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use special_folder::SpecialFolder;
|
||||
use std::fs;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use windows::Win32::System::Console;
|
||||
use {std::sync::mpsc, tray_item::TrayItem};
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
enum Message {
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut tray = TrayItem::new("ErsatzTV", "ersatztv-icon").unwrap();
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
tray.add_menu_item("Launch Web UI", || {
|
||||
let _ = Command::new("cmd")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("/C")
|
||||
.arg("start")
|
||||
.arg("http://localhost:8409")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
tray.add_menu_item("Show Logs", || {
|
||||
let path = SpecialFolder::LocalApplicationData
|
||||
.get()
|
||||
.unwrap()
|
||||
.join("ersatztv")
|
||||
.join("logs");
|
||||
match path.to_str() {
|
||||
None => {}
|
||||
Some(folder) => {
|
||||
fs::create_dir_all(folder).unwrap();
|
||||
let _ = Command::new("cmd")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.arg("/C")
|
||||
.arg("start")
|
||||
.arg(folder)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
tray.inner_mut().add_separator().unwrap();
|
||||
|
||||
tray.add_menu_item("Exit", move || {
|
||||
tx.send(Message::Exit).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let path = process_path::get_executable_path();
|
||||
let mut child: Option<Child> = None;
|
||||
match path {
|
||||
None => {}
|
||||
Some(path) => {
|
||||
let etv = path.parent().unwrap().join("ErsatzTV.exe");
|
||||
if etv.exists() {
|
||||
match etv.to_str() {
|
||||
None => {}
|
||||
Some(etv) => {
|
||||
child = Some(
|
||||
Command::new(etv)
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(Message::Exit) => {
|
||||
match child {
|
||||
None => {}
|
||||
Some(mut child) => {
|
||||
unsafe {
|
||||
if Console::AttachConsole(child.id()) == true
|
||||
{
|
||||
Console::GenerateConsoleCtrlEvent(Console::CTRL_C_EVENT, 0);
|
||||
}
|
||||
}
|
||||
child.wait().unwrap();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,4 @@ public record ArtistViewModel(
|
||||
List<string> Genres,
|
||||
List<string> Styles,
|
||||
List<string> Moods,
|
||||
List<CultureInfo> Languages);
|
||||
List<CultureInfo> Languages);
|
||||
|
||||
@@ -37,4 +37,4 @@ internal static class Mapper
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Artists;
|
||||
|
||||
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
|
||||
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
using ErsatzTV.Application.MediaItems;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.MediaItems.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Artists;
|
||||
|
||||
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
|
||||
public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public Task<List<NamedMediaItemViewModel>> Handle(
|
||||
public async Task<List<NamedMediaItemViewModel>> Handle(
|
||||
GetAllArtists request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_artistRepository.GetAllArtists()
|
||||
.Map(
|
||||
list => list.Filter(
|
||||
a => !string.IsNullOrWhiteSpace(
|
||||
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
List<Artist> allArtists = await dbContext.Artists
|
||||
.AsNoTracking()
|
||||
.Include(a => a.ArtistMetadata)
|
||||
.ToListAsync(cancellationToken: cancellationToken);
|
||||
|
||||
return allArtists.Bind(a => ProjectArtist(a)).ToList();
|
||||
}
|
||||
|
||||
private static Option<NamedMediaItemViewModel> ProjectArtist(Artist a)
|
||||
{
|
||||
foreach (ArtistMetadata metadata in a.ArtistMetadata.HeadOrNone())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Title))
|
||||
{
|
||||
return ProjectToViewModel(a);
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Artists;
|
||||
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
|
||||
@@ -29,4 +29,4 @@ public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<Artist
|
||||
},
|
||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ public record ChannelViewModel(
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
int PlayoutCount,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode);
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate);
|
||||
|
||||
@@ -12,8 +12,11 @@ public record CreateChannel
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
|
||||
|
||||
@@ -71,8 +71,11 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
StreamingMode = request.StreamingMode,
|
||||
Artwork = artwork,
|
||||
PreferredAudioLanguageCode = preferredAudioLanguageCode,
|
||||
PreferredAudioTitle = request.PreferredAudioTitle,
|
||||
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
|
||||
SubtitleMode = request.SubtitleMode
|
||||
SubtitleMode = request.SubtitleMode,
|
||||
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
|
||||
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
|
||||
};
|
||||
|
||||
foreach (int id in watermarkId)
|
||||
@@ -106,7 +109,9 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
|
||||
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
|
||||
private static async Task<Validation<BaseError, string>> ValidateNumber(
|
||||
TvContext dbContext,
|
||||
CreateChannel createChannel)
|
||||
{
|
||||
Option<Channel> maybeExistingChannel = await dbContext.Channels
|
||||
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
|
||||
@@ -169,4 +174,4 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
|
||||
o => o.ToValidation<BaseError>(
|
||||
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
|
||||
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
|
||||
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
|
||||
|
||||
@@ -20,4 +20,4 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
|
||||
(await _channelRepository.Get(deleteChannel.ChannelId))
|
||||
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
|
||||
.Map(c => c.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ public record UpdateChannel
|
||||
int FFmpegProfileId,
|
||||
string Logo,
|
||||
string PreferredAudioLanguageCode,
|
||||
string PreferredAudioTitle,
|
||||
StreamingMode StreamingMode,
|
||||
int? WatermarkId,
|
||||
int? FallbackFillerId,
|
||||
string PreferredSubtitleLanguageCode,
|
||||
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
ChannelSubtitleMode SubtitleMode,
|
||||
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
|
||||
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Subtitles;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using static ErsatzTV.Application.Channels.Mapper;
|
||||
using Channel = ErsatzTV.Core.Domain.Channel;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
|
||||
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
public UpdateChannelHandler(
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_workerChannel = workerChannel;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, ChannelViewModel>> Handle(
|
||||
UpdateChannel request,
|
||||
@@ -22,7 +31,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
|
||||
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
|
||||
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
|
||||
}
|
||||
|
||||
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
|
||||
@@ -33,8 +42,11 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
c.Categories = update.Categories;
|
||||
c.FFmpegProfileId = update.FFmpegProfileId;
|
||||
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
|
||||
c.PreferredAudioTitle = update.PreferredAudioTitle;
|
||||
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
|
||||
c.SubtitleMode = update.SubtitleMode;
|
||||
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
|
||||
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
|
||||
c.Artwork ??= new List<Artwork>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(update.Logo))
|
||||
@@ -60,11 +72,23 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
c.Artwork.Add(artwork);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
c.StreamingMode = update.StreamingMode;
|
||||
c.WatermarkId = update.WatermarkId;
|
||||
c.FallbackFillerId = update.FallbackFillerId;
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
if (c.SubtitleMode != ChannelSubtitleMode.None)
|
||||
{
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == c.Id);
|
||||
|
||||
foreach (Playout playout in maybePlayout)
|
||||
{
|
||||
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
|
||||
}
|
||||
}
|
||||
|
||||
return ProjectToViewModel(c);
|
||||
}
|
||||
|
||||
@@ -114,4 +138,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
|
||||
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToValidation<BaseError>("Preferred audio language code is invalid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,15 @@ internal static class Mapper
|
||||
channel.FFmpegProfileId,
|
||||
GetLogo(channel),
|
||||
channel.PreferredAudioLanguageCode,
|
||||
channel.PreferredAudioTitle,
|
||||
channel.StreamingMode,
|
||||
channel.WatermarkId,
|
||||
channel.FallbackFillerId,
|
||||
channel.Playouts?.Count ?? 0,
|
||||
channel.PreferredSubtitleLanguageCode,
|
||||
channel.SubtitleMode);
|
||||
channel.SubtitleMode,
|
||||
channel.MusicVideoCreditsMode,
|
||||
channel.MusicVideoCreditsTemplate);
|
||||
|
||||
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
|
||||
new(
|
||||
@@ -44,4 +47,4 @@ internal static class Mapper
|
||||
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
|
||||
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
|
||||
|
||||
@@ -18,4 +18,4 @@ public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi,
|
||||
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
|
||||
return channels.Map(ProjectToResponseModel).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<Channe
|
||||
|
||||
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
|
||||
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
|
||||
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<Chan
|
||||
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.Get(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,4 @@ public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Opt
|
||||
|
||||
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,22 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
|
||||
|
||||
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
FFmpegProfile ffmpegProfile = await dbContext.Channels
|
||||
.Filter(c => c.Number == request.ChannelNumber)
|
||||
.Include(c => c.FFmpegProfile)
|
||||
.Map(c => c.FFmpegProfile)
|
||||
.SingleAsync(cancellationToken);
|
||||
|
||||
if (!ffmpegProfile.NormalizeFramerate)
|
||||
{
|
||||
return Option<int>.None;
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
|
||||
public record GetChannelGuide(string Scheme, string Host, string BaseUrl) : IRequest<ChannelGuide>;
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Iptv;
|
||||
using Microsoft.IO;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
|
||||
{
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
|
||||
|
||||
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
|
||||
public GetChannelGuideHandler(
|
||||
IChannelRepository channelRepository,
|
||||
RecyclableMemoryStreamManager recyclableMemoryStreamManager)
|
||||
{
|
||||
_channelRepository = channelRepository;
|
||||
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
|
||||
}
|
||||
|
||||
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAllForGuide()
|
||||
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
|
||||
}
|
||||
.Map(
|
||||
channels => new ChannelGuide(
|
||||
_recyclableMemoryStreamManager,
|
||||
request.Scheme,
|
||||
request.Host,
|
||||
request.BaseUrl,
|
||||
channels));
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
|
||||
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
|
||||
|
||||
@@ -12,4 +12,4 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
|
||||
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelNameByPlayoutId(int PlayoutId) : IRequest<Option<string>>;
|
||||
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameByPlayoutId, Option<string>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
return await dbContext.Playouts
|
||||
.Include(p => p.Channel)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
|
||||
.MapT(p => p.Channel.Name);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Channels;
|
||||
|
||||
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
|
||||
public record GetChannelPlaylist(string Scheme, string Host, string BaseUrl, string Mode) : IRequest<ChannelPlaylist>;
|
||||
|
||||
@@ -14,7 +14,7 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
||||
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
|
||||
_channelRepository.GetAll()
|
||||
.Map(channels => EnsureMode(channels, request.Mode))
|
||||
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
|
||||
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, request.BaseUrl, channels));
|
||||
|
||||
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
|
||||
{
|
||||
@@ -47,4 +47,4 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest<Unit>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigE
|
||||
await _configElementRepository.Upsert(request.Key, request.Value);
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest<Either<BaseError, Unit>>;
|
||||
@@ -0,0 +1,32 @@
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using Serilog.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class UpdateGeneralSettingsHandler : IRequestHandler<UpdateGeneralSettings, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly LoggingLevelSwitch _loggingLevelSwitch;
|
||||
|
||||
public UpdateGeneralSettingsHandler(
|
||||
LoggingLevelSwitch loggingLevelSwitch,
|
||||
IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_loggingLevelSwitch = loggingLevelSwitch;
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateGeneralSettings request,
|
||||
CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings);
|
||||
|
||||
private async Task<Unit> ApplyUpdate(GeneralSettingsViewModel generalSettings)
|
||||
{
|
||||
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.MinimumLogLevel);
|
||||
_loggingLevelSwitch.MinimumLevel = generalSettings.MinimumLogLevel;
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class UpdateLibraryRefreshIntervalHandler :
|
||||
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
|
||||
IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
@@ -16,13 +16,16 @@ public class UpdateLibraryRefreshIntervalHandler :
|
||||
UpdateLibraryRefreshInterval request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
|
||||
.MapT(
|
||||
_ => _configElementRepository.Upsert(
|
||||
ConfigElementKey.LibraryRefreshInterval,
|
||||
request.LibraryRefreshInterval))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
|
||||
Optional(request.LibraryRefreshInterval)
|
||||
.Where(lri => lri > 0)
|
||||
.Where(lri => lri is >= 0 and < 1_000_000)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.ToValidation<BaseError>("Library refresh interval must be zero or greated")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record UpdatePlayoutSettings(PlayoutSettingsViewModel PlayoutSettings) : IRequest<Either<BaseError, Unit>>;
|
||||
@@ -9,13 +9,13 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
|
||||
public class UpdatePlayoutSettingsHandler : IRequestHandler<UpdatePlayoutSettings, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
|
||||
public UpdatePlayoutDaysToBuildHandler(
|
||||
public UpdatePlayoutSettingsHandler(
|
||||
IConfigElementRepository configElementRepository,
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel)
|
||||
@@ -26,17 +26,20 @@ public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDays
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, Unit>> Handle(
|
||||
UpdatePlayoutDaysToBuild request,
|
||||
UpdatePlayoutSettings request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, Unit> validation = await Validate(request);
|
||||
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
|
||||
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.PlayoutSettings));
|
||||
}
|
||||
|
||||
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
|
||||
private async Task<Unit> ApplyUpdate(TvContext dbContext, PlayoutSettingsViewModel playoutSettings)
|
||||
{
|
||||
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
|
||||
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, playoutSettings.DaysToBuild);
|
||||
await _configElementRepository.Upsert(
|
||||
ConfigElementKey.PlayoutSkipMissingItems,
|
||||
playoutSettings.SkipMissingItems);
|
||||
|
||||
// continue all playouts to proper number of days
|
||||
List<Playout> playouts = await dbContext.Playouts
|
||||
@@ -50,10 +53,10 @@ public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDays
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
|
||||
Optional(request.DaysToBuild)
|
||||
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutSettings request) =>
|
||||
Optional(request.PlayoutSettings.DaysToBuild)
|
||||
.Where(days => days > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Days to build must be greater than zero")
|
||||
.AsTask();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class GeneralSettingsViewModel
|
||||
{
|
||||
public LogEventLevel MinimumLogLevel { get; set; }
|
||||
}
|
||||
@@ -6,4 +6,4 @@ internal static class Mapper
|
||||
{
|
||||
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
|
||||
new(element.Key, element.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class PlayoutSettingsViewModel
|
||||
{
|
||||
public int DaysToBuild { get; set; }
|
||||
public bool SkipMissingItems { get; set; }
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKe
|
||||
GetConfigElementByKey request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetGeneralSettings : IRequest<GeneralSettingsViewModel>;
|
||||
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class GetGeneralSettingsHandler : IRequestHandler<GetGeneralSettings, GeneralSettingsViewModel>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public async Task<GeneralSettingsViewModel> Handle(GetGeneralSettings request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<LogEventLevel> maybeLogLevel =
|
||||
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
|
||||
|
||||
return new GeneralSettingsViewModel
|
||||
{
|
||||
MinimumLogLevel = await maybeLogLevel.IfNoneAsync(LogEventLevel.Information)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetLibraryRefreshInterval : IRequest<int>;
|
||||
public record GetLibraryRefreshInterval : IRequest<int>;
|
||||
|
||||
@@ -13,4 +13,4 @@ public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefres
|
||||
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
|
||||
.Map(result => result.IfNone(6));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetPlayoutDaysToBuild : IRequest<int>;
|
||||
@@ -1,16 +0,0 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||
.Map(result => result.IfNone(2));
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public record GetPlayoutSettings : IRequest<PlayoutSettingsViewModel>;
|
||||
@@ -0,0 +1,26 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public class GetPlayoutSettingsHandler : IRequestHandler<GetPlayoutSettings, PlayoutSettingsViewModel>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetPlayoutSettingsHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public async Task<PlayoutSettingsViewModel> Handle(GetPlayoutSettings request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
|
||||
|
||||
Option<bool> skipMissingItems =
|
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.PlayoutSkipMissingItems);
|
||||
|
||||
return new PlayoutSettingsViewModel
|
||||
{
|
||||
DaysToBuild = await daysToBuild.IfNoneAsync(2),
|
||||
SkipMissingItems = await skipMissingItems.IfNoneAsync(false)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Threading.Channels;
|
||||
using ErsatzTV.Application.Libraries;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Errors;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Runtime;
|
||||
using ErsatzTV.Infrastructure.Data;
|
||||
using ErsatzTV.Infrastructure.Extensions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchronizeEmbyLibraryById>,
|
||||
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
|
||||
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||
{
|
||||
public CallEmbyLibraryScannerHandler(
|
||||
IDbContextFactory<TvContext> dbContextFactory,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
|
||||
IMediator mediator,
|
||||
IRuntimeInfo runtimeInfo)
|
||||
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
|
||||
{
|
||||
}
|
||||
|
||||
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>.Handle(
|
||||
ForceSynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken) => Handle(request, cancellationToken);
|
||||
|
||||
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.Handle(
|
||||
SynchronizeEmbyLibraryByIdIfNeeded request,
|
||||
CancellationToken cancellationToken) => Handle(request, cancellationToken);
|
||||
|
||||
private async Task<Either<BaseError, string>> Handle(
|
||||
ISynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, string> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
scanner => PerformScan(scanner, request, cancellationToken),
|
||||
error =>
|
||||
{
|
||||
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
|
||||
{
|
||||
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
|
||||
}
|
||||
|
||||
return Task.FromResult<Either<BaseError, string>>(error.Join());
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, string>> PerformScan(
|
||||
string scanner,
|
||||
ISynchronizeEmbyLibraryById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"scan-emby", request.EmbyLibraryId.ToString()
|
||||
};
|
||||
|
||||
if (request.ForceScan)
|
||||
{
|
||||
arguments.Add("--force");
|
||||
}
|
||||
|
||||
if (request.DeepScan)
|
||||
{
|
||||
arguments.Add("--deep");
|
||||
}
|
||||
|
||||
return await base.PerformScan(scanner, arguments, cancellationToken);
|
||||
}
|
||||
|
||||
protected override async Task<DateTimeOffset> GetLastScan(
|
||||
TvContext dbContext,
|
||||
ISynchronizeEmbyLibraryById request)
|
||||
{
|
||||
return await dbContext.EmbyLibraries
|
||||
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId)
|
||||
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
|
||||
}
|
||||
|
||||
protected override bool ScanIsRequired(
|
||||
DateTimeOffset lastScan,
|
||||
int libraryRefreshInterval,
|
||||
ISynchronizeEmbyLibraryById request)
|
||||
{
|
||||
if (lastScan == SystemTime.MaxValueUtc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
|
||||
return request.ForceScan || (libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record DisconnectEmby : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Search;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
|
||||
public class DisconnectEmbyHandler : IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
@@ -38,4 +38,4 @@ public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Eit
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ using ErsatzTV.Core.Emby;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
public record SaveEmbySecrets(EmbySecrets Secrets) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
@@ -6,7 +6,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
|
||||
public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
@@ -53,4 +53,4 @@ public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, E
|
||||
}
|
||||
|
||||
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
|
||||
@@ -8,8 +8,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class
|
||||
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
|
||||
public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IEmbyApiClient _embyApiClient;
|
||||
private readonly IEmbySecretStore _embySecretStore;
|
||||
@@ -72,32 +71,33 @@ public class
|
||||
connectionParameters.ActiveConnection.Address,
|
||||
connectionParameters.ApiKey);
|
||||
|
||||
await maybeLibraries.Match(
|
||||
async libraries =>
|
||||
{
|
||||
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
|
||||
.ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.EmbyMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove);
|
||||
if (ids.Any())
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
},
|
||||
error =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
|
||||
connectionParameters.EmbyMediaSource.ServerName,
|
||||
error.Value);
|
||||
foreach (BaseError error in maybeLibraries.LeftToSeq())
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
|
||||
connectionParameters.EmbyMediaSource.ServerName,
|
||||
error.Value);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
foreach (List<EmbyLibrary> libraries in maybeLibraries.RightToSeq())
|
||||
{
|
||||
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
|
||||
.ToList();
|
||||
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
|
||||
var toUpdate = libraries
|
||||
.Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList();
|
||||
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
|
||||
connectionParameters.EmbyMediaSource.Id,
|
||||
toAdd,
|
||||
toRemove,
|
||||
toUpdate);
|
||||
if (ids.Any())
|
||||
{
|
||||
await _searchIndex.RemoveItems(ids);
|
||||
_searchIndex.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
@@ -108,4 +108,4 @@ public class
|
||||
{
|
||||
public string ApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
|
||||
IEmbyBackgroundServiceRequest
|
||||
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>, IEmbyBackgroundServiceRequest
|
||||
{
|
||||
int EmbyLibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
bool DeepScan { get; }
|
||||
}
|
||||
|
||||
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
|
||||
{
|
||||
public bool ForceScan => false;
|
||||
public bool DeepScan => false;
|
||||
}
|
||||
|
||||
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
|
||||
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId, bool DeepScan) : ISynchronizeEmbyLibraryById
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain;
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
|
||||
IEmbyBackgroundServiceRequest;
|
||||
IEmbyBackgroundServiceRequest;
|
||||
|
||||
@@ -32,4 +32,4 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
|
||||
|
||||
return mediaSources;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record UpdateEmbyLibraryPreferences
|
||||
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
|
||||
|
||||
@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Search;
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class
|
||||
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
|
||||
UpdateEmbyLibraryPreferencesHandler : IRequestHandler<UpdateEmbyLibraryPreferences,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
@@ -33,4 +33,4 @@ public class
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record UpdateEmbyPathReplacements(
|
||||
int EmbyMediaSourceId,
|
||||
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
List<EmbyPathReplacementItem> PathReplacements) : IRequest<Either<BaseError, Unit>>;
|
||||
|
||||
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
|
||||
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
|
||||
|
||||
@@ -4,7 +4,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
|
||||
public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathReplacements,
|
||||
Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
@@ -46,4 +46,4 @@ public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateE
|
||||
.Map(
|
||||
v => v.ToValidation<BaseError>(
|
||||
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyConnectionParametersViewModel(string Address);
|
||||
public record EmbyConnectionParametersViewModel(string Address);
|
||||
|
||||
@@ -3,5 +3,10 @@ using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
|
||||
: LibraryViewModel("Emby", Id, Name, MediaKind);
|
||||
public record EmbyLibraryViewModel(
|
||||
int Id,
|
||||
string Name,
|
||||
LibraryMediaKind MediaKind,
|
||||
bool ShouldSyncItems,
|
||||
int MediaSourceId)
|
||||
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId);
|
||||
|
||||
@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.Emby;
|
||||
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
|
||||
Id,
|
||||
Name,
|
||||
Address);
|
||||
Address);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
|
||||
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
|
||||
|
||||
@@ -11,8 +11,8 @@ internal static class Mapper
|
||||
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
|
||||
|
||||
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
|
||||
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems, library.MediaSourceId);
|
||||
|
||||
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
|
||||
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
|
||||
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
|
||||
|
||||
@@ -14,4 +14,4 @@ public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSour
|
||||
GetAllEmbyMediaSources request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
|
||||
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
|
||||
|
||||
@@ -63,4 +63,4 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
|
||||
private record ConnectionParameters(
|
||||
EmbyMediaSource EmbyMediaSource,
|
||||
EmbyConnection ActiveConnection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
|
||||
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
|
||||
|
||||
@@ -16,4 +16,4 @@ public class
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
|
||||
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
|
||||
|
||||
@@ -15,4 +15,4 @@ public class
|
||||
GetEmbyMediaSourceById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ErsatzTV.Application.Emby;
|
||||
|
||||
public record GetEmbyPathReplacementsBySourceId
|
||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user