Compare commits

...

109 Commits

Author SHA1 Message Date
Jason Dove
082bc6145c update changelog for release v0.6.1-beta [no ci] 2022-06-03 15:05:06 -05:00
Jason Dove
bf3f16451b music video credits tweaks (#834)
* fix song subtitles

* always use generated subtitles

* file not found/unavailable fixes
2022-06-03 14:44:52 -05:00
Jake
3cb37003cb UI rewrite - ffmpeg profiles (#823)
* ffmpeg profile functionality, sweetalert2

* add new files

* cleanup controller; remove unused classes

* apply formatting

* cleanup core project

* don't use bom

* whitespace

* remove generated css

* remove generated js/map

* Remove attempted linter fix, channels button, watermarks page. Fixed handlerror.

* Changed deleted confirmation to toast.

* Localized strings for language change. Modified Action icons to buttons and left default sizes. Changed Cancel to No where Yes is an option

* lint

Co-authored-by: Jason Dove <jason@jasondove.me>
2022-06-03 06:28:32 -05:00
Jason Dove
9acfd2cd06 fix plex server identification (#833) 2022-06-03 05:53:41 -05:00
dependabot[bot]
3242e7ebb8 Bump HtmlSanitizer from 7.1.488 to 7.1.509 (#830)
Bumps [HtmlSanitizer](https://github.com/mganss/HtmlSanitizer) from 7.1.488 to 7.1.509.
- [Release notes](https://github.com/mganss/HtmlSanitizer/releases)
- [Commits](https://github.com/mganss/HtmlSanitizer/compare/v7.1.488...v7.1.509)

---
updated-dependencies:
- dependency-name: HtmlSanitizer
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-02 21:21:17 -05:00
Jason Dove
7644d628e7 generate music video credits (#832) 2022-06-02 20:50:55 -05:00
Jason Dove
b4f19e6de4 fix jellyfin tv paging (#831) 2022-06-02 12:16:59 -05:00
Jason Dove
0388425763 update changelog for release v0.6.0-beta [no ci] 2022-06-01 18:01:07 -05:00
Jason Dove
ca5d303ac7 fix qsv encoder regression and memory errors (#828)
* fix qsv encoders; only use 64 extra hw frames

* update changelog
2022-05-31 06:07:50 -05:00
Jason Dove
18e66a92ad add paging to media server show and collection calls (#827)
* add paging to media server show library calls

* add paging to media server season and episode library calls

* formatting

* add paging to media server collection calls

* add paging to media server collection item calls

* update changelog
2022-05-31 05:56:48 -05:00
Jason Dove
7d0a56ab98 disable lower-power mode for qsv encoders (#826) 2022-05-29 20:44:27 -05:00
Jason Dove
5069792d12 update dependencies 2022-05-28 20:41:54 -05:00
Jason Dove
c9789458b9 page media server movie libraries 2022-05-28 20:41:22 -05:00
Jason Dove
777a0d09ed hls segmenter fixes (#824)
* fix pts warning when channel first starts streaming

* rework playlist filtering
2022-05-25 21:05:55 -05:00
Jason Dove
4e2ebcc48a fix watermark opacity filter (#820) 2022-05-23 10:29:04 -05:00
Jason Dove
90fe1d7709 fix hw accel health check for qsv in vaapi docker (#818) 2022-05-22 19:42:14 -05:00
Jason Dove
1576dd026e enable qsv accel for vaapi docker images (#817) 2022-05-22 18:43:24 -05:00
Jason Dove
ee7a64eea9 fix other video libraries (#816)
* update depdendencies

* reset other video libraries
2022-05-22 12:29:35 -05:00
Jason Dove
9742e1eef7 update changelog for release v0.5.8-beta [no ci] 2022-05-20 09:11:16 -05:00
Jason Dove
a61c4b3472 fix a handful of scheduling edge cases (#814) 2022-05-18 06:00:58 -05:00
Jason Dove
ea0d43cf99 use hardware acceleration for error messages (#813)
* logging fixes

* use hardware acceleration for error messages
2022-05-18 05:44:01 -05:00
Jason Dove
fd36ea51a7 unlock ffmpeg thread count (#812)
* revert logging changes

* unlock ffmpeg thread count
2022-05-17 21:37:44 -05:00
Jason Dove
5213b71d62 add debug logging to track down playlist filtering issue (#811)
* add debug logging to track down playlist filtering issue

* revert work-ahead change
2022-05-17 15:18:12 -05:00
Jason Dove
0ba3ac7f50 fix more error stream settings (#810) 2022-05-17 11:44:12 -05:00
Jason Dove
d960fec734 error stream needs video track timescale (#809) 2022-05-17 10:26:46 -05:00
Jason Dove
f272036c6f reduce hls disk use (#808)
* reduce hls segmenter disk use

* logging improvements

* update dependencies
2022-05-17 08:43:28 -05:00
Jason Dove
9fe03b6ef3 reduce hls segmenter disk use (#806) 2022-05-16 21:45:13 -05:00
Jason Dove
f895ab5304 fix nuget versions 2022-05-14 06:37:22 -05:00
Jason Dove
07c54ff45f update changelog for release v0.5.7-beta [no ci] 2022-05-14 05:54:27 -05:00
Jason Dove
6a29ce2049 update dependencies (#805) 2022-05-13 21:16:43 -05:00
Jason Dove
d19e95fb38 add random start point option (#804) 2022-05-13 20:36:03 -05:00
Jason Dove
d78daf8735 fix flood checkpoints (#803) 2022-05-13 15:31:04 -05:00
Jason Dove
4f6522379d fix custom title scheduling (#802) 2022-05-13 13:06:05 -05:00
Jason Dove
9e0972fec0 properly ignore plex other videos libraries (#801) 2022-05-13 12:31:34 -05:00
Jason Dove
6d564233ed filler scheduling fix (#800) 2022-05-12 14:02:06 -05:00
Jason Dove
47252b1243 read track from music video nfo metadata (#799) 2022-05-12 12:31:40 -05:00
Jason Dove
bd5b52922d add option to allow watermarks over filler (#796) 2022-05-09 17:51:11 -05:00
Jason Dove
59c793b9be add option to skip missing items in playouts (#795) 2022-05-09 09:21:51 -05:00
Jason Dove
3ad1ba01f8 add autocomplete to search bar (#791) 2022-05-08 19:58:15 -05:00
Jason Dove
ab10f0ed81 add metadata_kind to search index (#790)
* more nfo cleanup

* add metadata_kind to search index
2022-05-07 21:24:50 -05:00
Jason Dove
44dd68fe59 nfo and memory fixes (#789)
* partial episode nfo metadata

* nfo metadata reliability fixes

* use recyclable memory streams
2022-05-07 20:32:57 -05:00
Jason Dove
6326189444 update changelog for release v0.5.6-beta [no ci] 2022-05-06 12:42:57 -05:00
Jason Dove
198c693208 fix other video fallback metadata 2022-05-05 20:51:24 -05:00
Jason Dove
1431b33a98 support movie nfo metadata in other video libraries (#788)
* add other video nfo metadata

* update docs
2022-05-05 20:38:23 -05:00
Jason Dove
e81a8e58ea fix error continuity (#787)
* fix fallback filler playback

* use new transcoder logic for errors

* use realtime option for error streams
2022-05-05 13:31:09 -05:00
Jason Dove
daf7114ce2 bug fixes and logging (#786) 2022-05-05 10:04:24 -05:00
Jason Dove
8542bc20b1 update dependencies (#785) 2022-05-04 20:45:19 -05:00
Jason Dove
9decb91bf7 use aired for music video release date (#784) 2022-05-04 11:36:00 -05:00
Jason Dove
fcfd579b37 fix search index validation (#782) 2022-05-03 22:26:44 -05:00
Jason Dove
e9be182bed bug fixes and search tweaks (#781)
* fix movie nfo processing

* fix local movie fallback metadata

* use imagesharp again

* fix search edge case

* add show_genre and show_tag to search index
2022-05-03 21:58:39 -05:00
Jason Dove
610e261cd7 update changelog again [no ci] 2022-05-03 10:23:57 -05:00
Jason Dove
f65818c838 update changelog for release v0.5.5-beta [no ci] 2022-05-03 10:22:10 -05:00
Jason Dove
1651d2895e update dependencies 2022-05-03 09:45:25 -05:00
Jason Dove
b90c536dcb try to fix quemu condition 2022-05-03 09:33:02 -05:00
Jason Dove
5c98eb3df5 more conditions 2022-05-02 22:46:22 -05:00
Jason Dove
bdff5eba75 fix conditions 2022-05-02 22:41:11 -05:00
Jason Dove
7d112eda05 fix tag 2022-05-02 22:28:54 -05:00
Jason Dove
4f16431ca0 use specific arm64v8 tags 2022-05-02 22:14:49 -05:00
Jason Dove
69b39c6940 try building arm64 docker image 2022-05-02 22:10:50 -05:00
Jason Dove
fe7181ea1d workflow fixes 2022-05-02 21:47:37 -05:00
Jason Dove
88b287a094 use matrix for docker build workflow 2022-05-02 21:44:35 -05:00
Jason Dove
7953e3ad85 actually fix windows tests [no ci] 2022-05-02 13:41:05 -05:00
Jason Dove
8ba6374165 music video nfo multiple artists (#780)
* support multiple artist entries in music video nfo metadata

* clean up other video and song etags
2022-05-02 12:32:15 -05:00
Jason Dove
973dd4b53d try to fix tests on windows again [no ci] 2022-05-02 05:54:56 -05:00
Jason Dove
6facd745ec fix extracting embedded mov_text subtitles (#777)
* fix extracting embedded mov_text subtitles

* changelog

* cleanup
2022-05-01 21:24:14 -05:00
Jason Dove
32c4c4ec8b fix failing tests on windows [no ci] 2022-05-01 14:11:55 -05:00
Jason Dove
ecb6ed37f0 more local metadata parsing improvements (#776)
* extract remaining nfo xml serializers

* add artist nfo tests

* add music video nfo tests

* add tv show nfo reader tests

* custom artist nfo reader

* custom music video nfo reader

* custom tv show nfo reader

* local metadata parsing cleanup

* update changelog
2022-05-01 14:00:10 -05:00
Jason Dove
2a8bf57930 ignore emby and jellyfin path infos with unset network path (#775) 2022-04-30 21:48:37 -05:00
Jason Dove
1ebc4b62e3 bug fixes (#774)
* add custom movie metadata parsing

* refactor episode nfo reader

* fix emby and jellyfin bugs
2022-04-30 17:39:47 -05:00
Jason Dove
4ae671b633 fix trashing episodes with no title (#773) 2022-04-29 21:49:23 -05:00
Jason Dove
87aa69f4cc update changelog for release v0.5.4-beta [no ci] 2022-04-29 17:59:12 -05:00
Jason Dove
404ea49e35 jellyfin and emby path infos (#771)
* start adding jellyfin path info; fix some scanning bugs

* sync jellyfin libraries before scanning to ensure latest path infos

* code cleanup

* support emby path infos

* fix periodic scanning of emby and jellyfin libraries

* bug fixes
2022-04-29 15:07:17 -05:00
Jason Dove
4ed40acfbe rebuild corrupt search index (#770) 2022-04-28 13:49:06 -05:00
Jason Dove
17f540dc99 add more search fields (#769) 2022-04-28 10:40:27 -05:00
Jason Dove
780ebc01ee add v2 ffmpeg profile page (#768)
* wip

* remove transcode property; use i18n

* add api

* use computed table headers for i18n
2022-04-28 06:56:01 -05:00
Jason Dove
0a0fb71b94 refactor plex, emby and jellyfin television scanners (#767)
* refactor plex television scanner

* refactor emby television scanner

* refactor jellyfin television scanner

* update changelog
2022-04-27 22:34:25 -05:00
Jason Dove
53d6ecae8d fix windows build 2022-04-27 14:20:14 -05:00
Jason Dove
837f311ec0 add more search fields (#766)
* properly index show and season state

* add height, width, season_number, episode_number to search index

* update docs
2022-04-27 13:58:33 -05:00
Jason Dove
a9a89d04ea optimize search-index rebuilding (#765)
* update dependencies

* optimize search-index rebuilding

* cleanup logging
2022-04-27 12:23:37 -05:00
Rafael Vieira
2e1073eb53 Add support to internationalization (#764)
* client-app: Improve development documentation

* client-app: add basic support to translation

* client-app: fix i18n and create lang state

* client-app: add language selector

* client-app: add translation EN and PT-BR
2022-04-27 10:58:27 -05:00
Jason Dove
7687278b80 health check fixes (#763) 2022-04-26 09:38:03 -05:00
Jason Dove
392aebd46f refactor movie library scanners (#761)
* catch health check cancellation

* local library scanner cleanup

* emby and jf library scanner cleanup

* rework emby movie library scanner

* refactor emby movie library scanner

* refactor jellyfin movie library scanner

* clear etag until after new media has been processed

* refactor plex movie library scanner

* update changelog
2022-04-25 20:31:12 -05:00
Jason Dove
0a4f6d9b62 update changelog for release v0.5.3-beta [no ci] 2022-04-24 13:45:29 -05:00
Jason Dove
d879ce0d0d bug fixes (#757)
* fix docker blur hash generation

* scanner async cleanup

* catch and log some unauthorized exception errors
2022-04-24 13:43:06 -05:00
Jason Dove
558e8acf5f unavailable improvements (#756)
* add unavailable health check

* improve file not found health check
2022-04-24 11:59:41 -05:00
Jason Dove
89a2ac9455 add unavailable media state for plex media (#754)
* rework plex movie library scanner; add unavailable media item state

* plex television scanner improvements

* reset plex etags as needed

* update changelog
2022-04-23 22:19:10 -05:00
Jason Dove
39c05a24d8 update changelog for release v0.5.2-beta [no ci] 2022-04-22 19:33:42 -05:00
Jason Dove
78383bd5fa override languages and subtitles on schedule items (#753) 2022-04-22 15:45:54 -05:00
Jason Dove
d67251bfa0 jellyfin and emby collection sync (#752)
* sync jellyfin and emby collections

* update changelog
2022-04-22 13:33:35 -05:00
Jason Dove
e91ec98007 fix season sync from jellyfin and emby (#751) 2022-04-21 21:36:09 -05:00
Jason Dove
097b8c3d1f subtitle fixes (#750)
* fix crash with missing metadata

* fix subtitles in docker

* fix software overlay bug
2022-04-21 20:17:50 -05:00
Jason Dove
7284ee9fb7 fix updating local season metadata 2022-04-21 15:42:10 -05:00
Jason Dove
fccb9003a0 add plex deep scan mode and sync labels (#749) 2022-04-21 14:02:37 -05:00
Jason Dove
cc9c2f6ae3 fix external subtitles with brackets in the filename (#748) 2022-04-21 10:25:21 -05:00
Jason Dove
ec6eab97b2 plex scanner improvements (#747)
* plex api cleanup

* improve plex movie scanner

* sync plex collections as tags

* improve plex tv library scanner

* update dependencies

* fix plex season and episode collection tags
2022-04-21 09:54:38 -05:00
Jason Dove
3ede136ff3 fix windows build 2022-04-20 16:10:13 -05:00
Jason Dove
7c27241ab6 properly reset emby and jellyfin libraries 2022-04-20 15:56:48 -05:00
Jason Dove
3713711864 support external subtitles (#745)
* support external subtitles in local movie libraries

* code cleanup

* simplify subtitle updating

* skip external subtitles from media servers

* fix plex subtitles
2022-04-20 15:54:53 -05:00
Jason Dove
d755d0ae29 sync subtitles from media server scanners (#744) 2022-04-19 21:24:36 -05:00
Jason Dove
c02b83d0d6 code cleanup (#743)
* update tools

* run code cleanup

* update dependencies
2022-04-19 17:47:18 -05:00
Jason Dove
e250e93a8e add support for embedded text subtitles (#742)
* first pass at text subtitle support

* support text subtitles from movies, music videos and other videos

* fixes

* qsv fixes

* vaapi fixes

* update changelog
2022-04-19 12:57:24 -05:00
Jason Dove
60965d0961 update changelog for release v0.5.1-beta [no ci] 2022-04-17 17:36:32 -05:00
Jason Dove
ff1a7b376f add empty trash button (#739) 2022-04-17 14:45:49 -05:00
Jason Dove
741b00fd52 fix multiple filler scheduling bugs (#738) 2022-04-17 13:30:47 -05:00
Jason Dove
7e55681916 fix cliwrap usage (#737) 2022-04-16 20:12:18 -05:00
Jason Dove
210630d299 subtitle fixes for software, videotoolbox, vaapi (#736)
* fix subtitles using software encoders

* videotoolbox fixes

* fix some vaapi subtitle edge cases
2022-04-16 16:06:49 -05:00
Jason Dove
0ddbb898d6 fix subtitle stream selection (#735) 2022-04-15 19:40:08 -05:00
Jason Dove
d6bf579436 fix alpha => beta versioning 2022-04-15 09:08:30 -05:00
Jason Dove
765df64555 add picture subtitle transcoding tests, and make them all pass with nvenc (#734) 2022-04-14 22:30:26 -05:00
1442 changed files with 140144 additions and 12596 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2021.2.2",
"version": "2022.1.0",
"commands": [
"jb"
]

View File

@@ -1,6 +1,6 @@
[*]
charset=utf-8-bom
charset=utf-8
end_of_line=lf
trim_trailing_whitespace=false
insert_final_newline=false

View File

@@ -190,10 +190,10 @@ jobs:
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
run: dotnet restore -r "${{ matrix.target }}"
- uses: suisei-cn/actions-download-file@v1
if: ${{ matrix.kind }} == "windows"
if: ${{ matrix.kind == 'windows' }}
id: downloadffmpeg
name: Download ffmpeg
with:

View File

@@ -19,14 +19,14 @@ jobs:
tag=$(git describe --tags --abbrev=0)
tag2="${tag:1}"
short=$(git rev-parse --short HEAD)
final="${tag2/alpha/$short}"
final="${tag2/beta/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
final="${tag/alpha/$short}"
final="${tag/beta/$short}"
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:

View File

@@ -24,23 +24,38 @@ jobs:
name: Build & Publish
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- name: base
path: ''
suffix: ''
qemu: false
- name: nvidia
path: 'nvidia/'
suffix: '-nvidia'
qemu: false
- name: vaapi
path: 'vaapi/'
suffix: '-vaapi'
qemu: false
- name: arm64
path: 'arm64/'
suffix: '-arm64'
qemu: true
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 QEMU
uses: docker/setup-qemu-action@v1
if: ${{ matrix.qemu == true }}
- name: Set up Docker Buildx NVIDIA
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
id: builder-nvidia
- name: Set up Docker Buildx VAAPI
uses: docker/setup-buildx-action@v1
id: builder-vaapi
id: docker-buildx
- name: Login to DockerHub
uses: docker/login-action@v1
@@ -48,41 +63,31 @@ jobs:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Build and push base
- name: Build and push
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-base.outputs.name }}
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/Dockerfile
file: ./docker/${{ matrix.path }}Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}
jasongdove/ersatztv:${{ inputs.tag_version }}
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
if: ${{ matrix.name != 'arm64' }}
- name: Build and push nvidia
- name: Build and push
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
file: ./docker/${{ matrix.path }}Dockerfile
push: true
platforms: 'linux/arm64'
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker-nvidia
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}-nvidia
jasongdove/ersatztv:${{ inputs.tag_version }}-nvidia
- 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
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
if: ${{ matrix.name == 'arm64' }}

View File

@@ -16,7 +16,7 @@ jobs:
run: |
tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |

View File

@@ -5,6 +5,173 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [0.6.1-beta] - 2022-06-03
### Fixed
- Fix Jellyfin show library paging
- Properly locate and identify multiple Plex servers
- Properly restore `Unavailable`/`File Not Found` items when they are located on disk
### Added
- Add basic music video credits subtitle generation
- This can be enabled in channel settings
## [0.6.0-beta] - 2022-06-01
### Fixed
- Additional fix for duplicate `Other Videos` entries; trash may need to be emptied one last time after upgrading
- Fix watermark opacity in cultures where `,` is a decimal separator
- Rework playlist filtering to avoid empty playlist responses
- Fix some QSV/VAAPI memory errors by always requesting 64 extra hardware frames
### Added
- Enable QSV hardware acceleration for vaapi docker images
### Changed
- Use paging to synchronize all media from Plex, Jellyfin and Emby
- This will reduce memory use and improve reliability of synchronizing large libraries
- Disable low power mode for `h264_qsv` and `hevc_qsv` encoders
## [0.5.8-beta] - 2022-05-20
### Fixed
- Fix error display with `HLS Segmenter` and `MPEG-TS` streaming modes
- Remove erroneous log messages about normalizing framerate on channels where framerate normalization is disabled
- Fix unscheduled filler gaps that sometimes happen as playouts are automatically extended each hour
### Added
- Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason
### Changed
- Remove thread limitation for scenarios where it is not required
- This should give a performance boost to installations that don't use hardware acceleration
- Use hardware acceleration to display error messages where configured
## [0.5.7-beta] - 2022-05-14
### Fixed
- Reduce memory use due to library scan operations
- Fix some instances of filler getting "stuck" when a filler item is encountered that's too long for the gap
- Properly ignore Plex `Other Videos` libraries (`movie` libraries where agent is `com.plexapp.agents.none`)
- Fix `Custom Title` for schedule items with `One`, `Multiple` and `Flood` playout modes
- Fix scheduling bug where flood items would sometimes fail to continue after midnight
### Added
- Add `metadata_kind` field to search index to allow searching for items with a particular metdata source
- Valid metadata kinds are `fallback`, `sidecar` (NFO), `external` (from a media server) and `embedded` (songs)
- Add autocomplete functionality to search bar to quickly navigate to channels, ffmpeg profiles, collections and schedules by name
- Add global setting to skip missing (file-not-found or unavailable) items when building playouts
- Add filler preset option to allow watermarks to overlay on top of filler (disabled by default)
- This option is applied when new items are added to a playout; rebuilding is needed if you want the change to take effect immediately
- Read `track` field from music video NFO metadata and use it for chronological sorting (after release date)
- Add `Random Start Point` option to schedules
- When this option is enabled, all `Chronological` or `Shuffle In Order` content groups will have their start points randomized
- When this option is disabled, all `Chronological` or `Shuffle In Order` content groups will start with the chronologically earliest item
### Changed
- Replace invalid (control) characters in NFO metadata with replacement character `<60>` before parsing
- Store partial (incomplete) NFO metadata results when invalid XML is encountered
- Previously, no metadata would be stored if the XML within the NFO failed to validate
## [0.5.6-beta] - 2022-05-06
### Fixed
- Fix processing local movie NFO metadata without a `year` value
- Fix processing local movie fallback metadata
- Fix search edge case where very recently added items (hours) would not be returned by relative date queries
- Fix search index validation on startup; improper validation was causing a rebuild with every startup
- Block library scanning until search index has been recreated/upgraded
- Fix occasional erroneous log messages when HLS channel playback times out because all clients have left
- Fix fallback filler playback
- Fix stream continuity when error messages are displayed
- Fix duplicate scanning within `Other Video` libraries (i.e. folders would be scanned multiple times)
### Added
- Add `show_genre` and `show_tag` to search index for seasons and episodes
- Use `aired` value to source release date from music video nfo metadata
- Add NFO metadata support to `Other Video` libraries
- `Other Video` NFO metadata must be in the movie NFO metadata format
## [0.5.5-beta] - 2022-05-03
### Fixed
- Fix adding episodes with no title to the search index
- This behavior was preventing some items from being removed from the trash
- Support combination NFO metadata for movies, shows, artists and music videos
- Note that ErsatzTV does not scrape any metadata; any URLs after the XML will be ignored
- Fix bug causing some Jellyfin and Emby content to incorrectly show as unavailable
- Fix extracting embedded `mov_text` subtitles
- Properly extract embedded subtitles on playouts where subtitles are only enabled on schedule items (and not on the channel itself)
### Added
- Add experimental `arm64` docker tags (`develop-arm64` and `latest-arm64`)
- Use `Sort Title` from Movie NFO metadata if available
- Support multiple `Artist` entries in music video NFO metadata
## [0.5.4-beta] - 2022-04-29
### Fixed
- Cleanly stop all library scans when service termination is requested
- Fix health check crash when trash contains a show or a season
- Fix ability of health check crash to crash home page
- Remove and ignore Season 0/Specials from Plex shows that have no specials
- Automatically delete and rebuild the search index on startup if it has become corrupt
- Automatically scan Jellyfin and Emby libraries on startup and periodically
- Properly remove un-synchronized Plex, Jellyfin and Emby items from the database and search index
- Fix synchronizing movies within a collection from Jellyfin
### Changed
- Update Plex, Jellyfin and Emby movie and show library scanners to share a significant amount of code
- This should help maintain feature parity going forward
- Optimize search-index rebuilding to complete 100x faster
- **No longer use network paths to source content from Jellyfin and Emby**
- **If you previously used path replacements to convert network paths to local paths, you should remove them**
### Added
- Add `unavailable` state for Jellyfin and Emby movie and show libraries
- Add `height` and `width` to search index for all videos
- Add `season_number` and `episode_number` to search index for all episodes
- Add `season_number` to search index for seasons
- Add `show_title` to search index for seasons and episodes
## [0.5.3-beta] - 2022-04-24
### Fixed
- Cleanly stop Plex library scan when service termination is requested
- Fix bug introduced with 0.5.2-beta that prevented some Plex content from being played
- Fix spammy subtitle error message
- Fix generating blur hashes for song backgrounds in Docker
### Changed
- No longer remove Plex movies and episodes from ErsatzTV when they do not exist on disk
- Instead, a new `unavailable` media state will be used to indicate this condition
- After updating mounts, path replacements, etc - a library scan can be used to resolve this state
## [0.5.2-beta] - 2022-04-22
### Fixed
- Fix unlocking libraries when scanning fails for any reason
- Fix software overlay of actual size watermark
### Added
- Add support for burning in embedded and external text subtitles
- **This requires a one-time full library scan, which may take a long time with large libraries.**
- Sync Plex, Jellyfin and Emby collections as tags on movies, shows, seasons and episodes
- This allows smart collections that use queries like `tag:"Collection Name"`
- Note that Emby has an outstanding collections bug that prevents updates when removing items from a collection
- Sync Plex labels as tags on movies and shows
- This allows smart collections that use queries like `tag:"Plex Label Name"`
- Add `Deep Scan` button for Plex libraries
- This scanning mode is *slow* but is required to detect some changes like labels
### Changed
- Improve the speed and change detection of the Plex library scanners
## [0.5.1-beta] - 2022-04-17
### Fixed
- Fix subtitles edge case with NVENC
- Only select picture subtitles (text subtitles are not yet supported)
- Supported picture subtitles are `hdmv_pgs_subtitle` and `dvd_subtitle`
- Fix subtitles using software encoders, videotoolbox, VAAPI
- Fix setting VAAPI driver name
- Fix ffmpeg troubleshooting reports
- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
### Added
- Add `Empty Trash` button to `Trash` page
## [0.5.0-beta] - 2022-04-13
### Fixed
- Fix `HLS Segmenter` bug where it would drift off of the schedule if a playout was changed while the segmenter was running
@@ -1068,7 +1235,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...HEAD
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.1-beta...HEAD
[0.6.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.6.0-beta...v0.6.1-beta
[0.6.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.8-beta...v0.6.0-beta
[0.5.8-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.7-beta...v0.5.8-beta
[0.5.7-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.6-beta...v0.5.7-beta
[0.5.6-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.5-beta...v0.5.6-beta
[0.5.5-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.4-beta...v0.5.5-beta
[0.5.4-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.3-beta...v0.5.4-beta
[0.5.3-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.2-beta...v0.5.3-beta
[0.5.2-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.1-beta...v0.5.2-beta
[0.5.1-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.5.0-beta...v0.5.1-beta
[0.5.0-beta]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...v0.5.0-beta
[0.4.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.4-alpha...v0.4.5-alpha
[0.4.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.3-alpha...v0.4.4-alpha

View File

@@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.4.2" />
<PackageReference Include="CliWrap" Version="3.4.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -11,4 +11,4 @@ public record ArtistViewModel(
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
List<CultureInfo> Languages);

View File

@@ -37,4 +37,4 @@ internal static class Mapper
.Flatten()
.ToList();
}
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Artists;
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;

View File

@@ -19,4 +19,4 @@ public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMed
a => !string.IsNullOrWhiteSpace(
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Artists;
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;

View File

@@ -29,4 +29,4 @@ public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<Artist
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
}
}

View File

@@ -16,4 +16,5 @@ public record ChannelViewModel(
int? FallbackFillerId,
int PlayoutCount,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode);
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode);

View File

@@ -16,4 +16,5 @@ public record CreateChannel
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, CreateChannelResult>>;
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, CreateChannelResult>>;

View File

@@ -72,7 +72,8 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
Artwork = artwork,
PreferredAudioLanguageCode = preferredAudioLanguageCode,
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode
};
foreach (int id in watermarkId)
@@ -106,7 +107,9 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
CreateChannel createChannel)
{
Option<Channel> maybeExistingChannel = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
@@ -169,4 +172,4 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Channels;
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;

View File

@@ -20,4 +20,4 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
(await _channelRepository.Get(deleteChannel.ChannelId))
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
.Map(c => c.Id);
}
}

View File

@@ -17,4 +17,5 @@ public record UpdateChannel
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode) : IRequest<Either<BaseError, ChannelViewModel>>;
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode) : IRequest<Either<BaseError, ChannelViewModel>>;

View File

@@ -1,20 +1,29 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels;
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<ISubtitleWorkerRequest> _ffmpegWorkerChannel;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
public UpdateChannelHandler(
ChannelWriter<ISubtitleWorkerRequest> ffmpegWorkerChannel,
IDbContextFactory<TvContext> dbContextFactory)
{
_ffmpegWorkerChannel = ffmpegWorkerChannel;
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
@@ -35,6 +44,7 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
c.SubtitleMode = update.SubtitleMode;
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
@@ -60,11 +70,23 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
if (c.SubtitleMode != ChannelSubtitleMode.None)
{
Option<Playout> maybePlayout = await dbContext.Playouts
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == c.Id);
foreach (Playout playout in maybePlayout)
{
await _ffmpegWorkerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
}
}
return ProjectToViewModel(c);
}
@@ -114,4 +136,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred audio language code is invalid");
}
}

View File

@@ -20,7 +20,8 @@ internal static class Mapper
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0,
channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode);
channel.SubtitleMode,
channel.MusicVideoCreditsMode);
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
new(
@@ -44,4 +45,4 @@ internal static class Mapper
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
_ => throw new ArgumentOutOfRangeException()
};
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
public record GetAllChannels : IRequest<List<ChannelViewModel>>;

View File

@@ -18,4 +18,4 @@ public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi,
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
return channels.Map(ProjectToResponseModel).ToList();
}
}
}

View File

@@ -11,4 +11,4 @@ public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<Channe
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;

View File

@@ -12,4 +12,4 @@ public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<Chan
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
}

View File

@@ -11,4 +11,4 @@ public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Opt
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
}
}

View File

@@ -21,11 +21,22 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
FFmpegProfile ffmpegProfile = await dbContext.Channels
.Filter(c => c.Number == request.ChannelNumber)
.Include(c => c.FFmpegProfile)
.Map(c => c.FFmpegProfile)
.SingleAsync(cancellationToken);
if (!ffmpegProfile.NormalizeFramerate)
{
return Option<int>.None;
}
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;

View File

@@ -1,15 +1,23 @@
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using Microsoft.IO;
namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
{
private readonly IChannelRepository _channelRepository;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelGuideHandler(
IChannelRepository channelRepository,
RecyclableMemoryStreamManager recyclableMemoryStreamManager)
{
_channelRepository = channelRepository;
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
}
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
}
.Map(channels => new ChannelGuide(_recyclableMemoryStreamManager, request.Scheme, request.Host, channels));
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;

View File

@@ -12,4 +12,4 @@ public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<Li
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;

View File

@@ -47,4 +47,4 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
return result;
}
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Configuration;
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest<Unit>;

View File

@@ -2,7 +2,7 @@
namespace ErsatzTV.Application.Configuration;
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey, Unit>
{
private readonly IConfigElementRepository _configElementRepository;
@@ -14,4 +14,4 @@ public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigE
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Configuration;
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : IRequest<Either<BaseError, Unit>>;

View File

@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
@@ -16,7 +16,10 @@ public class UpdateLibraryRefreshIntervalHandler :
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.MapT(
_ => _configElementRepository.Upsert(
ConfigElementKey.LibraryRefreshInterval,
request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
@@ -25,4 +28,4 @@ public class UpdateLibraryRefreshIntervalHandler :
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
}
}

View File

@@ -1,5 +0,0 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Configuration;
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;

View File

@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Configuration;
public record UpdatePlayoutSettings(PlayoutSettingsViewModel PlayoutSettings) : IRequest<Either<BaseError, Unit>>;

View File

@@ -9,13 +9,13 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Configuration;
public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
public class UpdatePlayoutSettingsHandler : IRequestHandler<UpdatePlayoutSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
public UpdatePlayoutSettingsHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
@@ -26,17 +26,20 @@ public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDays
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
UpdatePlayoutSettings request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.PlayoutSettings));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
private async Task<Unit> ApplyUpdate(TvContext dbContext, PlayoutSettingsViewModel playoutSettings)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, playoutSettings.DaysToBuild);
await _configElementRepository.Upsert(
ConfigElementKey.PlayoutSkipMissingItems,
playoutSettings.SkipMissingItems);
// continue all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
@@ -50,10 +53,10 @@ public class UpdatePlayoutDaysToBuildHandler : IRequestHandler<UpdatePlayoutDays
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutSettings request) =>
Optional(request.PlayoutSettings.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record ConfigElementViewModel(string Key, string Value);
public record ConfigElementViewModel(string Key, string Value);

View File

@@ -6,4 +6,4 @@ internal static class Mapper
{
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
}

View File

@@ -0,0 +1,7 @@
namespace ErsatzTV.Application.Configuration;
public class PlayoutSettingsViewModel
{
public int DaysToBuild { get; set; }
public bool SkipMissingItems { get; set; }
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Configuration;
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;

View File

@@ -14,4 +14,4 @@ public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKe
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record GetLibraryRefreshInterval : IRequest<int>;
public record GetLibraryRefreshInterval : IRequest<int>;

View File

@@ -13,4 +13,4 @@ public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefres
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
}

View File

@@ -1,3 +0,0 @@
namespace ErsatzTV.Application.Configuration;
public record GetPlayoutDaysToBuild : IRequest<int>;

View File

@@ -1,16 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2));
}

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record GetPlayoutSettings : IRequest<PlayoutSettingsViewModel>;

View File

@@ -0,0 +1,26 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class GetPlayoutSettingsHandler : IRequestHandler<GetPlayoutSettings, PlayoutSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<PlayoutSettingsViewModel> Handle(GetPlayoutSettings request, CancellationToken cancellationToken)
{
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
Option<bool> skipMissingItems =
await _configElementRepository.GetValue<bool>(ConfigElementKey.PlayoutSkipMissingItems);
return new PlayoutSettingsViewModel
{
DaysToBuild = await daysToBuild.IfNoneAsync(2),
SkipMissingItems = await skipMissingItems.IfNoneAsync(false)
};
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Emby;
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
public record DisconnectEmby : IRequest<Either<BaseError, Unit>>;

View File

@@ -7,7 +7,7 @@ using ErsatzTV.Core.Interfaces.Search;
namespace ErsatzTV.Application.Emby;
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
public class DisconnectEmbyHandler : IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
{
private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
@@ -38,4 +38,4 @@ public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Eit
return Unit.Default;
}
}
}

View File

@@ -3,4 +3,4 @@ using ErsatzTV.Core.Emby;
namespace ErsatzTV.Application.Emby;
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
public record SaveEmbySecrets(EmbySecrets Secrets) : IRequest<Either<BaseError, Unit>>;

View File

@@ -6,7 +6,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Emby;
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
@@ -53,4 +53,4 @@ public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, E
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
}
}

View File

@@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyCollections(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;

View File

@@ -0,0 +1,73 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyCollectionsHandler : IRequestHandler<SynchronizeEmbyCollections, Either<BaseError, Unit>>
{
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IEmbyCollectionScanner _scanner;
public SynchronizeEmbyCollectionsHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyCollectionScanner scanner)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_scanner = scanner;
}
public async Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyCollections request,
CancellationToken cancellationToken)
{
Validation<BaseError, ConnectionParameters> validation = await Validate(request);
return await validation.Match(
SynchronizeCollections,
error => Task.FromResult<Either<BaseError, Unit>>(error.Join()));
}
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyCollections request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyCollections request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private async Task<Either<BaseError, Unit>> SynchronizeCollections(ConnectionParameters connectionParameters) =>
await _scanner.ScanCollections(
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
private record ConnectionParameters(EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}

View File

@@ -2,5 +2,5 @@
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;

View File

@@ -8,8 +8,7 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Emby;
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
{
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
@@ -72,32 +71,33 @@ public class
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
await maybeLibraries.Match(
async libraries =>
{
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
},
error =>
{
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
foreach (BaseError error in maybeLibraries.LeftToSeq())
{
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
}
return Task.CompletedTask;
});
foreach (List<EmbyLibrary> libraries in maybeLibraries.RightToSeq())
{
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
var toUpdate = libraries
.Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove,
toUpdate);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
}
return Unit.Default;
}
@@ -108,4 +108,4 @@ public class
{
public string ApiKey { get; set; }
}
}
}

View File

@@ -17,4 +17,4 @@ public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchroni
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
}

View File

@@ -1,4 +1,5 @@
using ErsatzTV.Core;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
@@ -17,6 +18,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _embyWorkerChannel;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
@@ -31,6 +33,7 @@ public class SynchronizeEmbyLibraryByIdHandler :
ILibraryRepository libraryRepository,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> embyWorkerChannel,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
@@ -40,61 +43,81 @@ public class SynchronizeEmbyLibraryByIdHandler :
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_embyWorkerChannel = embyWorkerChannel;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request);
CancellationToken cancellationToken) => HandleImpl(request, cancellationToken);
public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
CancellationToken cancellationToken) => HandleImpl(request, cancellationToken);
private Task<Either<BaseError, string>>
Handle(ISynchronizeEmbyLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
private async Task<Either<BaseError, string>>
HandleImpl(ISynchronizeEmbyLibraryById request, CancellationToken cancellationToken)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
Validation<BaseError, RequestParameters> validation = await Validate(request);
return await validation.Match(
parameters => Synchronize(parameters, cancellationToken),
error => Task.FromResult<Either<BaseError, string>>(error.Join()));
}
private async Task<Either<BaseError, string>> Synchronize(
RequestParameters parameters,
CancellationToken cancellationToken)
{
try
{
switch (parameters.Library.MediaKind)
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
case LibraryMediaKind.Movies:
await _embyMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath);
break;
case LibraryMediaKind.Shows:
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath);
break;
Either<BaseError, Unit> result = parameters.Library.MediaKind switch
{
LibraryMediaKind.Movies =>
await _embyMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
cancellationToken),
LibraryMediaKind.Shows =>
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
cancellationToken),
_ => Unit.Default
};
if (result.IsRight)
{
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
await _embyWorkerChannel.WriteAsync(
new SynchronizeEmbyCollections(parameters.Library.MediaSourceId),
cancellationToken);
}
return result.Map(_ => parameters.Library.Name);
}
else
{
_logger.LogDebug("Skipping unforced scan of emby media library {Name}", parameters.Library.Name);
}
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
return parameters.Library.Name;
}
else
finally
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
_entityLocker.UnlockLibrary(parameters.Library.Id);
}
_entityLocker.UnlockLibrary(parameters.Library.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
@@ -181,4 +204,4 @@ public class SynchronizeEmbyLibraryByIdHandler :
{
public string ApiKey { get; set; }
}
}
}

View File

@@ -4,4 +4,4 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
IEmbyBackgroundServiceRequest;

View File

@@ -32,4 +32,4 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
return mediaSources;
}
}
}

View File

@@ -3,6 +3,6 @@
namespace ErsatzTV.Application.Emby;
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);

View File

@@ -5,7 +5,7 @@ using ErsatzTV.Core.Interfaces.Search;
namespace ErsatzTV.Application.Emby;
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
UpdateEmbyLibraryPreferencesHandler : IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
@@ -33,4 +33,4 @@ public class
return Unit.Default;
}
}
}

View File

@@ -4,6 +4,6 @@ namespace ErsatzTV.Application.Emby;
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
List<EmbyPathReplacementItem> PathReplacements) : IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);

View File

@@ -4,7 +4,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Emby;
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
@@ -46,4 +46,4 @@ public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateE
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record EmbyConnectionParametersViewModel(string Address);
public record EmbyConnectionParametersViewModel(string Address);

View File

@@ -3,5 +3,10 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby;
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
: LibraryViewModel("Emby", Id, Name, MediaKind);
public record EmbyLibraryViewModel(
int Id,
string Name,
LibraryMediaKind MediaKind,
bool ShouldSyncItems,
int MediaSourceId)
: LibraryViewModel("Emby", Id, Name, MediaKind, MediaSourceId);

View File

@@ -5,4 +5,4 @@ namespace ErsatzTV.Application.Emby;
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
Id,
Name,
Address);
Address);

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);

View File

@@ -11,8 +11,8 @@ internal static class Mapper
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems, library.MediaSourceId);
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;

View File

@@ -14,4 +14,4 @@ public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSour
GetAllEmbyMediaSources request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Emby;
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;

View File

@@ -63,4 +63,4 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;

View File

@@ -16,4 +16,4 @@ public class
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.Emby;
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;

View File

@@ -15,4 +15,4 @@ public class
GetEmbyMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
}
}

View File

@@ -1,4 +1,4 @@
namespace ErsatzTV.Application.Emby;
public record GetEmbyPathReplacementsBySourceId
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;

View File

@@ -16,4 +16,4 @@ public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyP
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.Emby;
public record GetEmbySecrets : IRequest<EmbySecrets>;
public record GetEmbySecrets : IRequest<EmbySecrets>;

View File

@@ -12,4 +12,4 @@ public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
_embySecretStore.ReadSecrets();
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application;
public record EntityIdResult(int Id);
public record EntityIdResult(int Id);

View File

@@ -8,10 +8,11 @@
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.0.1" />
<PackageReference Include="CliWrap" Version="3.4.2" />
<PackageReference Include="CliWrap" Version="3.4.4" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.2.32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -38,6 +38,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -3,4 +3,4 @@
namespace ErsatzTV.Application.FFmpegProfiles;
public record CopyFFmpegProfile
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;

View File

@@ -29,4 +29,4 @@ public class
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
}
}

View File

@@ -21,4 +21,4 @@ public record CreateFFmpegProfile(
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;

View File

@@ -32,7 +32,9 @@ public class CreateFFmpegProfileHandler :
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
private async Task<Validation<BaseError, FFmpegProfile>> Validate(
TvContext dbContext,
CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply(
(name, threadCount, resolutionId) => new FFmpegProfile
@@ -70,4 +72,4 @@ public class CreateFFmpegProfileHandler :
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId)
.MapT(r => r.Id)
.Map(o => o.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist"));
}
}

View File

@@ -1,3 +1,3 @@
namespace ErsatzTV.Application.FFmpegProfiles;
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);

View File

@@ -2,4 +2,4 @@
namespace ErsatzTV.Application.FFmpegProfiles;
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Unit>>;

View File

@@ -6,14 +6,14 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles;
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
public async Task<Either<BaseError, Unit>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken)
{
@@ -22,11 +22,11 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
return await LanguageExtensions.Apply(validation, p => DoDeletion(dbContext, p));
}
private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
return LanguageExt.Unit.Default;
return Unit.Default;
}
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
@@ -35,4 +35,4 @@ public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, E
dbContext.FFmpegProfiles
.SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {request.FFmpegProfileId} does not exist"));
}
}

View File

@@ -4,4 +4,4 @@
/// Requests a new ffmpeg profile (view model) that contains
/// appropriate default values.
/// </summary>
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;
public record NewFFmpegProfile : IRequest<FFmpegProfileViewModel>;

View File

@@ -29,4 +29,4 @@ public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegP
return ProjectToViewModel(FFmpegProfile.New("New Profile", defaultResolution));
}
}
}

View File

@@ -22,4 +22,4 @@ public record UpdateFFmpegProfile(
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool DeinterlaceVideo) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
bool DeinterlaceVideo) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;

Some files were not shown because too many files have changed in this diff Show More