Compare commits

..
50 Commits
Author SHA1 Message Date
Jason Dove 17bc988b49 update changelog for release v0.4.0-alpha [no ci] 2022-01-29 18:22:07 -06:00
Jason Dove 749eea836b update install docs for tray apps (win, mac) [no build] 2022-01-29 18:18:58 -06:00
Jason DoveandGitHub 37c52c4cb4 update docs and dependencies (#603) 2022-01-29 18:08:05 -06:00
Jason DoveandGitHub 33ba58aa68 add windows launcher (#602) 2022-01-29 17:58:11 -06:00
Jason DoveandGitHub 5f6043e593 index added date (#601) 2022-01-29 14:47:58 -06:00
Jason Dove 96e95a21fb update changelog [no ci] 2022-01-29 12:34:10 -06:00
Jason DoveandGitHub 9168fd6bf2 write text file logs (#600) 2022-01-29 12:31:22 -06:00
Jason Dove 14413f62a7 properly sent content root on macos 2022-01-29 11:49:38 -06:00
Jason Dove 34c71a0c12 try to fix static resource loading 2022-01-29 10:20:03 -06:00
Jason Dove a487e7fe15 use absolute paths in bundle script 2022-01-28 22:11:50 -06:00
Jason Dove cd4ea42597 fix bundle script 2022-01-28 22:05:20 -06:00
Jason Dove a3d42145f7 update macos submodule 2022-01-28 21:57:16 -06:00
Jason Dove 261cf5052a fetch submodules for mac build 2022-01-28 21:48:29 -06:00
Jason Dove de9af2f0f6 first pass at native macos app 2022-01-28 21:41:26 -06:00
Jason DoveandGitHub 8d4e18ed2f update mac app icon (#599) 2022-01-28 19:46:34 -06:00
Jason DoveandGitHub 1ee01c1d78 fix hls timestamps (#598) 2022-01-27 23:51:18 -06:00
Jason DoveandGitHub 7de50dd916 minor hls segmenter improvements (#593) 2022-01-26 20:12:29 -06:00
Jason DoveandGitHub 744fd3beaa link file not found health check to trash (#592)
* update dependencies

* fix file not found health check
2022-01-26 08:37:40 -06:00
Jason DoveandGitHub 861c95e1bd fix m3u mode override (#590) 2022-01-25 18:36:54 -06:00
Jason Dove bb5b9f9be4 update changelog for release v0.3.8-alpha [no ci] 2022-01-23 21:46:00 -06:00
Jason Dove 135628441a re-add mac launcher script 2022-01-23 21:09:04 -06:00
Jason DoveandGitHub 4aa7204984 fix ts mode with hdhr clients (#588) 2022-01-23 18:58:15 -06:00
Jason Dove 1af59a0337 don't use macos launcher script 2022-01-23 18:47:00 -06:00
Jason Dove c4c97fcc8c customize mac dmg 2022-01-23 18:36:31 -06:00
Jason Dove 9c46e42792 fix gon variables 2022-01-23 14:21:13 -06:00
Jason Dove efa803aab6 split mac artifacts job 2022-01-23 14:10:57 -06:00
Jason Dove 6ea02a2d77 use proper version number in ci artifacts [no docs] [no build] 2022-01-23 14:04:13 -06:00
Jason Dove 631f7d2d5e don't use reserved secret name 2022-01-23 13:54:37 -06:00
Jason Dove e44a4cb2e1 properly pass secrets between workflows 2022-01-23 13:53:31 -06:00
Jason Dove f4b95419a6 properly pass data between jobs 2022-01-23 13:46:47 -06:00
Jason DoveandGitHub 1a5cf49563 refactor reusable docker workflow (#587)
* refactor reusable docker workflow

* refactor reusable artifacts workflow

* fix name

* try to fix

* fix
2022-01-23 13:42:01 -06:00
Jason Dove efef0b0fee don't use single file for mac bundles 2022-01-22 17:29:35 -06:00
Jason Dove ee7b8a71ab fix docker builds [no build] 2022-01-22 14:19:49 -06:00
Jason DoveandGitHub e7c9a51e96 macos app bundle (#585)
* test signed app bundle

* fix vars

* fix condition

* typo

* fix quoting

* use recursive signing script

* fix release cleanup

* restore proper ci action
2022-01-22 14:03:42 -06:00
Jason Dove 78a954f365 link to development builds in install docs 2022-01-21 20:55:38 -06:00
Jason Dove 355c0b7be9 try to fix deleting old assets 2022-01-21 20:29:50 -06:00
Jason Dove 3bcb2d36f9 another attempt at publishing artifacts 2022-01-21 20:21:26 -06:00
Jason Dove b240de9d4a publish develop artifacts to stable release url 2022-01-21 18:44:25 -06:00
Jason Dove f5001837cb properly separate build artifacts 2022-01-21 18:20:51 -06:00
Jason Dove 6ea916b1f0 fix fetch depth 2022-01-21 15:30:40 -06:00
Jason Dove db6fd22215 try to fix build 2022-01-21 15:25:03 -06:00
Jason DoveandGitHub 691842008d upload develop binaries for every merge to main (#584)
* upload develop binaries for every merge to main

* rename step
2022-01-21 15:18:08 -06:00
Jason DoveandGitHub 685f78bef8 fix search results bug (#583) 2022-01-21 14:05:09 -06:00
Jason DoveandGitHub 3ce267863b fix hls segmenter in some cultures (#582) 2022-01-21 10:42:33 -06:00
Jason DoveandGitHub e4231cb57d upgrade from ffmpeg 4.4 to 5.0 (#581) 2022-01-20 20:57:38 -06:00
Jason DoveandGitHub 03946b13ca always use a single ffmpeg thread with realtime (#580) 2022-01-20 14:53:13 -06:00
Jason DoveandGitHub f1a81bf086 clarify library kind/media kind support (#579) [no docker] 2022-01-19 09:11:23 -06:00
Jason Dove 7a88374362 clarify flood scheduling calc [no ci] 2022-01-18 18:22:46 -06:00
Jason DoveandGitHub 663a62431b properly fix startup paths (#576) 2022-01-17 16:31:22 -06:00
Jason Dove 1d4acc284d Update changelog for release v0.3.7-alpha [no ci] 2022-01-17 15:23:39 -06:00
70 changed files with 1173 additions and 371 deletions
+218
View File
@@ -0,0 +1,218 @@
name: Build Artifacts
on:
workflow_call:
inputs:
release_tag:
description: 'Release tag'
required: true
type: string
release_version:
description: 'Release version number (e.g. v0.3.7-alpha)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
secrets:
apple_developer_certificate_p12_base64:
required: true
apple_developer_certificate_password:
required: true
ac_username:
required: true
ac_password:
required: true
gh_token:
required: true
jobs:
build_and_upload_mac:
name: Mac Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: macos-latest
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
p12-password: ${{ secrets.apple_developer_certificate_password }}
- name: Calculate Release Name
shell: bash
run: |
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
- name: Build
shell: bash
run: dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
- name: Bundle
shell: bash
run: |
brew install coreutils
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
scripts/macOS/bundle.sh
- name: Sign
shell: bash
run: scripts/macOS/sign.sh
- name: Create DMG
shell: bash
run: |
brew install create-dmg
create-dmg \
--volname "ErsatzTV" \
--volicon "artwork/ErsatzTV.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "ErsatzTV.app" 200 190 \
--hide-extension "ErsatzTV.app" \
--app-drop-link 600 185 \
"ErsatzTV.dmg" \
"ErsatzTV.app/"
- name: Notarize
shell: bash
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
gon -log-level=debug -log-json ./gon.json
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Cleanup
shell: bash
run: |
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
rm -r publish
rm -r ErsatzTV.app
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- name: Build
shell: bash
run: |
# Define some variables for things we need
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
# Build Windows launcher
if [ "${{ matrix.kind }}" == "windows" ]; then
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
fi
# Pack files
if [ "${{ matrix.kind }}" == "windows" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
+39 -91
View File
@@ -1,49 +1,19 @@
name: Build name: Build
on: on:
workflow_dispatch: workflow_dispatch:
pull_request:
push: push:
branches: branches:
- main - main
jobs: jobs:
build_and_test: calculate_version:
runs-on: ${{ matrix.os }} name: Calculate version information
strategy: runs-on: ubuntu-latest
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
build_and_push:
name: Build & Publish to Docker Hub
needs: build_and_test
runs-on: ubuntu-latest
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
@@ -51,60 +21,38 @@ jobs:
short=$(git rev-parse --short HEAD) short=$(git rev-parse --short HEAD)
final="${tag2/alpha/$short}" final="${tag2/alpha/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
- name: Set up Docker Buildx NVIDIA final="${tag/alpha/$short}"
uses: docker/setup-buildx-action@v1 echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
id: builder-nvidia echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
- name: Set up Docker Buildx VAAPI git_tag: ${{ env.GIT_TAG }}
uses: docker/setup-buildx-action@v1 artifacts_version: ${{ env.ARTIFACTS_VERSION }}
id: builder-vaapi info_version: ${{ env.INFO_VERSION }}
build_and_upload:
- name: Login to DockerHub uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
uses: docker/login-action@v1 needs: calculate_version
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_tag: develop
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
- name: Build and push base secrets:
uses: docker/build-push-action@v2 apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
with: apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
builder: ${{ steps.builder-base.outputs.name }} ac_username: ${{ secrets.AC_USERNAME }}
context: . ac_password: ${{ secrets.AC_PASSWORD }}
file: ./docker/Dockerfile gh_token: ${{ secrets.GITHUB_TOKEN }}
push: true build_and_push:
build-args: | uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
INFO_VERSION=${{ env.GIT_TAG }}-docker needs: calculate_version
tags: | with:
jasongdove/ersatztv:develop base_version: develop
jasongdove/ersatztv:${{ github.sha }} info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ github.sha }}
- name: Build and push nvidia secrets:
uses: docker/build-push-action@v2 docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
with: docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:develop-nvidia
jasongdove/ersatztv:${{ github.sha }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:develop-vaapi
jasongdove/ersatztv:${{ github.sha }}-vaapi
+88
View File
@@ -0,0 +1,88 @@
name: Build & Publish to Docker Hub
on:
workflow_call:
inputs:
base_version:
description: 'Base version (latest or develop)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
tag_version:
description: 'Docker tag version (e.g. v0.3.7)'
required: true
type: string
secrets:
docker_hub_username:
required: true
docker_hub_access_token:
required: true
jobs:
build_and_push:
name: Build & Publish
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '[no build]') == false
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Docker Buildx Base
uses: docker/setup-buildx-action@v1
id: builder-base
- name: Set up Docker Buildx NVIDIA
uses: docker/setup-buildx-action@v1
id: builder-nvidia
- name: Set up Docker Buildx VAAPI
uses: docker/setup-buildx-action@v1
id: builder-vaapi
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Build and push base
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-base.outputs.name }}
context: .
file: ./docker/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}
jasongdove/ersatztv:${{ inputs.tag_version }}
- name: Build and push nvidia
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker-vaapi
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}-vaapi
jasongdove/ersatztv:${{ inputs.tag_version }}-vaapi
-1
View File
@@ -3,7 +3,6 @@ on:
push: push:
branches: branches:
- main - main
jobs: jobs:
build: build:
name: Deploy docs name: Deploy docs
+30
View File
@@ -0,0 +1,30 @@
name: Pull Request
on:
pull_request:
jobs:
build_and_test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
steps:
- name: Get the sources
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
+39 -128
View File
@@ -1,142 +1,53 @@
name: Publish name: Release
on: on:
release: release:
types: [ published ] types: [ published ]
jobs: jobs:
release: calculate_version:
name: Release name: Calculate version information
strategy: runs-on: ubuntu-latest
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: windows-latest
kind: windows
target: win-x64
- os: macos-latest
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
runs-on: ${{ matrix.os }}
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
shell: bash
run: |
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:DebugType=Embedded /property:PublishSingleFile=true --self-contained true
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
name: Build & Publish to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
- name: Set up Docker Buildx NVIDIA echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
uses: docker/setup-buildx-action@v1 outputs:
id: builder-nvidia git_tag: ${{ env.GIT_TAG }}
docker_tag: ${{ env.DOCKER_TAG }}
- name: Set up Docker Buildx VAAPI artifacts_version: ${{ env.ARTIFACTS_VERSION }}
uses: docker/setup-buildx-action@v1 info_version: ${{ env.INFO_VERSION }}
id: builder-vaapi build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
- name: Login to DockerHub needs: calculate_version
uses: docker/login-action@v1 with:
with: release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
- name: Build and push base apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
uses: docker/build-push-action@v2 apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
with: ac_username: ${{ secrets.AC_USERNAME }}
builder: ${{ steps.builder-base.outputs.name }} ac_password: ${{ secrets.AC_PASSWORD }}
context: . gh_token: ${{ secrets.GITHUB_TOKEN }}
file: ./docker/Dockerfile build_and_push:
push: true uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
build-args: | needs: calculate_version
INFO_VERSION=${{ env.GIT_TAG }}-docker with:
tags: | base_version: latest
jasongdove/ersatztv:latest info_version: ${{ needs.calculate_version.outputs.git_tag }}
jasongdove/ersatztv:${{ env.DOCKER_TAG }} tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
secrets:
- name: Build and push nvidia docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
uses: docker/build-push-action@v2 docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:latest-nvidia
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:latest-vaapi
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
+3
View File
@@ -0,0 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:jasongdove/ErsatzTV-macOS.git
+41 -2
View File
@@ -4,6 +4,42 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
## [0.4.0-alpha] - 2022-01-29
### Fixed
- Fix m3u `mode` query param to properly override streaming mode for all channels
- `segmenter` for `HLS Segmenter`
- `hls-direct` for `HLS Direct`
- `ts` for `MPEG-TS`
- `ts-legacy` for `MPEG-TS (Legacy)`
- omitting the `mode` parameter returns each channel as configured
- Link `File Not Found` health check to `Trash` page to allow deletion
- Fix `HLS Segmenter` streaming mode with multiple ffmpeg-based clients
- Jellyfin (web) and TiviMate (Android) were specifically tested
### Added
- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
- Also write logs to text files in the `logs` config subfolder
- Add `added_date` to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
- Add `added_inthelast`, `added_notinthelast` search field for relative added date queries
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
## [0.3.8-alpha] - 2022-01-23
### Fixed
- Fix issue preventing some versions of ffmpeg (usually 4.4.x) from streaming MPEG-TS (Legacy) channels at all
- The issue appears to be caused by using a thread count other than `1`
- Thread count is now forced to `1` for all streaming modes other than HLS Segmenter
- Fix bug with HLS Segmenter in cultures where `.` is a group/thousands separator
- Fix search results page crashing with some media kinds
- Always use MPEG-TS or MPEG-TS (Legacy) streaming mode with HDHR (Plex)
- Other configured modes will fall back to MPEG-TS when accessed by Plex
### Changed
- Upgrade ffmpeg from 4.4 to 5.0 in all docker images
- Upgrading from 4.4 to 5.0 is recommended for all installations
## [0.3.7-alpha] - 2022-01-17
### Fixed ### Fixed
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents - Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
- Fix double-click startup on mac - Fix double-click startup on mac
@@ -909,8 +945,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker. - Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...HEAD [Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...HEAD
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha [0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha [0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha [0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha [0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
+33
View File
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<RootNamespace>ErsatzTV_Windows</RootNamespace>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>Ersatztv.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="Ersatztv.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asmichi.ChildProcess" Version="0.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV_Windows;
public static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
public static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new TrayApplicationContext());
}
}
@@ -0,0 +1,77 @@
using ErsatzTV.Core;
using System.Diagnostics;
using Asmichi.ProcessManagement;
using System.Reflection;
namespace ErsatzTV_Windows;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly IChildProcess? _childProcess;
public TrayApplicationContext()
{
_trayIcon = new NotifyIcon
{
Icon = new Icon("./Ersatztv.ico"),
ContextMenuStrip = new ContextMenuStrip(),
Visible = true
};
AddMenuItem("Launch Web UI", LaunchWebUI);
AddMenuItem("Show Logs", ShowLogs);
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
AddMenuItem("Exit", Exit);
string folder = AppContext.BaseDirectory;
string exe = Path.Combine(folder, "ErsatzTV.exe");
if (File.Exists(exe))
{
var si = new ChildProcessStartInfo(exe);
_childProcess = ChildProcess.Start(si);
}
}
private void AddMenuItem(string name, EventHandler action)
{
var item = new ToolStripMenuItem(name);
item.Click += action;
_trayIcon.ContextMenuStrip.Items.Add(item);
}
private void LaunchWebUI(object? sender, EventArgs e)
{
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = "http://localhost:8409";
process.Start();
}
private void ShowLogs(object? sender, EventArgs e)
{
if (!Directory.Exists(FileSystemLayout.LogsFolder))
{
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
}
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
process.Start();
}
protected override void Dispose(bool disposing)
{
_childProcess?.Dispose();
base.Dispose(disposing);
}
private void Exit(object? sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
_trayIcon.Visible = false;
Application.Exit();
}
}
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
@@ -36,10 +36,14 @@ namespace ErsatzTV.Application.Channels.Queries
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect; channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel); result.Add(channel);
break; break;
case "ts": case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream; channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel); result.Add(channel);
break; break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default: default:
result.Add(channel); result.Add(channel);
break; break;
@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="9.0.0" /> <PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
@@ -132,12 +133,16 @@ namespace ErsatzTV.Application.Streaming
using IServiceScope scope = _serviceScopeFactory.CreateScope(); using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>(); IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
long ptsOffset = await GetPtsOffset(mediator, channelNumber, cancellationToken);
// _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset);
var request = new GetPlayoutItemProcessByChannelNumber( var request = new GetPlayoutItemProcessByChannelNumber(
channelNumber, channelNumber,
"segmenter", "segmenter",
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1), firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
!firstProcess, !firstProcess,
realtime); realtime,
ptsOffset);
// _logger.LogInformation("Request {@Request}", request); // _logger.LogInformation("Request {@Request}", request);
@@ -231,6 +236,28 @@ namespace ErsatzTV.Application.Streaming
_playlistStart = trimResult.PlaylistStart; _playlistStart = trimResult.PlaylistStart;
} }
} }
private async Task<long> GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken)
{
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
Option<FileInfo> lastSegment =
Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault());
long result = 0;
foreach (FileInfo segment in lastSegment)
{
Either<BaseError, PtsAndDuration> queryResult = await mediator.Send(
new GetLastPtsDuration(segment.FullName),
cancellationToken);
foreach (PtsAndDuration ptsAndDuration in queryResult.RightToSeq())
{
result = ptsAndDuration.Pts + ptsAndDuration.Duration;
}
}
return result;
}
private async Task<int> GetWorkAheadLimit() private async Task<int> GetWorkAheadLimit()
{ {
@@ -0,0 +1,12 @@
namespace ErsatzTV.Application.Streaming;
public record PtsAndDuration(long Pts, long Duration)
{
public static PtsAndDuration From(string ffprobeLine)
{
string[] split = ffprobeLine.Split("|");
var left = long.Parse(split[0]);
var right = long.Parse(split[1]);
return new PtsAndDuration(left, right);
}
}
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Streaming.Queries
string Mode, string Mode,
DateTimeOffset Now, DateTimeOffset Now,
bool StartAtZero, bool StartAtZero,
bool HlsRealtime) : IRequest<Either<BaseError, PlayoutItemProcessModel>>; bool HlsRealtime,
long PtsOffset) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;
} }
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
"ts-legacy", "ts-legacy",
DateTimeOffset.Now, DateTimeOffset.Now,
false, false,
true) true,
0)
{ {
Scheme = scheme; Scheme = scheme;
Host = host; Host = host;
@@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Streaming.Queries;
public record GetLastPtsDuration(string FileName) : IRequest<Either<BaseError, PtsAndDuration>>;
@@ -0,0 +1,87 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Streaming.Queries;
public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Either<BaseError, PtsAndDuration>>
{
private readonly IConfigElementRepository _configElementRepository;
public GetLastPtsDurationHandler(IConfigElementRepository configElementRepository)
{
_configElementRepository = configElementRepository;
}
public async Task<Either<BaseError, PtsAndDuration>> Handle(
GetLastPtsDuration request,
CancellationToken cancellationToken)
{
Validation<BaseError, RequestParameters> validation = await Validate(request);
return await validation.Match(
Handle,
error => Task.FromResult<Either<BaseError, PtsAndDuration>>(error.Join()));
}
private async Task<Validation<BaseError, RequestParameters>> Validate(GetLastPtsDuration request) =>
await ValidateFFprobePath()
.MapT(
ffprobePath => new RequestParameters(
request.FileName,
ffprobePath));
private async Task<Either<BaseError, PtsAndDuration>> Handle(RequestParameters parameters)
{
var startInfo = new ProcessStartInfo
{
FileName = parameters.FFprobePath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
startInfo.ArgumentList.Add("-v");
startInfo.ArgumentList.Add("0");
startInfo.ArgumentList.Add("-show_entries");
startInfo.ArgumentList.Add("packet=pts,duration");
startInfo.ArgumentList.Add("-of");
startInfo.ArgumentList.Add("compact=p=0:nk=1");
startInfo.ArgumentList.Add("-read_intervals");
startInfo.ArgumentList.Add("-999999");
startInfo.ArgumentList.Add(parameters.FileName);
var probe = new Process
{
StartInfo = startInfo
};
probe.Start();
return await probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, PtsAndDuration>>(
async output =>
{
await probe.WaitForExitAsync();
return probe.ExitCode == 0
? PtsAndDuration.From(output.Split("\n").Filter(s => !string.IsNullOrWhiteSpace(s)).Last().Trim())
: BaseError.New($"FFprobe at {parameters.FFprobePath} exited with code {probe.ExitCode}");
});
}
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(string FileName, string FFprobePath);
}
@@ -2,20 +2,15 @@
namespace ErsatzTV.Application.Streaming.Queries namespace ErsatzTV.Application.Streaming.Queries
{ {
public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest public record GetPlayoutItemProcessByChannelNumber(string ChannelNumber,
{ string Mode,
public GetPlayoutItemProcessByChannelNumber( DateTimeOffset Now,
string channelNumber, bool StartAtZero,
string mode, bool HlsRealtime,
DateTimeOffset now, long PtsOffset) : FFmpegProcessRequest(ChannelNumber,
bool startAtZero, Mode,
bool hlsRealtime) : base( Now,
channelNumber, StartAtZero,
mode, HlsRealtime,
now, PtsOffset);
startAtZero,
hlsRealtime)
{
}
}
} }
@@ -158,7 +158,8 @@ namespace ErsatzTV.Application.Streaming.Queries
request.HlsRealtime, request.HlsRealtime,
playoutItemWithPath.PlayoutItem.FillerKind, playoutItemWithPath.PlayoutItem.FillerKind,
playoutItemWithPath.PlayoutItem.InPoint, playoutItemWithPath.PlayoutItem.InPoint,
playoutItemWithPath.PlayoutItem.OutPoint); playoutItemWithPath.PlayoutItem.OutPoint,
request.PtsOffset);
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset); var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
@@ -193,7 +194,8 @@ namespace ErsatzTV.Application.Streaming.Queries
channel, channel,
maybeDuration, maybeDuration,
"Channel is Offline", "Channel is Offline",
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -212,7 +214,8 @@ namespace ErsatzTV.Application.Streaming.Queries
channel, channel,
maybeDuration, maybeDuration,
error.Value, error.Value,
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -231,7 +234,8 @@ namespace ErsatzTV.Application.Streaming.Queries
channel, channel,
maybeDuration, maybeDuration,
"Channel is Offline", "Channel is Offline",
request.HlsRealtime); request.HlsRealtime,
request.PtsOffset);
return new PlayoutItemProcessModel(errorProcess, finish); return new PlayoutItemProcessModel(errorProcess, finish);
} }
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
"ts", "ts",
DateTimeOffset.Now, DateTimeOffset.Now,
false, false,
true) true,
0)
{ {
Scheme = scheme; Scheme = scheme;
Host = host; Host = host;
@@ -6,7 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.3.0" /> <PackageReference Include="FluentAssertions" Version="6.4.0" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> <PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
@@ -16,7 +16,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" /> <PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" /> <PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
@@ -16,10 +16,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg
private readonly FFmpegPlaybackSettingsCalculator _calculator; private readonly FFmpegPlaybackSettingsCalculator _calculator;
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator(); public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
[Test]
public void Should_Not_GenPts_ForHlsSegmenter()
{
FFmpegProfile ffmpegProfile = TestProfile();
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile,
new MediaVersion(),
new MediaStream(),
new MediaStream(),
DateTimeOffset.Now,
DateTimeOffset.Now,
TimeSpan.Zero,
TimeSpan.Zero,
false);
actual.FormatFlags.Should().NotContain("+genpts");
}
[Test] [Test]
public void Should_UseSpecifiedThreadCount_ForTransportStream() public void Should_Not_UseSpecifiedThreadCount_ForTransportStream()
{ {
// MPEG-TS requires realtime output which is hardcoded to a single thread
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
@@ -31,18 +53,19 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(1);
} }
[Test] [Test]
public void Should_UseSpecifiedThreadCount_ForHttpLiveStreaming() public void Should_UseSpecifiedThreadCount_ForHttpLiveStreamingSegmenter()
{ {
FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 }; FFmpegProfile ffmpegProfile = TestProfile() with { ThreadCount = 7 };
FFmpegPlaybackSettings actual = _calculator.CalculateSettings( FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
StreamingMode.HttpLiveStreamingDirect, StreamingMode.HttpLiveStreamingSegmenter,
ffmpegProfile, ffmpegProfile,
new MediaVersion(), new MediaVersion(),
new MediaStream(), new MediaStream(),
@@ -50,7 +73,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ThreadCount.Should().Be(7); actual.ThreadCount.Should().Be(7);
} }
@@ -69,7 +93,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length); actual.FormatFlags.Count.Should().Be(expected.Length);
@@ -90,7 +115,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
string[] expected = { "+genpts", "+discardcorrupt", "+igndts" }; string[] expected = { "+genpts", "+discardcorrupt", "+igndts" };
actual.FormatFlags.Count.Should().Be(expected.Length); actual.FormatFlags.Count.Should().Be(expected.Length);
@@ -111,7 +137,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
} }
@@ -130,7 +157,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.RealtimeOutput.Should().BeTrue(); actual.RealtimeOutput.Should().BeTrue();
} }
@@ -151,7 +179,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now, now,
now.AddMinutes(5), now.AddMinutes(5),
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@@ -173,7 +202,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
now, now,
now.AddMinutes(5), now.AddMinutes(5),
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.StreamSeek.IsSome.Should().BeTrue(); actual.StreamSeek.IsSome.Should().BeTrue();
actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5)); actual.StreamSeek.IfNone(TimeSpan.Zero).Should().Be(TimeSpan.FromMinutes(5));
@@ -193,7 +223,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -219,7 +250,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -245,7 +277,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
} }
@@ -271,7 +304,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -299,7 +333,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -326,7 +361,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 }); IDisplaySize scaledSize = actual.ScaledSize.IfNone(new MediaVersion { Width = 0, Height = 0 });
scaledSize.Width.Should().Be(1280); scaledSize.Width.Should().Be(1280);
@@ -356,7 +392,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -384,7 +421,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -413,7 +451,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -445,7 +484,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -477,7 +517,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -508,7 +549,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -540,7 +582,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -574,7 +617,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -606,7 +650,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -637,7 +682,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -667,7 +713,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeTrue(); actual.PadToDesiredResolution.Should().BeTrue();
@@ -699,7 +746,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.ScaledSize.IsNone.Should().BeTrue(); actual.ScaledSize.IsNone.Should().BeTrue();
actual.PadToDesiredResolution.Should().BeFalse(); actual.PadToDesiredResolution.Should().BeFalse();
@@ -727,7 +775,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioCodec.Should().Be("aac"); actual.AudioCodec.Should().Be("aac");
} }
@@ -752,7 +801,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
} }
@@ -778,7 +828,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioCodec.Should().Be("aac"); actual.AudioCodec.Should().Be("aac");
} }
@@ -804,7 +855,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioCodec.Should().Be("copy"); actual.AudioCodec.Should().Be("copy");
} }
@@ -831,7 +883,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioBitrate.IfNone(0).Should().Be(2424); actual.AudioBitrate.IfNone(0).Should().Be(2424);
} }
@@ -858,7 +911,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioBufferSize.IfNone(0).Should().Be(2424); actual.AudioBufferSize.IfNone(0).Should().Be(2424);
} }
@@ -885,7 +939,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioChannels.IfNone(0).Should().Be(6); actual.AudioChannels.IfNone(0).Should().Be(6);
} }
@@ -912,7 +967,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioSampleRate.IfNone(0).Should().Be(48); actual.AudioSampleRate.IfNone(0).Should().Be(48);
} }
@@ -938,7 +994,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioChannels.IfNone(0).Should().Be(6); actual.AudioChannels.IfNone(0).Should().Be(6);
} }
@@ -964,7 +1021,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.AudioSampleRate.IfNone(0).Should().Be(48); actual.AudioSampleRate.IfNone(0).Should().Be(48);
} }
@@ -991,7 +1049,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromMinutes(2)); TimeSpan.FromMinutes(2),
false);
actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2)); actual.AudioDuration.IfNone(TimeSpan.MinValue).Should().Be(TimeSpan.FromMinutes(2));
} }
@@ -1017,7 +1076,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.NormalizeLoudness.Should().BeTrue(); actual.NormalizeLoudness.Should().BeTrue();
} }
@@ -1043,7 +1103,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.NormalizeLoudness.Should().BeFalse(); actual.NormalizeLoudness.Should().BeFalse();
} }
@@ -1071,7 +1132,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
DateTimeOffset.Now, DateTimeOffset.Now,
DateTimeOffset.Now, DateTimeOffset.Now,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv); actual.HardwareAcceleration.Should().Be(HardwareAccelerationKind.Qsv);
} }
@@ -214,7 +214,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false, false,
FillerKind.None, FillerKind.None,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.FromSeconds(5)); TimeSpan.FromSeconds(5),
0);
process.StartInfo.RedirectStandardError = true; process.StartInfo.RedirectStandardError = true;
@@ -79,7 +79,7 @@ namespace ErsatzTV.Core.Tests.Scheduling
playoutItems[2].FillerKind.Should().Be(FillerKind.None); playoutItems[2].FillerKind.Should().Be(FillerKind.None);
} }
[Test] [Test]
public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood() public void Should_Fill_Exactly_To_Next_Schedule_Item_Flood()
{ {
Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1)); Collection collectionOne = TwoItemCollection(1, 2, TimeSpan.FromHours(1));
+2 -2
View File
@@ -6,9 +6,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Flurl" Version="3.0.2" /> <PackageReference Include="Flurl" Version="3.0.4" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> <PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="MediatR" Version="9.0.0" /> <PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64"> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
@@ -12,7 +12,7 @@ namespace ErsatzTV.Core.FFmpeg
public List<string> FormatFlags { get; set; } public List<string> FormatFlags { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; } public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VideoDecoder { get; set; } public string VideoDecoder { get; set; }
public bool RealtimeOutput => true; public bool RealtimeOutput { get; set; }
public Option<TimeSpan> StreamSeek { get; set; } public Option<TimeSpan> StreamSeek { get; set; }
public Option<IDisplaySize> ScaledSize { get; set; } public Option<IDisplaySize> ScaledSize { get; set; }
public bool PadToDesiredResolution { get; set; } public bool PadToDesiredResolution { get; set; }
@@ -36,6 +36,12 @@ namespace ErsatzTV.Core.FFmpeg
"+igndts" "+igndts"
}; };
private static readonly List<string> SegmenterFormatFlags = new()
{
"+discardcorrupt",
"+igndts"
};
public FFmpegPlaybackSettings ConcatSettings => new() public FFmpegPlaybackSettings ConcatSettings => new()
{ {
ThreadCount = 1, ThreadCount = 1,
@@ -51,14 +57,26 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset start, DateTimeOffset start,
DateTimeOffset now, DateTimeOffset now,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint) TimeSpan outPoint,
bool hlsRealtime)
{ {
var result = new FFmpegPlaybackSettings var result = new FFmpegPlaybackSettings
{ {
ThreadCount = ffmpegProfile.ThreadCount, FormatFlags = streamingMode switch
FormatFlags = CommonFormatFlags {
StreamingMode.HttpLiveStreamingSegmenter => SegmenterFormatFlags,
_ => CommonFormatFlags,
},
RealtimeOutput = streamingMode switch
{
StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime,
_ => true
}
}; };
// always use one thread with realtime output
result.ThreadCount = result.RealtimeOutput ? 1 : ffmpegProfile.ThreadCount;
if (now != start || inPoint != TimeSpan.Zero) if (now != start || inPoint != TimeSpan.Zero)
{ {
result.StreamSeek = now - start + inPoint; result.StreamSeek = now - start + inPoint;
+18 -4
View File
@@ -21,6 +21,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@@ -379,8 +380,15 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add($"{format}"); _arguments.Add($"{format}");
return this; return this;
} }
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion) public FFmpegProcessBuilder WithInitialDiscontinuity()
{
_arguments.Add("-mpegts_flags");
_arguments.Add("+initial_discontinuity");
return this;
}
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion, long ptsOffset, Option<int> maybeTimeScale)
{ {
const int SEGMENT_SECONDS = 4; const int SEGMENT_SECONDS = 4;
@@ -405,10 +413,15 @@ namespace ErsatzTV.Core.FFmpeg
frameRate = fr; frameRate = fr;
} }
foreach (int timescale in maybeTimeScale)
{
_arguments.Add("-output_ts_offset");
_arguments.Add($"{(ptsOffset / (double)timescale).ToString(NumberFormatInfo.InvariantInfo)}");
}
_arguments.AddRange( _arguments.AddRange(
new[] new[]
{ {
"-use_wallclock_as_timestamps", "1",
"-g", $"{frameRate * SEGMENT_SECONDS}", "-g", $"{frameRate * SEGMENT_SECONDS}",
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}", "-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})", "-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
@@ -418,7 +431,8 @@ namespace ErsatzTV.Core.FFmpeg
"-segment_list_flags", "+live", "-segment_list_flags", "+live",
"-hls_segment_filename", "-hls_segment_filename",
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"), Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"),
"-hls_flags", "program_date_time+append_list+omit_endlist+independent_segments", "-hls_flags", "program_date_time+append_list+discont_start+omit_endlist+independent_segments",
"-mpegts_flags", "+initial_discontinuity",
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8") Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8")
}); });
+12 -9
View File
@@ -51,7 +51,8 @@ namespace ErsatzTV.Core.FFmpeg
bool hlsRealtime, bool hlsRealtime,
FillerKind fillerKind, FillerKind fillerKind,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint) TimeSpan outPoint,
long ptsOffset)
{ {
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion); Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
@@ -65,7 +66,8 @@ namespace ErsatzTV.Core.FFmpeg
start, start,
now, now,
inPoint, inPoint,
outPoint); outPoint,
hlsRealtime);
Option<WatermarkOptions> watermarkOptions = Option<WatermarkOptions> watermarkOptions =
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None); await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
@@ -157,11 +159,11 @@ namespace ErsatzTV.Core.FFmpeg
{ {
// HLS needs to segment and generate playlist // HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter: case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, videoVersion) return builder.WithHls(channel.Number, videoVersion, ptsOffset, playbackSettings.VideoTrackTimeScale)
.WithRealtimeOutput(hlsRealtime)
.Build(); .Build();
default: default:
return builder.WithFormat("mpegts") return builder.WithFormat("mpegts")
.WithInitialDiscontinuity()
.WithPipe() .WithPipe()
.Build(); .Build();
} }
@@ -172,7 +174,8 @@ namespace ErsatzTV.Core.FFmpeg
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
string errorMessage, string errorMessage,
bool hlsRealtime) bool hlsRealtime,
long ptsOffset)
{ {
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettings playbackSettings =
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile); _playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
@@ -220,8 +223,7 @@ namespace ErsatzTV.Core.FFmpeg
{ {
// HLS needs to segment and generate playlist // HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter: case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(channel.Number, None) return builder.WithHls(channel.Number, None, ptsOffset, playbackSettings.VideoTrackTimeScale)
.WithRealtimeOutput(hlsRealtime)
.Build(); .Build();
default: default:
return builder.WithFormat("mpegts") return builder.WithFormat("mpegts")
@@ -255,7 +257,7 @@ namespace ErsatzTV.Core.FFmpeg
.WithThreads(1) .WithThreads(1)
.WithQuiet() .WithQuiet()
.WithFormatFlags(playbackSettings.FormatFlags) .WithFormatFlags(playbackSettings.FormatFlags)
.WithRealtimeOutput(playbackSettings.RealtimeOutput) .WithRealtimeOutput(true)
.WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter") .WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter")
.WithMap("0") .WithMap("0")
.WithCopyCodec() .WithCopyCodec()
@@ -335,7 +337,8 @@ namespace ErsatzTV.Core.FFmpeg
DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch,
DateTimeOffset.UnixEpoch, DateTimeOffset.UnixEpoch,
TimeSpan.Zero, TimeSpan.Zero,
TimeSpan.Zero); TimeSpan.Zero,
false);
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger) FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, false, _logger)
.WithThreads(1) .WithThreads(1)
+6 -1
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Globalization;
using System.Text; using System.Text;
namespace ErsatzTV.Core.FFmpeg namespace ErsatzTV.Core.FFmpeg
@@ -55,7 +56,11 @@ namespace ErsatzTV.Core.FFmpeg
continue; continue;
} }
var duration = TimeSpan.FromSeconds(double.Parse(lines[i].TrimEnd(',').Split(':')[1])); var duration = TimeSpan.FromSeconds(
double.Parse(
lines[i].TrimEnd(',').Split(':')[1],
NumberStyles.Number,
CultureInfo.InvariantCulture));
if (currentTime < filterBefore) if (currentTime < filterBefore)
{ {
currentTime += duration; currentTime += duration;
+3
View File
@@ -18,9 +18,12 @@ namespace ErsatzTV.Core
Environment.SpecialFolderOption.Create), Environment.SpecialFolderOption.Create),
"etv-transcode"); "etv-transcode");
public static readonly string LogsFolder = Path.Combine(AppDataFolder, "logs");
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3"); public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3"); public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
public static readonly string LogFilePath = Path.Combine(LogsFolder, "ersatztv.log");
public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images"); public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources"); public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");
+1 -1
View File
@@ -22,7 +22,7 @@ namespace ErsatzTV.Core.Hdhr
public string URL => _channel.StreamingMode switch public string URL => _channel.StreamingMode switch
{ {
StreamingMode.HttpLiveStreamingDirect => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8", StreamingMode.TransportStream => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts?mode=ts-legacy",
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts" _ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
}; };
} }
@@ -27,14 +27,16 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
bool hlsRealtime, bool hlsRealtime,
FillerKind fillerKind, FillerKind fillerKind,
TimeSpan inPoint, TimeSpan inPoint,
TimeSpan outPoint); TimeSpan outPoint,
long ptsOffset);
Task<Process> ForError( Task<Process> ForError(
string ffmpegPath, string ffmpegPath,
Channel channel, Channel channel,
Option<TimeSpan> duration, Option<TimeSpan> duration,
string errorMessage, string errorMessage,
bool hlsRealtime); bool hlsRealtime,
long ptsOffset);
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
+1 -1
View File
@@ -45,7 +45,7 @@ namespace ErsatzTV.Core.Iptv
StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct", StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct",
StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter", StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter",
StreamingMode.TransportStreamHybrid => "ts", StreamingMode.TransportStreamHybrid => "ts",
_ => "ts?mode=legacy" _ => "ts?mode=ts-legacy"
}; };
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head(); string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();
@@ -72,10 +72,10 @@ namespace ErsatzTV.Core.Scheduling
itemDuration, itemDuration,
itemChapters); itemChapters);
// if the current time is before the next schedule item, but the current finish // if the next schedule item is supposed to start during this item,
// is after, we need to move on to the next schedule item // don't schedule this item and just move on
willFinishInTime = itemStartTime > peekScheduleItemStart || willFinishInTime = peekScheduleItemStart < itemStartTime ||
itemEndTimeWithFiller <= peekScheduleItemStart; peekScheduleItemStart >= itemEndTimeWithFiller;
if (willFinishInTime) if (willFinishInTime)
{ {
@@ -153,8 +153,10 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.AsNoTracking() .AsNoTracking()
.Filter(mm => ids.Contains(mm.MovieId)) .Filter(mm => ids.Contains(mm.MovieId))
.Include(mm => mm.Artwork) .Include(mm => mm.Artwork)
.OrderBy(mm => mm.SortTitle)
.Include(mm => mm.Movie) .Include(mm => mm.Movie)
.ThenInclude(m => m.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(mm => mm.SortTitle)
.ToListAsync(); .ToListAsync();
} }
@@ -133,6 +133,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Include(mvm => mvm.MusicVideo) .Include(mvm => mvm.MusicVideo)
.ThenInclude(mv => mv.Artist) .ThenInclude(mv => mv.Artist)
.ThenInclude(a => a.ArtistMetadata) .ThenInclude(a => a.ArtistMetadata)
.Include(mvm => mvm.MusicVideo)
.ThenInclude(e => e.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(mvm => mvm.Artwork) .Include(mvm => mvm.Artwork)
.OrderBy(mvm => mvm.SortTitle) .OrderBy(mvm => mvm.SortTitle)
.ToListAsync(); .ToListAsync();
@@ -100,6 +100,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Filter(ovm => ids.Contains(ovm.OtherVideoId)) .Filter(ovm => ids.Contains(ovm.OtherVideoId))
.Include(ovm => ovm.OtherVideo) .Include(ovm => ovm.OtherVideo)
.Include(ovm => ovm.Artwork) .Include(ovm => ovm.Artwork)
.Include(ovm => ovm.OtherVideo)
.ThenInclude(ov => ov.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(ovm => ovm.SortTitle) .OrderBy(ovm => ovm.SortTitle)
.ToListAsync(); .ToListAsync();
} }
@@ -113,6 +113,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
.Filter(ovm => ids.Contains(ovm.SongId)) .Filter(ovm => ids.Contains(ovm.SongId))
.Include(ovm => ovm.Song) .Include(ovm => ovm.Song)
.Include(ovm => ovm.Artwork) .Include(ovm => ovm.Artwork)
.Include(sm => sm.Song)
.ThenInclude(s => s.MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.OrderBy(ovm => ovm.SortTitle) .OrderBy(ovm => ovm.SortTitle)
.ToListAsync(); .ToListAsync();
} }
@@ -22,8 +22,8 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Refit" Version="6.1.15" /> <PackageReference Include="Refit" Version="6.2.16" />
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.1.15" /> <PackageReference Include="Refit.Newtonsoft.Json" Version="6.2.16" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
</ItemGroup> </ItemGroup>
@@ -11,7 +11,7 @@ namespace ErsatzTV.Infrastructure.Health.Checks
{ {
public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck
{ {
private const string BundledVersion = "N-105153-g8abc192236"; private const string BundledVersion = "N-105324-g0f5fd44dc9";
private readonly IConfigElementRepository _configElementRepository; private readonly IConfigElementRepository _configElementRepository;
public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository) public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository)
@@ -71,19 +71,15 @@ namespace ErsatzTV.Infrastructure.Health.Checks
private Option<HealthCheckResult> ValidateVersion(string version, string app) private Option<HealthCheckResult> ValidateVersion(string version, string app)
{ {
if (version.StartsWith("3.")) if (version.StartsWith("3.") || version.StartsWith("4."))
{ {
return FailResult($"{app} version {version} is too old; please install 4.4!"); return FailResult($"{app} version {version} is too old; please install 5.0!");
} }
if (version.StartsWith("4.3")) if (!version.StartsWith("5.0") && version != BundledVersion)
{ {
return WarningResult($"{app} version 4.4 is now supported, please upgrade from 4.3!"); return WarningResult(
} $"{app} version {version} is unexpected and may have problems; please install 5.0!");
if (!version.StartsWith("4.4") && version != BundledVersion)
{
return WarningResult($"{app} version {version} is unexpected and may have problems; please install 4.4!");
} }
return None; return None;
@@ -68,7 +68,7 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck
return WarningResult( return WarningResult(
$"There are {all.Count} files that do not exist on disk, including the following: {files}", $"There are {all.Count} files that do not exist on disk, including the following: {files}",
$"/search?query=state%3AFileNotFound"); "/media/trash");
} }
return OkResult(); return OkResult();
@@ -60,6 +60,21 @@ namespace ErsatzTV.Infrastructure.Search
return base.GetRangeQuery("release_date", "00000000", dateString, false, false); return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
} }
if (field == "added_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedStart))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", dateString, todayString, true, true);
}
if (field == "added_notinthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedFinish))
{
var dateString = addedFinish.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", "00000000", dateString, false, false);
}
return base.GetFieldQuery(field, queryText, slop); return base.GetFieldQuery(field, queryText, slop);
} }
@@ -57,6 +57,21 @@ namespace ErsatzTV.Infrastructure.Search
return base.GetRangeQuery("release_date", "00000000", dateString, false, false); return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
} }
if (field == "added_inthelast" && ParseStart(queryText, out DateTime addedStart))
{
var todayString = DateTime.Today.ToString("yyyyMMdd");
var dateString = addedStart.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", dateString, todayString, true, true);
}
if (field == "added_notinthelast" && ParseStart(queryText, out DateTime addedFinish))
{
var dateString = addedFinish.ToString("yyyyMMdd");
return base.GetRangeQuery("added_date", "00000000", dateString, false, false);
}
return base.GetFieldQuery(field, queryText, slop); return base.GetFieldQuery(field, queryText, slop);
} }
+18 -1
View File
@@ -43,6 +43,7 @@ namespace ErsatzTV.Infrastructure.Search
private const string TitleAndYearField = "title_and_year"; private const string TitleAndYearField = "title_and_year";
private const string JumpLetterField = "jump_letter"; private const string JumpLetterField = "jump_letter";
private const string ReleaseDateField = "release_date"; private const string ReleaseDateField = "release_date";
private const string AddedDateField = "added_date";
private const string StudioField = "studio"; private const string StudioField = "studio";
private const string LanguageField = "language"; private const string LanguageField = "language";
private const string StyleField = "style"; private const string StyleField = "style";
@@ -80,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Search
_initialized = false; _initialized = false;
} }
public int Version => 19; public int Version => 20;
public Task<bool> Initialize(ILocalFileSystem localFileSystem) public Task<bool> Initialize(ILocalFileSystem localFileSystem)
{ {
@@ -342,6 +343,8 @@ namespace ErsatzTV.Infrastructure.Search
Field.Store.NO)); Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
if (!string.IsNullOrWhiteSpace(metadata.Plot)) if (!string.IsNullOrWhiteSpace(metadata.Plot))
{ {
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
@@ -468,6 +471,8 @@ namespace ErsatzTV.Infrastructure.Search
Field.Store.NO)); Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
if (!string.IsNullOrWhiteSpace(metadata.Plot)) if (!string.IsNullOrWhiteSpace(metadata.Plot))
{ {
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO)); doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
@@ -558,6 +563,8 @@ namespace ErsatzTV.Infrastructure.Search
Field.Store.NO)); Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
foreach (TraktListItem item in season.TraktListItems) foreach (TraktListItem item in season.TraktListItems)
{ {
doc.Add(new StringField(TraktListField, item.TraktList.TraktId.ToString(), Field.Store.NO)); doc.Add(new StringField(TraktListField, item.TraktList.TraktId.ToString(), Field.Store.NO));
@@ -597,6 +604,8 @@ namespace ErsatzTV.Infrastructure.Search
List<string> languages = await searchRepository.GetLanguagesForArtist(artist); List<string> languages = await searchRepository.GetLanguagesForArtist(artist);
await AddLanguages(searchRepository, doc, languages); await AddLanguages(searchRepository, doc, languages);
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
foreach (Genre genre in metadata.Genres) foreach (Genre genre in metadata.Genres)
{ {
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO)); doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
@@ -659,6 +668,8 @@ namespace ErsatzTV.Infrastructure.Search
metadata.ReleaseDate.Value.ToString("yyyyMMdd"), metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO)); Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
if (!string.IsNullOrWhiteSpace(metadata.Album)) if (!string.IsNullOrWhiteSpace(metadata.Album))
{ {
@@ -748,6 +759,8 @@ namespace ErsatzTV.Infrastructure.Search
metadata.ReleaseDate.Value.ToString("yyyyMMdd"), metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
Field.Store.NO)); Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
if (!string.IsNullOrWhiteSpace(metadata.Plot)) if (!string.IsNullOrWhiteSpace(metadata.Plot))
{ {
@@ -827,6 +840,8 @@ namespace ErsatzTV.Infrastructure.Search
{ {
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
foreach (Tag tag in metadata.Tags) foreach (Tag tag in metadata.Tags)
{ {
@@ -871,6 +886,8 @@ namespace ErsatzTV.Infrastructure.Search
{ {
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO)); doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
} }
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
if (!string.IsNullOrWhiteSpace(metadata.Album)) if (!string.IsNullOrWhiteSpace(metadata.Album))
{ {
+2 -2
View File
@@ -27,14 +27,14 @@ namespace ErsatzTV.Controllers
public Task<IActionResult> GetConcatPlaylist(string channelNumber) => public Task<IActionResult> GetConcatPlaylist(string channelNumber) =>
_mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)) _mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
.ToActionResult(); .ToActionResult();
[HttpGet("ffmpeg/stream/{channelNumber}")] [HttpGet("ffmpeg/stream/{channelNumber}")]
public Task<IActionResult> GetStream( public Task<IActionResult> GetStream(
string channelNumber, string channelNumber,
[FromQuery] [FromQuery]
string mode = "mixed") => string mode = "mixed") =>
_mediator.Send( _mediator.Send(
new GetPlayoutItemProcessByChannelNumber(channelNumber, mode, DateTimeOffset.Now, false, true)) new GetPlayoutItemProcessByChannelNumber(channelNumber, mode, DateTimeOffset.Now, false, true, 0))
.Map( .Map(
result => result =>
result.Match<IActionResult>( result.Match<IActionResult>(
+1 -1
View File
@@ -59,7 +59,7 @@ namespace ErsatzTV.Controllers
{ {
FFmpegProcessRequest request = mode switch FFmpegProcessRequest request = mode switch
{ {
"legacy" => new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber), "ts-legacy" => new GetConcatProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber),
_ => new GetWrappedProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber) _ => new GetWrappedProcessByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber)
}; };
+14 -8
View File
@@ -8,18 +8,17 @@
<ItemGroup> <ItemGroup>
<Folder Include="Resources" /> <Folder Include="Resources" />
<Folder Include="wwwroot\css\bootstrap" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" /> <PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
<PackageReference Include="FluentValidation" Version="10.3.6" /> <PackageReference Include="FluentValidation" Version="10.3.6" />
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" /> <PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
<PackageReference Include="HtmlSanitizer" Version="6.0.453" /> <PackageReference Include="HtmlSanitizer" Version="7.1.475" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> <PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="Markdig" Version="0.26.0" /> <PackageReference Include="Markdig" Version="0.27.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="4.0.0" /> <PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@@ -29,10 +28,10 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MudBlazor" Version="6.0.4" /> <PackageReference Include="MudBlazor" Version="6.0.5" />
<PackageReference Include="NaturalSort.Extension" Version="3.2.0" /> <PackageReference Include="NaturalSort.Extension" Version="3.2.0" />
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" /> <PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="6.1.15" /> <PackageReference Include="Refit.HttpClientFactory" Version="6.2.16" />
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
@@ -59,4 +58,11 @@
<EmbeddedResource Include="Resources\ISO-639-2_utf-8.txt" /> <EmbeddedResource Include="Resources\ISO-639-2_utf-8.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="..\artwork\Ersatztv.icns">
<Link>Ersatztv.icns</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>
+28 -10
View File
@@ -1,6 +1,5 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@@ -12,14 +11,31 @@ namespace ErsatzTV
{ {
public class Program public class Program
{ {
private static IConfiguration Configuration { get; } = new ConfigurationBuilder() private static readonly string BasePath;
.SetBasePath(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location))
.AddJsonFile("appsettings.json", false, true) static Program()
.AddJsonFile( {
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", string executablePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
true) string executable = Path.GetFileNameWithoutExtension(executablePath);
.AddEnvironmentVariables()
.Build(); IConfigurationBuilder builder = new ConfigurationBuilder();
BasePath = Path.GetDirectoryName(
"dotnet".Equals(executable, StringComparison.InvariantCultureIgnoreCase)
? typeof(Program).Assembly.Location
: executablePath);
Configuration = builder
.SetBasePath(BasePath)
.AddJsonFile("appsettings.json", false, true)
.AddJsonFile(
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json",
true)
.AddEnvironmentVariables()
.Build();
}
private static IConfiguration Configuration { get; }
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
@@ -27,6 +43,7 @@ namespace ErsatzTV
.ReadFrom.Configuration(Configuration) .ReadFrom.Configuration(Configuration)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1)) .WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
.WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day)
.CreateLogger(); .CreateLogger();
try try
@@ -50,7 +67,8 @@ namespace ErsatzTV
.ConfigureWebHostDefaults( .ConfigureWebHostDefaults(
webBuilder => webBuilder.UseStartup<Startup>() webBuilder => webBuilder.UseStartup<Startup>()
.UseConfiguration(Configuration) .UseConfiguration(Configuration)
.UseKestrel(options => options.AddServerHeader = false)) .UseKestrel(options => options.AddServerHeader = false)
.UseContentRoot(BasePath))
.UseSerilog(); .UseSerilog();
} }
} }
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
style="enable-background:new 0 0 500 500;" xml:space="preserve">
<style type="text/css">
.st0{fill:#C0C000;}
.st1{font-family:'Roboto-Bold';}
.st2{font-size:144.6417px;}
.st3{fill:#00C0C0;}
.st4{font-size:312.4454px;}
.st5{fill:#00C000;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" fit-margin-bottom="0" fit-margin-left="0" fit-margin-right="0" fit-margin-top="0" id="base" inkscape:current-layer="layer1" inkscape:cx="199.98633" inkscape:cy="92.251514" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1364" inkscape:window-maximized="1" inkscape:window-width="3408" inkscape:window-x="14" inkscape:window-y="36" inkscape:zoom="0.49497475" pagecolor="#ffffff" showgrid="false">
</sodipodi:namedview>
<g>
<text transform="matrix(1 0 0 1 47.5005 181.2363)" class="st0 st1 st2">Ersatz</text>
<text transform="matrix(1 0 0 1 53.6772 427.7119)"><tspan x="0" y="0" class="st3 st1 st4">T</tspan><tspan x="193.3" y="0" class="st5 st1 st4">V</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+2 -1
View File
@@ -1,6 +1,6 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime
FROM jasongdove/ffmpeg:4.4-ubuntu2004 AS runtime-base FROM jasongdove/ffmpeg:5.0-ubuntu2004 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata
@@ -13,6 +13,7 @@ WORKDIR /source
COPY *.sln . COPY *.sln .
COPY nuget.config . COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/ COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/ COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/ COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/ COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
+2 -1
View File
@@ -7,6 +7,7 @@ WORKDIR /source
COPY *.sln . COPY *.sln .
COPY nuget.config . COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/ COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/ COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/ COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/ COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
@@ -25,7 +26,7 @@ ARG INFO_VERSION="unknown"
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION} RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image # final stage/image
FROM jasongdove/ffmpeg:4.4-nvidia2004 AS runtime-base FROM jasongdove/ffmpeg:5.0-nvidia2004 AS runtime-base
WORKDIR /app WORKDIR /app
EXPOSE 8409 EXPOSE 8409
COPY --from=build /app ./ COPY --from=build /app ./
+1 -1
View File
@@ -1,6 +1,6 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime
FROM jasongdove/ffmpeg-base:4.4-nvidia2004 AS runtime-base FROM jasongdove/ffmpeg-base:5.0-nvidia2004 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update \ RUN apt-get update \
&& DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata \ && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev tzdata \
+2 -1
View File
@@ -7,6 +7,7 @@ WORKDIR /source
COPY *.sln . COPY *.sln .
COPY nuget.config . COPY nuget.config .
COPY lib/nuget/* ./lib/nuget/ COPY lib/nuget/* ./lib/nuget/
COPY artwork/* ./artwork/
COPY ErsatzTV/*.csproj ./ErsatzTV/ COPY ErsatzTV/*.csproj ./ErsatzTV/
COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/ COPY ErsatzTV.Application/*.csproj ./ErsatzTV.Application/
COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/ COPY ErsatzTV.Core/*.csproj ./ErsatzTV.Core/
@@ -25,7 +26,7 @@ ARG INFO_VERSION="unknown"
RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION} RUN dotnet publish -c release -o /app -r linux-x64 --self-contained false --no-restore /p:DebugType=Embedded /p:InformationalVersion=${INFO_VERSION}
# final stage/image # final stage/image
FROM jasongdove/ffmpeg:4.4-vaapi2004 AS runtime-base FROM jasongdove/ffmpeg:5.0-vaapi2004 AS runtime-base
WORKDIR /app WORKDIR /app
EXPOSE 8409 EXPOSE 8409
COPY --from=build /app ./ COPY --from=build /app ./
+1 -1
View File
@@ -1,6 +1,6 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime FROM mcr.microsoft.com/dotnet/aspnet:6.0-focal-amd64 AS dotnet-runtime
FROM jasongdove/ffmpeg-base:4.4-vaapi2004 AS runtime-base FROM jasongdove/ffmpeg-base:5.0-vaapi2004 AS runtime-base
COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet COPY --from=dotnet-runtime /usr/share/dotnet /usr/share/dotnet
RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev \ RUN apt-get update && DEBIAN_FRONTEND="noninteractive" apt-get install -y libicu-dev \
tzdata \ tzdata \
+3
View File
@@ -1,6 +1,9 @@
ErsatzTV needs to know about your media items in order to create channels. ErsatzTV needs to know about your media items in order to create channels.
Four library kinds are currently supported: [Local](#local-libraries), [Plex](#plex-libraries), [Jellyfin](#jellyfin-libraries) and [Emby](#emby-libraries). Four library kinds are currently supported: [Local](#local-libraries), [Plex](#plex-libraries), [Jellyfin](#jellyfin-libraries) and [Emby](#emby-libraries).
ErsatzTV supports a subset of media kinds when synchronizing from media servers, i.e. [Plex](#plex-libraries), [Jellyfin](#jellyfin-libraries) and [Emby](#emby-libraries) only support `Movies` and `Shows`.
[Local Libraries](#local-libraries) support all media kinds: `Movies`, `Shows`, `Music Videos`, `Other Videos` and `Songs`.
## Local Libraries ## Local Libraries
Local libraries provide a way to schedule media directly from folders; no media server is necessary. Local libraries provide a way to schedule media directly from folders; no media server is necessary.
+11 -6
View File
@@ -82,23 +82,28 @@ docker run -d \
7. Open your browser to `http://[docker-ip]:8409` (First run may take a few minutes to be available.) 7. Open your browser to `http://[docker-ip]:8409` (First run may take a few minutes to be available.)
## Development Builds
Development builds for all supported architectures can be found on the [develop release](https://github.com/jasongdove/ErsatzTV/releases/tag/develop).
As with the docker development tags, the development builds are updated with every push to the main branch and have the potential to be less stable than releases.
## Windows ## Windows
### Manual Installation ### Manual Installation
1. Create a folder `ersatztv` at your preferred install location. 1. Create a folder `ersatztv` at your preferred install location.
2. Download and extract the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases) to the `ersatztv` folder. 2. Download and extract the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases) to the `ersatztv` folder.
3. Run `ErsatzTV.exe` 3. Run `ErsatzTV-Windows.exe`
4. Open your browser to `http://[server-ip]:8409` 4. Use the tray menu to open the UI, view logs or exit
## MacOS ## MacOS
### Manual Installation ### Manual Installation
1. Create a folder `ersatztv` at your preferred install location. 1. Download the .dmg from the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases)
2. Download and extract the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases) to the `ersatztv` folder. 2. Open the .dmg and drag `ErsatzTV` to the `Applications` folder
3. Run `ErsatzTV` 3. Double-click the `ErsatzTV` application
4. Open your browser to `http://[server-ip]:8409` 4. Use the tray menu to open the UI, view logs or exit
## Linux ## Linux
+9
View File
@@ -24,6 +24,7 @@ The following fields are available for searching movies:
- `content_rating`: The movie content rating (case-sensitive) - `content_rating`: The movie content rating (case-sensitive)
- `language`: The movie audio stream language - `language`: The movie audio stream language
- `release_date`: The movie release date (YYYYMMDD) - `release_date`: The movie release date (YYYYMMDD)
- `added_date`: The date the movie was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the movie in minutes - `minutes`: the rounded-up whole number duration of the movie in minutes
- `type`: Always `movie` - `type`: Always `movie`
@@ -41,6 +42,7 @@ The following fields are available for searching shows:
- `content_rating`: The show content rating (case-sensitive) - `content_rating`: The show content rating (case-sensitive)
- `language`: The show audio stream language - `language`: The show audio stream language
- `release_date`: The show release date (YYYYMMDD) - `release_date`: The show release date (YYYYMMDD)
- `added_date`: The date the show was added to ErsatzTV (YYYYMMDD)
- `type`: Always `show` - `type`: Always `show`
### Episodes ### Episodes
@@ -54,6 +56,7 @@ The following fields are available for searching episodes:
- `library_name`: The name of the library that contains the episode - `library_name`: The name of the library that contains the episode
- `language`: The episode audio stream language - `language`: The episode audio stream language
- `release_date`: The episode release date (YYYYMMDD) - `release_date`: The episode release date (YYYYMMDD)
- `added_date`: The date the episode was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the episode in minutes - `minutes`: the rounded-up whole number duration of the episode in minutes
- `type`: Always `episode` - `type`: Always `episode`
@@ -65,6 +68,7 @@ The following fields are available for searching artists:
- `genre`: The artist genre - `genre`: The artist genre
- `style`: The artist style - `style`: The artist style
- `mood`: The artist mood - `mood`: The artist mood
- `added_date`: The date the artist was added to ErsatzTV (YYYYMMDD)
- `library_name`: The name of the library that contains the artist - `library_name`: The name of the library that contains the artist
- `type`: Always `artist` - `type`: Always `artist`
@@ -79,6 +83,7 @@ The following fields are available for searching music videos:
- `library_name`: The name of the library that contains the music video - `library_name`: The name of the library that contains the music video
- `language`: The music video audio stream language - `language`: The music video audio stream language
- `release_date`: The music video release date (YYYYMMDD) - `release_date`: The music video release date (YYYYMMDD)
- `added_date`: The date the music video was added to ErsatzTV (YYYYMMDD)
- `minutes`: the rounded-up whole number duration of the music video in minutes - `minutes`: the rounded-up whole number duration of the music video in minutes
- `type`: Always `music_video` - `type`: Always `music_video`
@@ -89,6 +94,7 @@ The following fields are available for searching other videos:
- `title`: The filename of the video (without extension) - `title`: The filename of the video (without extension)
- `tag`: All of the video's parent folders - `tag`: All of the video's parent folders
- `minutes`: the rounded-up whole number duration of the video in minutes - `minutes`: the rounded-up whole number duration of the video in minutes
- `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD)
- `type`: Always `other_video` - `type`: Always `other_video`
### Songs ### Songs
@@ -101,6 +107,7 @@ The following fields are available for searching songs:
- `genre`: The song genre - `genre`: The song genre
- `tag`: All of the song's parent folders - `tag`: All of the song's parent folders
- `minutes`: the rounded-up whole number duration of the song in minutes - `minutes`: the rounded-up whole number duration of the song in minutes
- `added_date`: The date the song was added to ErsatzTV (YYYYMMDD)
- `type`: Always `song` - `type`: Always `song`
## Special Search Fields ## Special Search Fields
@@ -108,6 +115,8 @@ The following fields are available for searching songs:
- `released_inthelast`: For any media type that supports `release_date`, `released_inthelast` takes a number and a unit (days, weeks, months, years) and returns items released between the specified time ago and now - `released_inthelast`: For any media type that supports `release_date`, `released_inthelast` takes a number and a unit (days, weeks, months, years) and returns items released between the specified time ago and now
- `released_notinthelast`: For any media type that supports `release_date`, `released_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items released before the specified time ago - `released_notinthelast`: For any media type that supports `release_date`, `released_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items released before the specified time ago
- `released_onthisday`: For any media type that supports `release_date`, `released_onthisday` takes any value (ignored) and will return items released on this month number and day number in previous years - `released_onthisday`: For any media type that supports `release_date`, `released_onthisday` takes any value (ignored) and will return items released on this month number and day number in previous years
- `added_inthelast`: For any media type that supports `added_date`, `added_inthelast` takes a number and a unit (days, weeks, months, years) and returns items added to ErsatzTV between the specified time ago and now
- `added_notinthelast`: For any media type that supports `added_date`, `added_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items added to ErsatzTV before the specified time ago
## Sample Searches ## Sample Searches
+11
View File
@@ -0,0 +1,11 @@
{
"notarize": [{
"path": "./ErsatzTV.dmg",
"bundle_id": "com.jasongdove.ersatztv",
"staple": true
}],
"apple_id": {
"password": "@env:AC_PASSWORD"
}
}
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+19
View File
@@ -0,0 +1,19 @@
#! /bin/bash
SCRIPT_FOLDER=$(dirname ${BASH_SOURCE[0]})
REPO_ROOT=$(realpath "$SCRIPT_FOLDER/../..")
APP_NAME="$REPO_ROOT/ErsatzTV.app"
PUBLISH_OUTPUT_DIRECTORY="$REPO_ROOT/publish/."
if [ -d "$APP_NAME" ]
then
rm -rf "$APP_NAME"
fi
cd "$REPO_ROOT/ErsatzTV-macOS" || exit
xcodebuild build
cd "$REPO_ROOT" || exit
cp -R "$REPO_ROOT/ErsatzTV-macOS/build/Release/ErsatzTV-macOS.app" "$APP_NAME"
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"
+10
View File
@@ -0,0 +1,10 @@
#! /bin/bash
SCRIPT_FOLDER=$(dirname ${BASH_SOURCE[0]})
REPO_ROOT="$SCRIPT_FOLDER/../.."
DMG_NAME="$REPO_ROOT/ErsatzTV.dmg"
ENTITLEMENTS="$SCRIPT_FOLDER/ErsatzTV.entitlements"
SIGNING_IDENTITY="C3BBCFB2D6851FF0DCA6CAC06A3EF1ECE71F9FFF"
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$DMG_NAME"
+19
View File
@@ -0,0 +1,19 @@
#! /bin/bash
SCRIPT_FOLDER=$(dirname ${BASH_SOURCE[0]})
REPO_ROOT="$SCRIPT_FOLDER/../.."
APP_NAME="$REPO_ROOT/ErsatzTV.app"
ENTITLEMENTS="$SCRIPT_FOLDER/ErsatzTV.entitlements"
SIGNING_IDENTITY="C3BBCFB2D6851FF0DCA6CAC06A3EF1ECE71F9FFF"
find "$APP_NAME/Contents/MacOS/"|while read fname; do
if [[ -f $fname ]]; then
echo "[INFO] Signing $fname"
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$fname"
fi
done
echo "[INFO] Signing app file"
codesign --force --timestamp --options=runtime --entitlements "$ENTITLEMENTS" --sign "$SIGNING_IDENTITY" "$APP_NAME"