Compare commits

...
Author SHA1 Message Date
Jason Dove 0308106c1b update changelog for release v0.4.5-alpha [no ci] 2022-03-29 20:12:01 -05:00
Jason DoveandGitHub ba93e3eeea always check for plex on the localhost (#716) 2022-03-29 19:39:55 -05:00
Jason DoveandGitHub aa2c914d8a always transcode and normalize, except with HLS Direct (#715)
* remove transcode, normalize video, normalize audio settings

* cleanup

* update changelog
2022-03-28 18:25:33 -05:00
Jason Dove caa9bf82d5 update changelog [no ci] 2022-03-28 13:10:33 -05:00
Jason Dove e397035c5a update changelog [no ci] 2022-03-18 14:07:15 -05:00
Jason DoveandGitHub d32f881c4e use cliwrap for windows wrapper (#710) 2022-03-18 13:36:38 -05:00
Jason DoveandGitHub 9b3c24559d add deinterlace option to ffmpeg profile (#709)
* update dependencies

* add option to deinterlace video
2022-03-18 10:30:56 -05:00
Jason Dove 7c75b169ec try gon again 2022-03-16 11:39:44 -05:00
Jason Dove 4f1952340f fix gon since brew is not working 2022-03-16 11:24:52 -05:00
Jason Dove ac2de24f6e fix build 2022-03-16 10:50:38 -05:00
Jason DoveandGitHub 4b9781dad4 add v2 channels table (#708)
* change mock api port; fix cors

* fix version request

* accept base url env var

* tweak colors

* fix version service

* add basic channels list

* fix auto page title for ts classes

* add GET /api/channels
2022-03-16 10:28:54 -05:00
Jason DoveandGitHub d88c179b63 axios test (#705)
* add version controller

* use axios to get version

* allow typescript

* use version api service
2022-03-15 17:55:05 -05:00
Jason DoveandGitHub 809a623a95 vue formatting tweaks (#704)
* add .prettierrc

* apply formatting
2022-03-14 10:59:04 -05:00
Jason Dove b453dce57e try npm ci with cache 2022-03-13 20:38:04 -05:00
Jason Dove edd31755c0 try npm install again 2022-03-13 20:21:16 -05:00
Jason DoveandGitHub 7de1a87bbf bug fixes (#703)
* catch expected shutdown error in scheduler service

* fix streaming mode inconsistencies
2022-03-13 18:45:21 -05:00
Jason DoveandGitHub 5731edc82e v2 ui tweaks (#702)
* fix m3u and xml urls (kind of)

* use svg logos

* tweak theme colors
2022-03-13 12:44:35 -05:00
Jason DoveandGitHub 2f668e53dd fix npm build (#701) 2022-03-13 11:11:08 -05:00
Jason DoveandGitHub abd223acd2 fix docker builds (#700) 2022-03-13 10:52:03 -05:00
6a9075dc11 add vue ui at /v2 (#698)
* add vue ui

* add channels mock api

* Initial Vue framework with Vuetify UI (#688)

* fix hls direct streaming mode (#682)

* duration analysis on files with missing duration metadata (#683)

* first pass

* analyze zero-duration files

* add readme note for WIP

* add vuetify and basic sidebar layout - responsive

* add vue-router and initial home page

* setup composition-api for vue2

* install pinia ie... vuex4

* mixing for automatic page title

* add logo files

* tweak theme colors

* install store

* use store for menu toggle

* replicate old menu

* implement menu and children menus

* rename state to application state

* update vue files and add version to sidebar

* lock logo and make expandable list remove minified menu

* remove todo, will add to PR

* top bar links and attempt at snackbar with state

* fix snackbar

* add search basic component

* fix search bar placement

* remove un-used footer

* Revert "Merge branch 'jasongdove:main' into intitial-vuetify-ui"

This reverts commit 43016d502b.

Co-authored-by: Jason Dove <jason@jasondove.me>

* Add ESLint and Prettier for VueJS (#691)

* add prettier to config

* run npm run lint over project to clean up files

* replace hr tag with v-dividers

* add vue-lint github action

* fix the dodo in me

* Hu

* Fix path

* convert to multi-run step

* forgot the name

* add vue-lint github action

fix the dodo in me

Hu

Fix path

convert to multi-run step

forgot the name

* Fix new line at end of file

* WIP

* dockerfile consistency

* use npm ci and node v14

* force prettier indenting and end of line (#695)

* disable filename hashing

* don't build tests in docker

* update dependencies

* fix running both uis

Co-authored-by: James Mackay <info@notexpectedyet.com>
2022-03-12 14:15:31 -06:00
Jason DoveandGitHub d5a03963c0 use video and audio format instead of video and audio codec (#696) 2022-03-11 19:58:05 -06:00
Jason Dove f3e5ff198b update changelog for release v0.4.4-alpha [no ci] 2022-03-10 17:54:39 -06:00
Jason DoveandGitHub 6be5111195 fix errors; add nouveau vaapi driver option (#692)
* fix service shutdown errors

* add nouveau vaapi driver option
2022-03-10 12:19:30 -06:00
Jason DoveandGitHub f0670b345f use wrapped processes; fix hls pts bug (#690) 2022-03-09 14:38:09 -06:00
Jason DoveandGitHub 6a1c2b7659 read ffprobe pts output from stdout and stderr (#689) 2022-03-09 08:54:51 -06:00
Jason DoveandGitHub 7cd2f9a56f update dependencies (#687) 2022-03-08 20:21:10 -06:00
Jason DoveandGitHub f66bc783a7 fix hls segmenter on windows (#686) 2022-03-08 19:58:41 -06:00
Jason DoveandGitHub bc225d35fa add troubleshooting code to hls segmenter (#685) 2022-03-08 12:52:29 -06:00
Jason DoveandGitHub 52a8b7db81 duration analysis on files with missing duration metadata (#683)
* first pass

* analyze zero-duration files
2022-03-07 21:42:34 -06:00
Jason DoveandGitHub dcd792a354 fix hls direct streaming mode (#682) 2022-03-07 20:04:21 -06:00
Jason Dove f69c58c6bf update changelog for release v0.4.3-alpha [no ci] 2022-03-05 19:49:31 -06:00
Jason DoveandGitHub 44e90b0ecc more bug reporting (#679) 2022-03-05 18:21:53 -06:00
Jason DoveandGitHub dcc8f19a6b remove transient IDbConnection (#678) 2022-03-05 14:06:27 -06:00
Jason DoveandGitHub fc1a051df5 fix path replacement bug (#677) 2022-03-05 12:21:26 -06:00
Jason DoveandGitHub a2e7e6df1e fix thread sync bug in hls segmenter (#676) 2022-03-05 11:26:29 -06:00
Jason DoveandGitHub 6c06fbe621 fix mid-roll filler scheduling bug (#675) 2022-03-04 21:40:08 -06:00
Jason DoveandGitHub a3260b2316 fix album_artist metadata (#674) 2022-03-04 21:10:55 -06:00
Jason DoveandGitHub ea339a1622 add song album_artist metadata (#673) 2022-03-04 18:44:54 -06:00
Jason DoveandGitHub 58697496fa metadata stack trace improvements (#672)
* improve stack traces from local metadata provider

* more metadata line number fixes
2022-03-04 15:44:51 -06:00
Jason DoveandGitHub ec1b2502f1 rework bugsnag integration (#671) 2022-03-03 21:39:46 -06:00
Jason DoveandGitHub 1ab98578ab refactor namespaces and imports (#670)
* re-namespace

* optimize usings

* more usings

* more of the same

* more implicit/global usings

* cleanup all usings

* minor fixes
2022-03-03 15:36:07 -06:00
Jason DoveandGitHub 748581bf5a update dependencies (#669) 2022-03-03 08:46:08 -06:00
Jason DoveandGitHub 2058c44949 minor ui fixes (#668)
* remove unused package

* fix controller name

* catch and ignore jsruntime exceptions

* separate debug stage from public stage
2022-03-02 20:45:07 -06:00
Jason DoveandGitHub 24ef5e68eb add error reporting health check (#667) 2022-03-02 14:51:15 -06:00
Jason Dove b1c905233f add thank you page to docs [no build] 2022-03-02 09:19:54 -06:00
Jason DoveandGitHub 452f361384 use cancellation tokens in ui (#666)
* use cancellation tokens in pages

* use cancellation token in shared ui

* use cancellation tokens in artwork controller
2022-03-01 21:31:48 -06:00
Jason DoveandGitHub 9e2f445785 add bugsnag error reporting (#665) 2022-03-01 18:41:24 -06:00
Jason DoveandGitHub ea72e7b689 use new transcoder logic by default (#664) 2022-03-01 14:29:47 -06:00
Jason DoveandGitHub 19a7f90d52 ffmpeg lib hardware accel fixes (#663)
* more scaling fixes

* qsv fixes

* cleanup

* nvidia fixes

* vaapi fixes

* test nvidia by default
2022-03-01 14:03:36 -06:00
Jason DoveandGitHub 572a3be33e limit framerate normalization to 24fps and above (#662) 2022-02-28 19:22:11 -06:00
Jason DoveandGitHub f8412c4f5c more nvidia fixes (#661)
* dont use mpeg2_cuvid with interlaced content

* fix nvidia scaling when padding is not needed
2022-02-28 18:35:49 -06:00
Jason DoveandGitHub 6b0ced6be9 fix nvidia watermark (#660)
* add watermark transcoding tests

* nvidia fixes

* update changelog
2022-02-28 14:21:42 -06:00
Jason DoveandGitHub 19161b12ea fix chronological and shuffle-in-order song sorting (#659) 2022-02-28 13:41:22 -06:00
Jason Dove fd3ef90880 update changelog for release v0.4.2-alpha [no ci] 2022-02-26 19:23:18 -06:00
Jason DoveandGitHub 696b29c9e9 fix videotoolbox acceleration with new transcoder (#658)
* fix videotoolbox acceleration with new transcoder

* cleanup
2022-02-26 18:45:05 -06:00
Jason DoveandGitHub 70c37df596 fix vaapi watermarks with new transcoder (#657) 2022-02-26 14:00:23 -06:00
Jason DoveandGitHub 040785b0d7 fix qsv scaling and watermarks with new transcoder (#656)
* fix qsv scaling

* fix qsv watermarks
2022-02-26 11:40:16 -06:00
Jason DoveandGitHub b25f783343 hide unused local libraries (#655) 2022-02-25 21:16:38 -06:00
Jason DoveandGitHub a21f62ff8c add watermark support to experimental transcoder logic (#654)
* wip

* fix songs again

* don't use lists of video and audio input files

* test concat command output

* move concat pipeline

* start to use ffmpeg state

* add ffmpeg state and audio state

* audio state is required

* attach input options directly to input files

* move filters to input files

* add watermark support
2022-02-25 21:06:17 -06:00
Jason DoveandGitHub 78fdc9c57a add option to shuffle schedule items (#652)
* add schedule setting

* it works

* fix tests

* update readme

* rebuild all playouts
2022-02-22 21:20:40 -06:00
Jason DoveandGitHub f6c42f3ff5 add configurable channel group and categories (#651) 2022-02-21 21:04:12 -06:00
Jason DoveandGitHub c92b6cb909 fix song playback (#644) 2022-02-17 21:52:28 -06:00
Jason DoveandGitHub a2e1dc8bfb don't deinterlace using nvidia decoders (#643) 2022-02-17 13:28:03 -06:00
Jason DoveandGitHub 8a6093ce8d properly specify audio codec even when source has correct format (#641) 2022-02-16 11:29:54 -06:00
Jason DoveandGitHub 1d6279cee8 log problematic playlists (#640) 2022-02-16 08:18:22 -06:00
Jason DoveandGitHub 66ab0b3990 use single thread and disable framerate normalization (#639)
* try one thread for everything

* add (unused) framerate filter

* disable framerate normalization by default

* update dependencies
2022-02-16 06:01:28 -06:00
Jason DoveandGitHub a7922beaed qsv and logging fixes (#637)
* improve pipeline logging

* fix qsv acceleration

* fix qsv parameter order
2022-02-15 12:41:06 -06:00
Jason DoveandGitHub a1d9d6790e fix copy codec when transcoding is disabled (#636) 2022-02-15 09:22:40 -06:00
Jason DoveandGitHub 2f2d7952dd use vaapi driver settings with new transcoder logic (#635)
* add LIBVA_DRIVER_NAME env var

* add vaapi device name

* add FFREPORT env var

* fixes
2022-02-15 08:44:49 -06:00
Jason DoveandGitHub c96b800b52 ffmpeg lib fixes (#633)
* try to fix vaapi inconsistencies

* log unexpected data

* vaapi fixes

* disable 444 test

* add qsv deinterlace filter; qsv fixes

* add videotoolbox acceleration
2022-02-14 22:01:26 -06:00
Jason Dove c05882f4a6 fix docker builds 2022-02-14 14:36:35 -06:00
Jason DoveandGitHub 5a442a06a0 start to use new ffmpeg library (#632)
* start to add ffmpeg library

* start to hook ffmpeg lib into main app

* improvements

* more progress

* make pipeline builder configurable

* more options

* move more logic down into ffmpeg lib

* ffmpeg lib desired state refactoring

* add software scaling and padding

* add loudness normalization and software deinterlace

* add metadata output options

* add setsar filter

* use built-in scaling logic

* fixes

* initial nvidia support

* nvidia improvements

* support hls mode

* print old arguments at debug level

* fix package reference

* start to add qsv support

* formatting

* fix tests

* add timeout to transcode tests

* show successful ffmpeg arguments

* add vaapi support

* add more software decoders

* add experimental transcoder option

* call existing ffmpeg process service for unimplemented features

* fix nvidia mpeg2video bug

* update changelog

* ignore some neglected unit tests
2022-02-14 14:34:00 -06:00
Jason Dove 640fed0a43 fix hls segmenter bug with unknown packet duration 2022-02-11 18:37:02 -06:00
Jason Dove ab1f294c1f update changelog for release v0.4.1-alpha 2022-02-10 20:49:26 -06:00
Jason DoveandGitHub ea08453913 vaapi improvements (#629)
* fix interlaced video with vaapi

* downgrade imagesharp to fix blurhash generation

* fix ui crash loading collection editor
2022-02-10 20:19:59 -06:00
Jason DoveandGitHub 87deaa6f3a nvidia improvements (#628) 2022-02-10 15:39:45 -06:00
Jason DoveandGitHub 9d99c19ea4 fix playback with unknown pixel format (#627) 2022-02-10 08:37:25 -06:00
Jason DoveandGitHub 49d14b05f6 update more dependencies (#626) 2022-02-09 11:08:49 -06:00
Jason DoveandGitHub a8ba9edf2b update dependencies (#624)
* update dependencies

* include refit xml serializer
2022-02-09 10:10:12 -06:00
Jason DoveandGitHub 89811a1203 wait for one segment by default (#617) 2022-02-07 12:44:12 -06:00
Jason DoveandGitHub 534e2c4512 add hls segmenter initial segment count (#616) 2022-02-07 12:18:58 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c1e148633d Bump Blazored.LocalStorage from 4.1.5 to 4.2.0 (#614)
Bumps [Blazored.LocalStorage](https://github.com/Blazored/LocalStorage) from 4.1.5 to 4.2.0.
- [Release notes](https://github.com/Blazored/LocalStorage/releases)
- [Commits](https://github.com/Blazored/LocalStorage/compare/v4.1.5...v4.2.0)

---
updated-dependencies:
- dependency-name: Blazored.LocalStorage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-07 03:10:34 -06:00
Jason DoveandGitHub a9dff5eff7 properly flag local missing folders (#615) 2022-02-07 02:39:24 -06:00
Jason Dove a2da043f4b try to fix mac permission issues 2022-02-06 17:45:28 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
252c185562 Bump MudBlazor from 6.0.5 to 6.0.6 (#609)
Bumps [MudBlazor](https://github.com/MudBlazor/MudBlazor) from 6.0.5 to 6.0.6.
- [Release notes](https://github.com/MudBlazor/MudBlazor/releases)
- [Changelog](https://github.com/MudBlazor/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/MudBlazor/MudBlazor/compare/v6.0.5...v6.0.6)

---
updated-dependencies:
- dependency-name: MudBlazor
  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-02-05 18:20:45 -06:00
Jason DoveandGitHub a47987a9d7 revert windows trimming (#613)
* Revert "disable trimming in docker"

This reverts commit 5937211bb8.

* Revert "try to reduce windows artifact size"

This reverts commit e32dbd0474.
2022-02-05 18:09:02 -06:00
Jason Dove 5937211bb8 disable trimming in docker 2022-02-05 13:41:47 -06:00
Jason Dove e32dbd0474 try to reduce windows artifact size 2022-02-05 13:34:21 -06:00
Jason Dove 6bcc1ede2b try again 2022-02-05 11:25:14 -06:00
Jason Dove 6c9764a51e try a different method for downloading ffmpeg 2022-02-05 11:19:56 -06:00
Jason Dove ff5438459c try to bundle ffmpeg with windows artifacts 2022-02-05 11:09:01 -06:00
Jason DoveandGitHub 0c53a4509c show collection name in some error messages (#612) 2022-02-04 20:17:17 -06:00
Jason DoveandGitHub 5fd315ead8 change framerate normalization method (#611) 2022-02-04 14:39:27 -06:00
Jason DoveandGitHub f02b0ac345 re-introduce framerate normalization (#610) 2022-02-04 12:57:40 -06:00
Jason Dove fd83007296 try to fix watermark on vaapi 2022-02-01 17:50:47 -06:00
Jason DoveandGitHub 70ca5bf050 fix bug with watermark and short content (#608) 2022-01-31 14:07:12 -06:00
Jason DoveandGitHub eed9f60273 fade in and fade out intermittent watermarks (#607)
* first pass at fading in/out overlay

* fix tests

* update changelog
2022-01-31 12:31:56 -06:00
Jason Dove 0e2e6cd52e build linux-arm64 artifacts 2022-01-31 01:07:03 -06:00
Jason DoveandGitHub c9b557f2e6 more xmltv category improvements (#606) 2022-01-30 17:58:45 -06:00
Jason Dove cde869f3eb enable docker dependency scanning [no build] 2022-01-30 11:50:16 -06:00
Jason DoveandGitHub 90d6a59d3f normalize smart quotes in search queries (#605) 2022-01-30 11:09:32 -06:00
Jason DoveandGitHub b972947747 xmltv category improvements (#604) 2022-01-30 10:56:53 -06:00
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
Jason DoveandGitHub 0440f7643b add videotoolbox acceleration (#575) 2022-01-17 15:05:23 -06:00
Jason DoveandGitHub 0f4219f731 properly unlock libraries after failed scans (#574) 2022-01-14 13:03:15 -06:00
Jason DoveandGitHub cbe5d47611 fix trakt list sync when show does not contain a year (#572) 2022-01-12 21:09:26 -06:00
Jason DoveandGitHub afa52ccc89 add trash system for local libraries (#571)
* flag local movies as file not found

* show warning icon on cards

* unflag movie that is found during scan

* skip missing files when building playouts

* add state to search index

* add file not found health check

* link to search from file not found health check

* support flagging other media kinds as file not found

* continue to schedule missing items

* support episode files not found

* wip trash page

* fix trash url

* trash page is functional

* update changelog

* fix changelog merge
2022-01-12 20:27:53 -06:00
Jason DoveandGitHub 7d1163c68f fix double-click startup on mac (#570) 2022-01-11 15:36:12 -06:00
Jason Dove 883492bd33 update changelog for release v0.3.6-alpha [no ci] 2022-01-10 19:51:01 -06:00
Jason DoveandGitHub a4eac4feea properly overwrite environment variables (#567) 2022-01-08 10:01:22 -06:00
Jason Dove dab58f5840 fix tests 2022-01-07 19:18:31 -06:00
Jason DoveandGitHub 176f136c23 fix some nvenc edge cases where only padding is needed for normalization (#565) 2022-01-07 18:53:28 -06:00
Jason Dove 816d77e15b update changelog [no ci] 2022-01-06 12:01:25 -06:00
Jason Dove 7c4d47a211 update changelog [no ci] 2022-01-06 10:31:18 -06:00
Jason DoveandGitHub d9d2cfa8be search index fixes (#559)
* add music video artist to search index

* properly index minutes field when adding from scan

* bump search index version
2022-01-06 10:28:53 -06:00
Jason DoveandGitHub 8036e46966 update streaming mode docs (#558) [no docker] 2022-01-06 09:10:12 -06:00
Jason DoveandGitHub 594ce437fb rework mpeg-ts mode (#557) 2022-01-05 21:27:28 -06:00
Jason Dove 004c43f895 update changelog for release v0.3.5-alpha [no ci] 2022-01-05 09:26:58 -06:00
Jason DoveandGitHub 257384ea9b fix health checks (#556)
* update bundled ffmpeg version in health check

* recognize qsv acceleration on linux

* update changelog
2022-01-05 08:29:52 -06:00
Jason Dove 637f3a0c8b update docs [no docker] 2022-01-04 22:38:52 -06:00
Jason DoveandGitHub 7346808059 update dependencies (#555) 2022-01-04 22:35:14 -06:00
Jason DoveandGitHub 4210d97ee2 optimize setsar filter (#553) 2022-01-02 23:47:07 -06:00
Jason DoveandGitHub 6a8ecd2532 use software decoding for mpeg4 with vaapi (#550) 2022-01-02 10:59:08 -06:00
Jason Dove 9b834f7cbe update changelog for release v0.3.4-alpha [no ci] 2021-12-21 09:46:43 -06:00
Jason DoveandGitHub 7b73677bad allow ffmpeg reports on windows (#547)
* enable troubleshooting reports on windows

* update changelog

* tweak changelog
2021-12-21 09:27:49 -06:00
Jason DoveandGitHub 85b2a46353 update dependencies (#546) 2021-12-21 08:52:51 -06:00
Jason Dove 6f40f2cbd6 fix songs docs [no docker] 2021-12-17 08:48:40 -06:00
Jason DoveandGitHub b62ee4dee9 add files from top-level folder (#541) 2021-12-14 14:27:12 -06:00
Jason Dove a6e7f192cc add jellyfin path replacement tests [no ci] 2021-12-13 06:25:37 -06:00
Jason Dove 59a1a4a8dc update changelog for release v0.3.3-alpha [no ci] 2021-12-12 23:53:12 -06:00
Jason DoveandGitHub 85a9afb51c update dependencies (#538) 2021-12-12 23:51:57 -06:00
Jason DoveandGitHub 246b4d7591 properly sort channels in m3u (#537) 2021-12-10 20:22:52 -06:00
Jason DoveandGitHub ae2c6350e1 sync virtual shows and season from jellyfin (#536) 2021-12-10 14:41:47 -06:00
Jason DoveandGitHub ce228604e8 use select controls instead of autocomplete (#532)
* use select instead of autocomplete for playout editor

* use select instead of autocomplete for filler preset editor

* reset selected collection when changing collection type

* use select instead of autocomplete for multi collection editor

* more select

* more select controls
2021-12-06 12:49:48 -06:00
Jason DoveandGitHub 3656e932d3 more song fixes (#529)
* use blurhash for default etv song backgrounds

* fix saving artwork blurhash

* fix song detail alignment

* rename song background files

* watermark path is always none here
2021-12-04 13:30:25 -06:00
Jason Dove 73887706ed update changelog for release v0.3.2-alpha [no ci] 2021-12-03 14:57:19 -06:00
Jason DoveandGitHub abc103308b optimize song artwork scanning (#527) 2021-12-03 13:40:55 -06:00
Jason DoveandGitHub 3773bbec19 use blurhash for song backgrounds (#526)
* generate blurhash for all local artwork

* use blurhash song background if available

* only write blur hash to disk once

* use multiple blur hashes

* update changelog

* fix song detail outline

* reset song metadata (artwork)
2021-12-03 12:30:47 -06:00
Jason DoveandGitHub e223d6a43f remove unused cli project (#525) [no ci] 2021-12-02 09:01:47 -06:00
Jason DoveandGitHub 8369111e31 update dependencies (#524) 2021-12-02 08:45:43 -06:00
Jason DoveandGitHub 35ba2bab2c fix unicode song metadata on windows (#523)
* test setting utf8 encoding with ffprobe

* use utf8 encoding for console (logging) output

* use proper sink package

* reset song metadata on windows

* fix nfo processing with missing year

* update changelog
2021-12-01 13:43:09 -06:00
Jason DoveandGitHub 094ed71ad0 fix docker builds with custom (local) nuget package (#520) 2021-11-30 20:49:02 -06:00
Jason DoveandGitHub 89e24b2b78 use custom log database backend that is more portable (#519) 2021-11-30 20:34:14 -06:00
Jason DoveandGitHub 848795af32 fix artwork upload on windows (#518)
* update changelog for release v0.3.1-alpha [no ci]

* fix artwork upload on windows
2021-11-30 12:23:24 -06:00
Jason DoveandGitHub 56f94f489a fix filler playout crash (#517) 2021-11-30 10:38:42 -06:00
Jason DoveandGitHub 475dc7660b fix artwork uploads (#516) 2021-11-29 14:34:18 -06:00
Jason DoveandGitHub db3dfbd446 disambiguate song search results (#515) 2021-11-27 21:37:55 -06:00
Jason DoveandGitHub b4c9cdbbfa use embedded song cover art (#514) 2021-11-27 21:08:18 -06:00
Jason DoveandGitHub 7f84933c0b index song genres (#513)
* add song genres to search index

* reset all song genre metadata

* update changelog and docs
2021-11-27 18:08:55 -06:00
Jason DoveandGitHub 1e35e9a5b0 use subtitles to display errors (#512)
* use subtitles to display errors

* fix margin calculation
2021-11-27 12:25:30 -06:00
Jason DoveandGitHub 7edf6f5d13 song cleanup (#511)
* refactor song background logic

* move song video generation

* move subtitle generation

* build ASS subtitles

* randomize song detail layout

* update changelog
2021-11-27 11:15:53 -06:00
Jason DoveandGitHub 919325033d use subtitles instead of drawtext for songs (#510) 2021-11-26 21:39:10 -06:00
Jason DoveandGitHub 2cb5252320 fix song banding (#509)
* increase spacing in song details; uniformly darken to eliminate banding

* this isn't needed anymore
2021-11-26 15:20:41 -06:00
Jason DoveandGitHub 015232fad6 song improvements (#508)
* fix song details margin and use dynamic font size

* sometimes use cover art color for song background
2021-11-26 13:23:28 -06:00
Jason DoveandGitHub af51b790b6 randomize cover art placement (#507) 2021-11-26 09:21:00 -06:00
Jason DoveandGitHub 9195ef7878 song fixes (#506)
* fix song page links

* show song artist in playout detail

* show more song details in channel guide
2021-11-26 08:49:04 -06:00
Jason Dove dfc4c7a284 update changelog for release v0.3.0-alpha [no ci] 2021-11-25 20:49:23 -06:00
Jason DoveandGitHub a6b15f68c9 randomize default backgrounds (#504)
* randomize default song backgrounds

* update docs
2021-11-25 20:19:26 -06:00
Jason DoveandGitHub 0edfb71f8d limit disk use and keep cover art aspect ratio (#502)
* use temp file pool to limit disk use

* keep aspect ratio and crop when scaling cover art for blurred background

* fix typo
2021-11-25 18:47:22 -06:00
Jason DoveandGitHub 21b90a1b6c fix songs with white backgrounds (#501) 2021-11-25 15:36:40 -06:00
Jason Dove 1582f5dd15 update changelog [no ci] 2021-11-25 13:37:33 -06:00
Jason DoveandGitHub fd3b72525d fix vaapi songs (#500) 2021-11-25 13:35:57 -06:00
Jason DoveandGitHub 55d1871d94 re-enable hardware acceleration for songs (#499) 2021-11-25 13:04:13 -06:00
Jason DoveandGitHub a90eb2d4de optimize generated video (#498)
* use different framerate flags

* pre-generate song image and always use software encoders

* fix tests
2021-11-25 12:31:57 -06:00
Jason DoveandGitHub ed3f1b1dad generate song video (#497)
* use blurred cover art as song background

* use channel watermark when cover art is unavailable

* add drawtext to song filter

* cleanup

* force song cover art as png

* fix songs on windows and qsv
2021-11-25 06:22:38 -06:00
Jason DoveandGitHub 8e08ff059f load embedded song metadata (#495)
* load embedded song metadata

* index song artist and song album

* reset all song metadata
2021-11-24 07:31:34 -06:00
Jason DoveandGitHub fb8c3a0453 disable autoscale when looping with vaapi or qsv (#494) 2021-11-23 13:25:23 -06:00
Jason DoveandGitHub e45fb67769 bug fixes (#493)
* don't align audio when playing songs

* fix grouping duration items in epg
2021-11-23 11:44:39 -06:00
Jason DoveandGitHub 3a40d6ce77 fix local library locking when adding paths (#492) 2021-11-23 10:54:34 -06:00
Jason DoveandGitHub ac048b72ae add cover art watermark source (#491)
* add cover art watermark source

* update changelog
2021-11-23 10:02:36 -06:00
Jason DoveandGitHub 852728c816 add songs libraries (#490)
* first pass at adding song libraries

* start handling optional video

* fix song playback

* fix song transitions

* add songs page to UI
2021-11-22 22:26:06 -06:00
Jason DoveandGitHub 096f2d42e8 properly fix database upgrade (#489) 2021-11-22 17:56:29 -06:00
Jason Dove 1b29e252ff update changelog for release v0.2.5-alpha [no ci] 2021-11-21 07:24:20 -06:00
Jason DoveandGitHub a4dc9bfb31 Ignore local plex guids (#488)
* ignore local plex guids

* update dependencies
2021-11-21 06:25:56 -06:00
Jason DoveandGitHub 184c21a91b optimize trakt matching (#487) 2021-11-21 06:13:28 -06:00
Jason DoveandGitHub 6ea3191cf8 fix playout building (#486) 2021-11-20 22:36:15 -06:00
Jason DoveandGitHub d487bbca08 include other video title in channel guide (#483) 2021-11-16 08:46:07 -06:00
Jason Dove 05034b47e2 update changelog for release v0.2.4-alpha [no ci] 2021-11-13 12:54:41 -06:00
Jason DoveandGitHub b0c85b6478 use scale_cuda instead of scale_npp (#481) 2021-11-13 09:06:02 -06:00
Jason DoveandGitHub f1356563da fix ef shared table warnings (#480) 2021-11-10 18:03:28 -06:00
Jason DoveandGitHub c0aad028a8 more dotnet 6 updates (#479) 2021-11-09 13:09:57 -06:00
Jason DoveandGitHub dae06ec0ef upgrade to dotnet 6 (#475) 2021-11-09 07:44:34 -06:00
Jason DoveandGitHub 72f452fd36 update dependencies (#474) 2021-11-09 06:16:26 -06:00
Jason Dove aaf832c0b6 update changelog for release v0.2.3-alpha [no ci] 2021-11-03 13:53:06 -05:00
Jason DoveandGitHub 08a18daf23 movie scanner should respect .etvignore files (#468) 2021-11-03 05:47:29 -05:00
Jason DoveandGitHub 90c1c61a09 fix bug with flood playout mode (#467) 2021-11-02 21:47:46 -05:00
Jason DoveandGitHub 053db71d44 fix decimal separator in ffmpeg apad filter syntax (#464) 2021-11-01 22:18:06 -05:00
Jason Dove 11f90f5d44 update changelog for release v0.2.2-alpha [no ci] 2021-10-30 17:49:30 -05:00
Jason DoveandGitHub bda4117655 allow per-episode folders in local show libraries (#462)
* allow per-episode folders in local show libraries

* fix subfolder etag generation
2021-10-30 12:54:07 -05:00
Jason Dove 3240703840 fix build 2021-10-30 12:24:27 -05:00
Jason DoveandGitHub 53a7570ba3 fix epg for multiple playout mode (#461) 2021-10-30 12:16:39 -05:00
Jason DoveandGitHub 0e789fd6d8 update dependencies and fix languageext deprecation warnings (#460) 2021-10-30 11:57:50 -05:00
Jason DoveandGitHub 0136de700c add global and channel fallback filler (#459)
* configure channel and global fallback filler

* play random item from configured channel/global fallback filler as needed
2021-10-30 11:45:40 -05:00
Jason DoveandGitHub 2ea0e64ac1 fix duration schedule item epg (#455) 2021-10-24 22:00:21 -05:00
Jason Dove 5993f23ec5 update changelog for release v0.2.1-alpha [no ci] 2021-10-24 19:46:33 -05:00
Jason DoveandGitHub 417f35a834 fix saving dynamic start time (#453) 2021-10-24 18:18:22 -05:00
Jason DoveandGitHub a74547997d scheduling fixes (#451)
* scheduling fixes

* restore plex service

* restore plex service part 2
2021-10-24 06:49:35 -05:00
Jason Dove a2f74dd284 update changelog for release v0.2.0-alpha [no ci] 2021-10-23 20:18:01 -05:00
Jason Dove 373daf9ce6 add basic filler docs [no docker] 2021-10-23 20:14:10 -05:00
Jason Dove 68693cffa0 use info log level for search index migration 2021-10-21 20:43:17 -05:00
Jason DoveandGitHub 6d147de2f3 filler rework (#449)
* add chapter statistics and new filler options

* refactor playout builder

* more refactor prep for filler

* rewrite schedulers

* refactor collectionkey

* add tail filler kind

* migrate tail filler to filler preset

* optionally show filler

* fix playout detail row count

* remove duration tail filler options

* implement tail and fallback in flood scheduler

* implement tail and fallback in one scheduler

* implement tail and fallback in multiple scheduler

* implement looping fallback filler

* more duration tests

* start to add post-roll filler to flood

* rework playoutitem filler tagging

* rework scheduler logging

* calculate whether configured filler will fit

* implement pre-roll and post-roll duration and count filler

* improve duration filler calculation

* add minutes to search index

* update channel guide to work with new filler

* add mid-roll filler

* don't clone enumerators for filler calculations

* support pre-roll and post-roll pad filler

* implement mid-roll pad filler

* allow clearing filler selections in schedule editor

* fix tests

* filler config validation

* use consistent time zone for tests
2021-10-21 20:23:14 -05:00
Jason DoveandGitHub f4a63a1a1a fix deleting jellyfin and emby movies (#447)
* fix deleting jellyfin and emby movies

* revert jf service change
2021-10-19 14:33:48 -05:00
Jason DoveandGitHub bc9d17ca25 show path replacement logs by default (#445) 2021-10-19 06:32:00 -05:00
Jason DoveandGitHub 42e13cbbaf fix generated streams with mpeg2video (#444) 2021-10-18 19:51:50 -05:00
Jason Dove 6cc61f3212 update changelog for release v0.1.5-alpha [no ci] 2021-10-18 17:55:34 -05:00
Jason DoveandGitHub 4cf44616a8 Include music video thumbnail in epg (#443)
* include music video thumbnail in epg

* update changelog
2021-10-18 17:53:58 -05:00
Jason DoveandGitHub 33aaadae68 multiple fixes to duration mode (#442) 2021-10-18 16:21:32 -05:00
Jason DoveandGitHub fe3f8e391e fix updating jellyfin and emby artwork (#440) 2021-10-17 05:36:00 -05:00
Jason DoveandGitHub 1a68dd040a find working plex connection on startup (#438) 2021-10-16 11:55:54 -05:00
Jason DoveandGitHub 67761c1a14 fix updating jellyfin and emby tv seasons (#437)
* fix updating jellyfin and emby tv seasons

* update changelog
2021-10-16 09:05:08 -05:00
Jason DoveandGitHub 1802f9d797 fix database migration (#436) 2021-10-15 11:31:40 -05:00
Jason DoveandGitHub 69354c9296 fix double scheduling (#435) 2021-10-15 09:14:53 -05:00
Jason Dove 0021e21b50 fix other video playback 2021-10-14 22:53:32 -05:00
Jason Dove cdf7765059 update changelog for release v0.1.4-alpha [no ci] 2021-10-14 15:00:24 -05:00
Jason DoveandGitHub 71658c448f update docs (#431) 2021-10-14 14:41:12 -05:00
Jason DoveandGitHub 3ecdd741a5 add guide mode to schedule items (#430) 2021-10-14 13:24:54 -05:00
Jason DoveandGitHub 0daeb844b9 add other videos library kind (#429) 2021-10-14 12:58:37 -05:00
Jason DoveandGitHub 22da19845b add filler option to duration playout mode (#428)
* add duration tail options to schedule items editor

* add naive filler scheduling

* fix duration item length in xmltv

* show offline image for unfilled duration tail

* fix tests

* update changelog

* update dependencies
2021-10-13 21:15:16 -05:00
Jason Dove 3a6d9e9f39 update changelog for release v0.1.3-alpha [no ci] 2021-10-13 15:17:23 -05:00
Jason DoveandGitHub 7ed4b8ae3c fix startup bug (#426) 2021-10-13 13:30:26 -05:00
Jason Dove be7311e620 update changelog for release v0.1.2-alpha [no ci] 2021-10-12 19:00:30 -05:00
Jason Dove 03be372070 update changelog [no ci] 2021-10-12 18:57:02 -05:00
Jason DoveandGitHub d196308ee9 fix ffmpeg profile editing (#421) 2021-10-12 18:39:51 -05:00
Jason Dove 3d68b0f055 fix vaapi migration 2021-10-12 18:22:09 -05:00
Jason Dove 37e32f06ad update changelog [no ci] 2021-10-12 17:56:56 -05:00
Jason DoveandGitHub c43ca2837d support radeon vaapi acceleration (#420) 2021-10-12 17:51:55 -05:00
Jason DoveandGitHub 992121f308 add more watermark locations (#419) 2021-10-12 07:19:52 -05:00
Jason DoveandGitHub 04adbfeffa add hls segmenter settings to optimize performance (#418)
* add hls segmenter settings to optimize performance

* use consistent setting defaults
2021-10-12 06:31:11 -05:00
Jason DoveandGitHub 1fc905c6ad upgrade vaapi to ffmpeg 4.4 (#417) 2021-10-11 22:26:32 -05:00
Jason DoveandGitHub 4b5dff2159 ffnvcodec fixes (#416) 2021-10-11 22:14:43 -05:00
Jason DoveandGitHub 2a5edf8214 ffmpeg 4.4 llvm nvidia fixes (#415) 2021-10-11 21:31:59 -05:00
Jason DoveandGitHub 69912c8cae support ffmpeg 4.4 (#414)
* support ffmpeg 4.4

* update changelog
2021-10-11 20:16:15 -05:00
Jason DoveandGitHub fd3de2d82a nvidia 10 bit fixes (#413) 2021-10-11 16:00:35 -05:00
Jason DoveandGitHub 6ba9404752 nvidia transcoding improvements (#412)
* nvidia transcoding fixes

* use yadif_cuda to deinterlace
2021-10-10 22:40:43 -05:00
Jason Dove db080375c5 update changelog for release v0.1.1-alpha [no ci] 2021-10-10 12:54:46 -05:00
Jason Dove 9abc7ad8b7 try to fix tests on windows 2021-10-10 12:39:08 -05:00
Jason DoveandGitHub 9e531a82d7 add some hls playlist filter tests (#411) 2021-10-10 12:33:02 -05:00
Jason DoveandGitHub d84bd2b948 upgrade nvidia docker image (#410) 2021-10-10 11:45:02 -05:00
Jason DoveandGitHub d7d3ec1235 add music video album to search index (#409)
* add music video album to search index

* update search docs
2021-10-10 10:28:35 -05:00
Jason Dove 742ac21ad7 update collection docs [no docker] 2021-10-10 07:53:29 -05:00
Jason DoveandGitHub 819b55e21f increase max hls segments (#408) 2021-10-10 06:47:24 -05:00
Jason DoveandGitHub cf5718c288 rework hls segmenter (#407)
* rework hls segmenter to start more quickly

* don't use realtime encoding for hls until we're at least a minute ahead

* ugly but functional playlist filtering
2021-10-09 22:46:38 -05:00
Jason DoveandGitHub adc7982955 reduce initial hls segmenter delay (#406) 2021-10-09 10:26:57 -05:00
Jason Dove 67a6f554d0 rename v0.0.63-alpha v0.1.0-alpha [no ci] 2021-10-08 18:44:49 -05:00
Jason Dove 609df217ae update changelog for release 63 [no ci] 2021-10-08 18:39:03 -05:00
Jason DoveandGitHub d3086264c7 unraid doc fixes (#405) 2021-10-08 18:31:22 -05:00
Jason DoveandGitHub 8cd9b23787 fix transcode folder preparation (#404) 2021-10-08 15:29:19 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc5c9e42ff Bump Serilog.Settings.Configuration from 3.2.0 to 3.3.0 (#403)
Bumps [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/serilog/serilog-settings-configuration/releases)
- [Changelog](https://github.com/serilog/serilog-settings-configuration/blob/dev/CHANGES.md)
- [Commits](https://github.com/serilog/serilog-settings-configuration/commits)

---
updated-dependencies:
- dependency-name: Serilog.Settings.Configuration
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-08 09:09:57 -05:00
Jason DoveandGitHub 2dd267e4db fix xmltv generation with missing episode metadata (#402) 2021-10-07 22:31:46 -05:00
Jason DoveandGitHub b069a21473 allow hls segmenter to run before framerate is known (#401) 2021-10-07 22:07:54 -05:00
Jason DoveandGitHub 6c8813ce22 add hls segmenter streaming mode (#400)
* hls segmenter wip

* log message

* close unused transcode sessions after 2 minutes

* use frame rate for 2s keyframes in hls segmenter

* add frame rate to media version

* fix segmenter framerate calculation

* automatically restart hls segmenter with next scheduled item

* cleanup

* update changelog

* decrease segmenter start delay
2021-10-07 21:42:29 -05:00
Jason DoveandGitHub b5de5e2b7f fix statistic updates (#399) 2021-10-07 19:56:41 -05:00
Jason DoveandGitHub 4b7da4e468 speed up builds by using base images (#398) 2021-10-06 06:06:13 -05:00
Jason DoveandGitHub ae8e795228 vaapi downsample 10bit hevc 8bit h264 (#397)
* vaapi downsample 10bit hevc to 8bit h264

* update changelog
2021-10-05 22:16:57 -05:00
Jason DoveandGitHub 334781485d use latest intel driver in vaapi docker images (#396)
* compile latest iHD driver

* cleanup to reduce image size

* update changelog
2021-10-05 20:24:26 -05:00
Jason Dove 27fefa1b38 update changelog for release 62 [no ci] 2021-10-05 15:28:45 -05:00
Jason DoveandGitHub fc3175591e use libx264 for all errors (#395) 2021-10-05 15:13:35 -05:00
Jason DoveandGitHub 3363d2c9d7 update plex paths when they are changed (#394) 2021-10-05 06:27:05 -05:00
Jason DoveandGitHub 1d5217fa84 support imdb ids from plex (#393) 2021-10-05 06:01:53 -05:00
Jason DoveandGitHub 904cdb8780 vaapi improvements (#392)
* add transcoding tests

* dont use full paths for transcoding tests

* add error details

* use 1-second videos for transcoding tests

* vaapi fixes

* include format in scale_vaapi

* more vaapi fixes

* unsupported errors

* fix unsupported checks

* maybe not failure?

* fix formatting

* ignore nvdec warnings

* update changelog

* fix tests
2021-10-02 13:42:44 -05:00
Jason Dove 85fee64565 update changelog [no ci] 2021-10-01 13:44:04 -05:00
Jason DoveandGitHub 13cfb9728f include season zero episode-num in xmltv (#391) 2021-10-01 13:42:46 -05:00
Jason DoveandGitHub 60b82876ea mudblazor updates (#390) 2021-09-30 17:40:26 -05:00
Jason DoveandGitHub a99249c375 revert nvenc changes (#389) 2021-09-30 17:35:44 -05:00
Jason Dove 36e6ef4c18 update changelog for release 60 2021-09-25 15:12:47 -05:00
Jason DoveandGitHub 21e53532c1 trakt season bug fixes (#386) 2021-09-25 14:55:42 -05:00
Jason DoveandGitHub a864d53327 add seasons to search index (#385)
* update trakt list items when re-adding existing list

* add seasons to search index
2021-09-25 14:01:35 -05:00
Jason DoveandGitHub e6446f9983 better trakt lists (#384)
* better trakt list support

* update dependencies

* revert unneeded brackets
2021-09-25 09:12:25 -05:00
Jason DoveandGitHub ad40213f90 fix synchronizing trakt lists that contain unreleased movies (#382) 2021-09-21 21:14:27 -05:00
Jason DoveandGitHub 45c6d20fd0 sync trakt list to collection (#381)
* sync trakt list to collection

* move trakt client id
2021-09-20 18:46:03 -05:00
Jason DoveandGitHub 5439db89a7 nvidia fixes (#380)
* nvidia fixes

* fix tests
2021-09-19 21:39:36 -05:00
Jason DoveandGitHub a39231bb5a fix local episode metadata update (#379) 2021-09-19 20:57:12 -05:00
Jason Dove 4c8584b517 try to fix develop versioning 2021-09-18 18:08:19 -05:00
Jason Dove ca8bcacbd3 update changelog for release 59 [no ci] 2021-09-18 14:35:01 -05:00
Jason DoveandGitHub f27286d1dd properly disable transcoding when unchecked in mpeg-ts mode (#378)
* properly disable transcoding in MPEG-TS mode

* update changelog
2021-09-18 14:21:49 -05:00
Jason Dove 23870b75f7 update changelog [no ci] 2021-09-18 14:02:49 -05:00
Jason Dove 7f5a91c643 include libva-x11-2 in vaapi docker image 2021-09-18 13:25:01 -05:00
Jason DoveandGitHub f1f50e883c add vaapi driver setting and health check (#377)
* add vaapi driver option

* add vaapi driver setting and health check
2021-09-18 13:00:36 -05:00
Jason Dove 7506f49f5b remove codeql [no docker] 2021-09-18 11:30:16 -05:00
Jason DoveandGitHub 944f1e4307 add scheduled playout rebuild (#376)
* configure scheduled playout rebuild

* implement scheduled playout rebuild

* remove variable
2021-09-18 11:23:58 -05:00
Jason DoveandGitHub f7de9ac5ea include intel-media-va-driver-non-free in vaapi image (#375)
* include intel-media-va-driver-non-free in vaapi image

* tweak changelog
2021-09-17 21:13:27 -05:00
Jason DoveandGitHub 1eb51ad2f4 add some health checks to home page (#374) 2021-09-17 17:59:59 -05:00
Jason DoveandGitHub c3e0aaf0b7 missing metadata fixes (#373) 2021-09-17 08:53:41 -05:00
1562 changed files with 316085 additions and 41398 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2021.1.3", "version": "2021.2.2",
"commands": [ "commands": [
"jb" "jb"
] ]
+18
View File
@@ -6,3 +6,21 @@ updates:
interval: daily interval: daily
assignees: assignees:
- jasongdove - jasongdove
- package-ecosystem: docker
directory: "/docker"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/nvidia"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/vaapi"
schedule:
interval: daily
assignees:
- jasongdove
+255
View File
@@ -0,0 +1,255 @@
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: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Cache NPM dependencies
uses: bahmutov/npm-install@v1.4.5
with:
working-directory: ErsatzTV/client-app
- 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: |
curl -o gon.zip -L -s "https://github.com/mitchellh/gon/releases/latest/download/gon_macos.zip"
unzip -o -q gon.zip
./gon -log-level=debug -log-json ./gon.json
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Cleanup
shell: bash
run: |
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
rm -r publish
rm -r ErsatzTV.app
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: ubuntu-latest
kind: linux
target: linux-arm64
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Cache NPM dependencies
uses: bahmutov/npm-install@v1.4.5
with:
working-directory: ErsatzTV/client-app
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- uses: suisei-cn/actions-download-file@v1
if: ${{ matrix.kind }} == "windows"
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build.7z"
target: ffmpeg/
- name: Build
shell: bash
run: |
# Define some variables for things we need
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
# Build Windows launcher
if [ "${{ matrix.kind }}" == "windows" ]; then
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
fi
# Download ffmpeg
if [ "${{ matrix.kind }}" == "windows" ]; then
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
rm -f "$release_name/ffplay.exe"
fi
# Pack files
if [ "${{ matrix.kind }}" == "windows" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
+40 -92
View File
@@ -1,110 +1,58 @@
name: Build name: Build
on: on:
workflow_dispatch: workflow_dispatch:
pull_request:
push: push:
branches: branches:
- main - main
jobs: jobs:
build_and_test: calculate_version:
runs-on: ${{ matrix.os }} name: Calculate version information
strategy: runs-on: ubuntu-latest
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
build_and_push:
name: Build & Publish to Docker Hub
needs: build_and_test
runs-on: ubuntu-latest
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
tag2="${tag:1}" tag2="${tag:1}"
short=$(git rev-parse --short HEAD) short=$(git rev-parse --short HEAD)
final="${tag2/prealpha/$short}" final="${tag2/alpha/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
- name: Set up Docker Buildx NVIDIA final="${tag/alpha/$short}"
uses: docker/setup-buildx-action@v1 echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
id: builder-nvidia echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
- name: Set up Docker Buildx VAAPI git_tag: ${{ env.GIT_TAG }}
uses: docker/setup-buildx-action@v1 artifacts_version: ${{ env.ARTIFACTS_VERSION }}
id: builder-vaapi info_version: ${{ env.INFO_VERSION }}
build_and_upload:
- name: Login to DockerHub uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
uses: docker/login-action@v1 needs: calculate_version
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_tag: develop
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
- name: Build and push base secrets:
uses: docker/build-push-action@v2 apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
with: apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
builder: ${{ steps.builder-base.outputs.name }} ac_username: ${{ secrets.AC_USERNAME }}
context: . ac_password: ${{ secrets.AC_PASSWORD }}
file: ./docker/Dockerfile gh_token: ${{ secrets.GITHUB_TOKEN }}
push: true build_and_push:
build-args: | uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
INFO_VERSION=${{ env.GIT_TAG }}-docker needs: calculate_version
tags: | with:
jasongdove/ersatztv:develop base_version: develop
jasongdove/ersatztv:${{ github.sha }} info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ github.sha }}
- name: Build and push nvidia secrets:
uses: docker/build-push-action@v2 docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
with: docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:develop-nvidia
jasongdove/ersatztv:${{ github.sha }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:develop-vaapi
jasongdove/ersatztv:${{ github.sha }}-vaapi
-71
View File
@@ -1,71 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '30 3 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'csharp' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+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 -2
View File
@@ -3,14 +3,13 @@ on:
push: push:
branches: branches:
- main - main
jobs: jobs:
build: build:
name: Deploy docs name: Deploy docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout master - name: Checkout master
uses: actions/checkout@v1 uses: actions/checkout@v2
- name: Deploy docs - name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master uses: mhausenblas/mkdocs-deploy-gh-pages@master
+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
+40 -129
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: maxOS
target: osx-x64
runs-on: ${{ matrix.os }}
steps: steps:
- name: Get the sources - name: Get the sources
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
shell: bash
run: |
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
elif [ "${{ matrix.target }}" == "linux-arm" ]; then
cp lib/linux-arm/* "$release_name/"
tar czvf "${release_name}.tar.gz" "$release_name"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
name: Build & Publish to Docker Hub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Extract Docker Tag
- name: Extract Git Tag
shell: bash shell: bash
run: | run: |
tag=$(git describe --tags --abbrev=0) tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-prealpha/}" >> $GITHUB_ENV echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
- name: Set up Docker Buildx Base shell: bash
uses: docker/setup-buildx-action@v1 run: |
id: builder-base tag=$(git describe --tags --abbrev=0)
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
- name: Set up Docker Buildx NVIDIA echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
uses: docker/setup-buildx-action@v1 outputs:
id: builder-nvidia git_tag: ${{ env.GIT_TAG }}
docker_tag: ${{ env.DOCKER_TAG }}
- name: Set up Docker Buildx VAAPI artifacts_version: ${{ env.ARTIFACTS_VERSION }}
uses: docker/setup-buildx-action@v1 info_version: ${{ env.INFO_VERSION }}
id: builder-vaapi build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
- name: Login to DockerHub needs: calculate_version
uses: docker/login-action@v1 with:
with: release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
username: ${{ secrets.DOCKER_HUB_USERNAME }} release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
- name: Build and push base apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
uses: docker/build-push-action@v2 apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
with: ac_username: ${{ secrets.AC_USERNAME }}
builder: ${{ steps.builder-base.outputs.name }} ac_password: ${{ secrets.AC_PASSWORD }}
context: . gh_token: ${{ secrets.GITHUB_TOKEN }}
file: ./docker/Dockerfile build_and_push:
push: true uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
build-args: | needs: calculate_version
INFO_VERSION=${{ env.GIT_TAG }}-docker with:
tags: | base_version: latest
jasongdove/ersatztv:latest info_version: ${{ needs.calculate_version.outputs.git_tag }}
jasongdove/ersatztv:${{ env.DOCKER_TAG }} tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
secrets:
- name: Build and push nvidia docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
uses: docker/build-push-action@v2 docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:latest-nvidia
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:latest-vaapi
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
+22
View File
@@ -0,0 +1,22 @@
name: Lint VueJS Files on PR Request
on:
pull_request:
jobs:
vue-lint:
runs-on: ubuntu-latest
steps:
# Checkout the current repo
- name: Checkout current repository
uses: actions/checkout@v2
# Setup NodeJS version 14
- name: Setup NodeJS V14.x.x
uses: actions/setup-node@v2
with:
node-version: '14'
# CD into the current client directory and lint and build the client
- name: Lint and Build the client
run: |
cd ./ErsatzTV/client-app/
npm ci --no-optional
npm run lint
npm run build --if-present
+1
View File
@@ -43,3 +43,4 @@ scripts/generate-api-sdk/swagger.json
docker-compose.override.yml docker-compose.override.yml
ErsatzTV/wwwroot/v2/
+3
View File
@@ -0,0 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:jasongdove/ErsatzTV-macOS.git
+475 -2
View File
@@ -5,6 +5,448 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
## [0.4.5-alpha] - 2022-03-29
### Fixed
- Fix streaming mode inconsistencies when `mode` parameter is unspecified
- Fix startup on Windows 7
### Added
- Add option to automatically deinterlace video when transcoding
- Previously, this was always enabled; the purpose of the option is to allow disabling any deinterlace filters
- Note that there is no performance gain to disabling the option with progressive content; filters are only ever applied to interlaced content
### Changed
- Change FFmpeg Profile video codec and audio codec text fields to select fields
- The appropriate video encoder will be determined based on the video format and hardware acceleration selections
- Remove FFmpeg Profile `Transcode`, `Normalize Video` and `Normalize Audio` settings
- All content will be transcoded and have audio and video normalized
- The only exception to this rule is `HLS Direct` streaming mode, which directly copies video and audio streams
- Always try to connect to Plex at `http://localhost:32400` even if that address isn't advertised by the Plex API
- If Plex isn't on the localhost, all other addresses will be checked as with previous releases
## [0.4.4-alpha] - 2022-03-10
### Fixed
- Fix `HLS Direct` streaming mode
- Fix bug with `HLS Segmenter` (and `MPEG-TS`) on Windows that caused errors at program boundaries
### Added
- Perform additional duration analysis on files with missing duration metadata
- Add `nouveau` VAAPI driver option
## [0.4.3-alpha] - 2022-03-05
### Fixed
- Fix song sorting with `Chronological` and `Shuffle In Order` playback orders
- Fix watermark on scaled and/or padded video with NVIDIA acceleration
- Fix playback of interlaced mpeg2video content with NVIDIA acceleration
- Fix playback of all interlaced content with QSV acceleration
- Fix adding songs to collections from search results page
- Fix bug scheduling mid-roll filler with content that contains one chapter
- No mid-roll filler will be inserted for content with zero or one chapters
- Fix thread sync bug with `HLS Segmenter` (and `MPEG-TS`) streaming modes
- Fix path replacement bug when media server path is left blank
### Added
- Add automated error reporting via Bugsnag
- This can be disabled by editing the `appsettings.json` file or by setting the `Bugsnag:Enable` environment variable to `false`
- Add `album_artist` to song metadata and to search index
- Display `album_artist` on some song videos when it's different than the `artist`
### Changed
- Framerate normalization will never normalize framerate below 24fps
- Instead, content with a lower framerate will be normalized up to 24fps
- `Shuffle In Order` will group songs by album artist instead of by track artist
## [0.4.2-alpha] - 2022-02-26
### Fixed
- Add improved but experimental transcoder logic, which can be toggled on and off in `Settings`
- Fix `HLS Segmenter` bug when source video packet contains no duration (`N/A`)
- Fix green line at the bottom of some content scaled using QSV acceleration
### Added
- Add configurable channel group (M3U) and categories (XMLTV)
- Add `Shuffle Schedule Items` option to schedule configuration
- When this is enabled, schedule items will be shuffled rather than looped in order
- **To support this, all playouts will be rebuilt (one time) after upgrading to this version**
### Changed
- Disable framerate normalization by default and on all ffmpeg profiles
- If framerate normalization is desired (not typically needed), it can be re-enabled manually
- Show watermarks over songs
- Hide unused local libraries
## [0.4.1-alpha] - 2022-02-10
### Fixed
- Normalize smart quotes in search queries as they are unsupported by the search library
- Fix incorrect watermark time calculations caused by working ahead in `HLS Segmenter`
- Fix ui crash adding empty path to local library
- Fix ui crash loading collection editor
- Properly flag items as `File Not Found` when local library path (folder) is missing from disk
- Fix playback bug with unknown pixel format
- Fix playback of interlaced mpeg2video on NVIDIA, VAAPI
### Added
- Include `Series` category tag for all episodes in XMLTV
- Include movie, episode (show), music video (artist) genres as `category` tags in XMLTV
- Add framerate normalization to `HLS Segmenter` and `MPEG-TS` streaming modes
- Add `HLS Segmenter Initial Segment Count` setting to allow segmenter to work ahead before allowing client playback
### Changed
- Intermittent watermarks will now fade in and out
- Show collection name in some playout build error messages
- Use hardware-accelerated filter for watermarks on NVIDIA
- Use hardware-accelerated deinterlace for some content on NVIDIA
## [0.4.0-alpha] - 2022-01-29
### Fixed
- Fix m3u `mode` query param to properly override streaming mode for all channels
- `segmenter` for `HLS Segmenter`
- `hls-direct` for `HLS Direct`
- `ts` for `MPEG-TS`
- `ts-legacy` for `MPEG-TS (Legacy)`
- omitting the `mode` parameter returns each channel as configured
- Link `File Not Found` health check to `Trash` page to allow deletion
- Fix `HLS Segmenter` streaming mode with multiple ffmpeg-based clients
- Jellyfin (web) and TiviMate (Android) were specifically tested
### Added
- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
- Also write logs to text files in the `logs` config subfolder
- Add `added_date` to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
- Add `added_inthelast`, `added_notinthelast` search field for relative added date queries
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
## [0.3.8-alpha] - 2022-01-23
### Fixed
- Fix issue preventing some versions of ffmpeg (usually 4.4.x) from streaming MPEG-TS (Legacy) channels at all
- The issue appears to be caused by using a thread count other than `1`
- Thread count is now forced to `1` for all streaming modes other than HLS Segmenter
- Fix bug with HLS Segmenter in cultures where `.` is a group/thousands separator
- Fix search results page crashing with some media kinds
- Always use MPEG-TS or MPEG-TS (Legacy) streaming mode with HDHR (Plex)
- Other configured modes will fall back to MPEG-TS when accessed by Plex
### Changed
- Upgrade ffmpeg from 4.4 to 5.0 in all docker images
- Upgrading from 4.4 to 5.0 is recommended for all installations
## [0.3.7-alpha] - 2022-01-17
### Fixed
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
- Fix double-click startup on mac
- Fix trakt list sync when show does not contain a year
- Properly unlock libraries when a scan is unable to be performed because ffmpeg or ffprobe have not been found
### Added
- Add trash system for local libraries to maintain collection and schedule integrity through media share outages
- When items are missing from disk, they will be flagged and present in the `Media` > `Trash` page
- The trash page can be used to permanently remove missing items from the database
- When items reappear at the expected location on disk, they will be unflagged and removed from the trash
- Add basic Mac hardware acceleration using VideoToolbox
### Changed
- Local libraries only: when items are missing from disk, they will be added to the trash and no longer removed from collections, etc.
- Show song thumbnail in song list
## [0.3.6-alpha] - 2022-01-10
### Fixed
- Properly index `minutes` field when adding new items during scan (vs when rebuilding index)
- Fix some nvenc edge cases where only padding is needed for normalization
- Properly overwrite environment variables for ffmpeg processes (`LIBVA_DRIVER_NAME`, `FFREPORT`)
### Added
- Add music video `artist` to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Changed
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
- This improves compatibility with many clients and also improves performance at program boundaries
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
- This mode will be removed in a future release
## [0.3.5-alpha] - 2022-01-05
### Fixed
- Fix bundled ffmpeg version in base docker image (NOT nvidia or vaapi) which prevented playback since v0.3.0-alpha
- Use software decoding for mpeg4 content when VAAPI acceleration is enabled
- Fix hardware acceleration health check to recognize QSV on non-Windows platforms
### Changed
- Treat `setsar` as a hardware filter, avoiding unneeded `hwdownload` and `hwupload` steps when padding isn't required
## [0.3.4-alpha] - 2021-12-21
### Fixed
- Fix other video and song scanners to include videos contained directly in top-level folders that are added to a library
- Allow saving ffmpeg troubleshooting reports on Windows
## [0.3.3-alpha] - 2021-12-12
### Fixed
- Fix bug with saving multiple blurhash versions for cover art; all cover art will be automatically rescanned
- Fix song detail margin when no cover art exists and no watermark exists
- Fix synchronizing virtual shows and seasons from Jellyfin
- Properly sort channels in M3U
### Changed
- Use blurhash of ErsatzTV colors instead of solid colors for default song backgrounds
- Use select control instead of autocomplete control in many places
- The autocomplete control is not intuitive to use and has focus bugs
## [0.3.2-alpha] - 2021-12-03
### Fixed
- Fix artwork upload on Windows
- Fix unicode song metadata on Windows
- Fix unicode console output on Windows
- Fix TV Show NFO metadata processing when `year` is missing
- Fix song detail outline to help legibility on white backgrounds
- Optimize song artwork scanning to prevent re-processing album artwork for each song
### Changed
- Use custom log database backend which should be more portable (i.e. work in osx-arm64)
- Use cover art blurhashes for song backgrounds instead of solid colors or box blur
## [0.3.1-alpha] - 2021-11-30
### Fixed
- Fix song page links in UI
- Show song artist in playout detail
- Include song artist and cover art in channel guide (xmltv)
- Use subtitles to display errors, which fixes many edge cases of unescaped characters
- Properly split song genre tags
- Properly display all songs that have an identical album and title
- Fix channel logo and watermark uploads
- Fix regression introduced with `v0.2.4-alpha` that caused some filler edge cases to crash the playout builder
### Added
- Add song genres to search index
- Use embedded song cover art when sidecar cover art is unavailable
### Changed
- Randomly place song cover art on left or right side of screen
- Randomly use a solid color from the cover art instead of blurred cover art for song background
- Randomly select song detail layout (large title/small artist or small artist/title/album)
## [0.3.0-alpha] - 2021-11-25
### Fixed
- Properly fix database incompatibility introduced with `v0.2.4-alpha` and partially fixed with `v0.2.5-alpha`
- The proper fix requires rebuilding all playouts, which will happen on startup after upgrading
- Fix local library locking/progress display when adding paths
- Fix grouping duration items in EPG when custom title is configured
### Added
- Add *experimental* `Songs` local libraries
- Like `Other Videos`, `Songs` require no metadata or particular folder layout, and will have tags added for each containing folder
- For Example, a song at `rock/band/1990 - Album/01 whatever.flac` will have the tags `rock`, `band` and `1990 - Album`, and the title `01 whatever`
- Songs will also have basic metadata read from embedded tags (album, artist, title)
- Video will be automatically generated for songs using metadata and cover art or watermarks if available
- Add support for `.webm` video files
## [0.2.5-alpha] - 2021-11-21
### Fixed
- Include other video title in channel guide (xmltv)
- Fix bug introduced with 0.2.4-alpha that caused some playouts to build from year 0
- Use less memory matching Trakt list items
### Added
- Build osx-arm64 packages on release
### Changed
- No longer warn about local Plex guids; they aren't used for Trakt matching and can be ignored
## [0.2.4-alpha] - 2021-11-13
### Changed
- Upgrade to dotnet 6
- Use `scale_cuda` instead of `scale_npp` for NVIDIA scaling in all cases
## [0.2.3-alpha] - 2021-11-03
### Fixed
- Fix bug with audio filter in cultures where `.` is a group/thousands separator
- Fix bug where flood playout mode would only schedule one item
- This would happen if the flood was followed by another flood with a fixed start time
### Added
- Support empty `.etvignore` file to instruct local movie scanner to ignore the containing folder
## [0.2.2-alpha] - 2021-10-30
### Fixed
- Fix EPG entries for Duration schedule items that play multiple items
- Fix EPG entries for Multiple schedule items that play more than one item
### Added
- Add fallback filler settings to Channel and global FFmpeg Settings
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
- Channel fallback filler
- Global fallback filler
- Generated `Channel Is Offline` error message video
### Changed
- Allow per-episode folders for local show libraries
- e.g. `Show Name\Season #\Episode #\Show Name - s#e#.mkv`
## [0.2.1-alpha] - 2021-10-24
### Fixed
- Fix saving dynamic start time on schedule items
## [0.2.0-alpha] - 2021-10-23
### Fixed
- Fix generated streams with mpeg2video
- Fix incorrect row count in playout detail table
- Fix deleting movies that have been removed from Jellyfin and Emby
- Fix bug that caused large unscheduled gaps in playouts
- This was caused by schedule items with a fixed start of midnight
### Added
- Add new filler system
- `Pre-Roll Filler` plays before each media item
- `Mid-Roll Filler` plays between media item chapters
- `Post-Roll Filler` plays after each media item
- `Tail Filler` plays after all media items, until the next media item
- `Fallback Filler` loops instead of default offline image to fill any remaining gaps
- Store chapter details with media statistics; this is needed to support mid-roll filler
- This requires re-ingesting statistics for all media items the first time this version is launched
- Add switch to show/hide filler in playout detail table
- Add `minutes` field to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Changed
- Change some debug log messages to info so they show by default again
- Remove tail collection options from `Duration` playout mode
- Show localized start time in schedule items tables
## [0.1.5-alpha] - 2021-10-18
### Fixed
- Fix double scheduling; this could happen if the app was shutdown during a playout build
- Fix updating Jellyfin and Emby TV seasons
- Fix updating Jellyfin and Emby artwork
- Fix Plex, Jellyfin, Emby worker crash attempting to sync library that no longer exists
- Fix bug with `Duration` mode scheduling when media items are too long to fit in the requested duration
### Added
- Include music video thumbnails in channel guide (xmltv)
### Changed
- Automatically find working Plex address on startup
- Automatically select schedule item in schedules that contain only one item
- Change default log level from `Debug` to `Information`
- The `Debug` log level can be enabled in the `appsettings.json` file for non-docker installs
- The `Debug` log level can be enabled by setting the environment variable `Serilog:MinimumLevel=Debug` for docker installs
## [0.1.4-alpha] - 2021-10-14
### Fixed
- Fix error message/offline stream continuity with channels that use HLS Segmenter
- Fix removing items from search index when folders are removed from local libraries
### Added
- Add `Other Video` local libraries
- Other video items require no metadata or particular folder layout, and will have tags added for each containing folder
- For Example, a video at `commercials/sd/1990/whatever.mkv` will have the tags `commercials`, `sd` and `1990`, and the title `whatever`
- Add filler `Tail Mode` option to `Duration` playout mode (in addition to existing `Offline` option)
- Filler collection will always be randomized (to fill as much time as possible)
- Filler will be hidden from channel guide, but visible in playout details in ErsatzTV
- Unfilled time will show offline image
- Add `Guide Mode` option to all schedule items
- `Normal` guide mode will show all scheduled items in the channel guide (xmltv)
- `Filler` guide mode will hide all scheduled items from the channel guide, and extend the end time for the previous item in the guide
## [0.1.3-alpha] - 2021-10-13
### Fixed
- Fix startup bug for some docker installations
## [0.1.2-alpha] - 2021-10-12
### Added
- Include more cuda (nvidia) filters in docker image
- Enable deinterlacing with nvidia using new `yadif_cuda` filter
- Add two HLS Segmenter settings: idle timeout and work-ahead limit
- `HLS Segmenter Idle Timeout` - the number of seconds to keep transcoding a channel while no requests have been received from any client
- This setting must be greater than or equal to 30 (seconds)
- `Work-Ahead HLS Segmenter Limit` - the number of segmenters (channels) that will work-ahead simultaneously (if multiple channels are being watched)
- "working ahead" means transcoding at full speed, which can take a lot of resources
- This setting must be greater than or equal to 0
- Add more watermark locations ("middle" of each side)
- Add `VAAPI Device` setting to ffmpeg profile to support installations with multiple video cards
- Add *experimental* `RadeonSI` option for `VAAPI Driver` and include mesa drivers in vaapi docker image
### Changed
- Upgrade ffmpeg from 4.3 to 4.4 in all docker images
- Upgrading from 4.3 to 4.4 is recommended for all installations
- Move `VAAPI Driver` from settings page to ffmpeg profile to support installations with multiple video cards
### Fixed
- Fix some transcoding edge cases with nvidia and pixel format `yuv420p10le`
## [0.1.1-alpha] - 2021-10-10
### Added
- Add music video album to search index
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Changed
- Remove forced initial delay from `HLS Segmenter` streaming mode
- Upgrade nvidia docker image from 18.04 to 20.04
## [0.1.0-alpha] - 2021-10-08
### Added
- Add *experimental* streaming mode `HLS Segmenter` (most similar to `HLS Hybrid`)
- This mode is intended to increase client compatibility and reduce issues at program boundaries
- If you want the temporary transcode files to be located on a particular drive, the docker path is `/root/.local/share/etv-transcode`
- Store frame rate with media statistics; this is needed to support HLS Segmenter
- This requires re-ingesting statistics for all media items the first time this version is launched
### Changed
- Use latest iHD driver (21.2.3 vs 20.1.1) in vaapi docker images
### Fixed
- Add downsampling to support transcoding 10-bit HEVC content with the h264_vaapi encoder
- Fix updating statistics when media items are replaced
- Fix XMLTV generation when scheduled episode is missing metadata
## [0.0.62-alpha] - 2021-10-05
### Added
- Support IMDB ids from Plex libraries, which may improve Trakt matching for some items
### Fixed
- Include Specials/Season 0 `episode-num` entry in XMLTV
- Fix some transcoding edge cases with VAAPI and pixel formats `yuv420p10le`, `yuv444p10le` and `yuv444p`
- Update Plex movie and episode paths when they are changed within Plex
- Always use `libx264` software encoder for error messages
## [0.0.61-alpha] - 2021-09-30
### Fixed
- Revert nvenc/cuda filter change from v60
## [0.0.60-alpha] - 2021-09-25
### Added
- Add Trakt list support under `Lists` > `Trakt Lists`
- Trakt lists can be added by url or by `user/list`
- To re-download a Trakt list, simply add it again (no need to delete)
- See `Logs` for unmatched item details
- Trakt lists can only be scheduled by using Smart Collections
- Add seasons to search index
- This is needed because Trakt lists can contain seasons
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Fixed
- Fix local television scanner to properly update episode metadata when NFO files have been added/changed
- Properly detect ffmpeg nvenc (cuda) support in Hardware Acceleration health check
- Fix nvenc/cuda filter for some yuv420p content
## [0.0.59-alpha] - 2021-09-18
### Added
- Add `Health Checks` table to home page to identify and surface common misconfigurations
- `FFmpeg Version` checks `ffmpeg` and `ffprobe` versions
- `FFmpeg Reports` checks whether ffmpeg troubleshooting reports are enabled since they can use a lot of disk space over time
- `Hardware Acceleration` checks whether channels that transcode are using acceleration methods that ffmpeg claims to support
- `Movie Metadata` checks whether all movies have metadata (fallback metadata counts as metadata)
- `Episode Metadata` checks whether all episodes have metadata (fallback metadata counts as metadata)
- `Zero Duration` checks whether all movies and episodes have a valid (non-zero) duration
- `VAAPI Driver` checks whether a vaapi driver preference is configured when using the vaapi docker image
- Add setting to each playout to schedule an automatic daily rebuild
- This is useful if the playout uses a smart collection with `released_onthisday`
### Fixed
- Fix docker vaapi support for newer Intel platforms (Gen 8+)
- This includes a new setting to force a particular vaapi driver (`iHD` or `i965`), as some Gen 8 or 9 hardware that is supported by both drivers will perform better with one or the other
- Fix scanning and indexing local movies and episodes without NFO metadata
- Fix displaying seasons for shows with no year (in metadata or in folder name)
- Fix "direct play" in MPEG-TS mode (copy audio and video stream when `Transcode` is unchecked)
## [0.0.58-alpha] - 2021-09-15 ## [0.0.58-alpha] - 2021-09-15
### Added ### Added
- Add `released_notinthelast` search field for relative release date queries - Add `released_notinthelast` search field for relative release date queries
@@ -594,8 +1036,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker. - Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.58-alpha...HEAD [Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.5-alpha...HEAD
[0.0.58-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...v0.0.57-alpha [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
[0.4.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.2-alpha...v0.4.3-alpha
[0.4.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.1-alpha...v0.4.2-alpha
[0.4.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...v0.4.1-alpha
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
[0.3.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.1-alpha...v0.3.2-alpha
[0.3.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.0-alpha...v0.3.1-alpha
[0.3.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
[0.2.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.2-alpha...v0.2.3-alpha
[0.2.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.1-alpha...v0.2.2-alpha
[0.2.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.0-alpha...v0.2.1-alpha
[0.2.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.5-alpha...v0.2.0-alpha
[0.1.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.4-alpha...v0.1.5-alpha
[0.1.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.3-alpha...v0.1.4-alpha
[0.1.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.2-alpha...v0.1.3-alpha
[0.1.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.1-alpha...v0.1.2-alpha
[0.1.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.1.0-alpha...v0.1.1-alpha
[0.1.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.62-alpha...v0.1.0-alpha
[0.0.62-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.61-alpha...v0.0.62-alpha
[0.0.61-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.60-alpha...v0.0.61-alpha
[0.0.60-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.59-alpha...v0.0.60-alpha
[0.0.59-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.58-alpha...v0.0.59-alpha
[0.0.58-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.57-alpha...v0.0.58-alpha
[0.0.57-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha [0.0.57-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.56-alpha...v0.0.57-alpha
[0.0.56-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha [0.0.56-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.55-alpha...v0.0.56-alpha
[0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha [0.0.55-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.0.54-alpha...v0.0.55-alpha
+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="CliWrap" Version="3.4.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>
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,81 @@
using ErsatzTV.Core;
using System.Diagnostics;
using CliWrap;
namespace ErsatzTV_Windows;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly CancellationTokenSource _tokenSource;
public TrayApplicationContext()
{
_trayIcon = new NotifyIcon
{
Icon = new Icon("./Ersatztv.ico"),
ContextMenuStrip = new ContextMenuStrip(),
Visible = true
};
_tokenSource = new CancellationTokenSource();
AddMenuItem("Launch Web UI", LaunchWebUI);
AddMenuItem("Show Logs", ShowLogs);
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
AddMenuItem("Exit", Exit);
string folder = AppContext.BaseDirectory;
string exe = Path.Combine(folder, "ErsatzTV.exe");
if (File.Exists(exe))
{
Cli.Wrap(exe)
.WithWorkingDirectory(folder)
.WithValidation(CommandResultValidation.None)
.ExecuteAsync(_tokenSource.Token);
}
}
private void AddMenuItem(string name, EventHandler action)
{
var item = new ToolStripMenuItem(name);
item.Click += action;
_trayIcon.ContextMenuStrip.Items.Add(item);
}
private void LaunchWebUI(object? sender, EventArgs e)
{
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = "http://localhost:8409";
process.Start();
}
private void ShowLogs(object? sender, EventArgs e)
{
if (!Directory.Exists(FileSystemLayout.LogsFolder))
{
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
}
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
process.Start();
}
protected override void Dispose(bool disposing)
{
_tokenSource?.Cancel();
base.Dispose(disposing);
}
private void Exit(object? sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
_trayIcon.Visible = false;
Application.Exit();
}
}
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
+13 -15
View File
@@ -1,16 +1,14 @@
using System.Collections.Generic; using System.Globalization;
using System.Globalization;
namespace ErsatzTV.Application.Artists namespace ErsatzTV.Application.Artists;
{
public record ArtistViewModel( public record ArtistViewModel(
string Name, string Name,
string Disambiguation, string Disambiguation,
string Biography, string Biography,
string Thumbnail, string Thumbnail,
string FanArt, string FanArt,
List<string> Genres, List<string> Genres,
List<string> Styles, List<string> Styles,
List<string> Moods, List<string> Moods,
List<CultureInfo> Languages); List<CultureInfo> Languages);
}
+35 -41
View File
@@ -1,46 +1,40 @@
using System; using System.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Artists namespace ErsatzTV.Application.Artists;
internal static class Mapper
{ {
internal static class Mapper internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{ {
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages) ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
{ return new ArtistViewModel(
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head(); metadata.Title,
return new ArtistViewModel( metadata.Disambiguation,
metadata.Title, metadata.Biography,
metadata.Disambiguation, Artwork(metadata, ArtworkKind.Thumbnail),
metadata.Biography, Artwork(metadata, ArtworkKind.FanArt),
Artwork(metadata, ArtworkKind.Thumbnail), metadata.Genres.Map(g => g.Name).ToList(),
Artwork(metadata, ArtworkKind.FanArt), metadata.Styles.Map(s => s.Name).ToList(),
metadata.Genres.Map(g => g.Name).ToList(), metadata.Moods.Map(m => m.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(), LanguagesForArtist(languages));
metadata.Moods.Map(m => m.Name).ToList(),
LanguagesForArtist(languages));
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.ToList();
}
} }
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.ToList();
}
}
@@ -1,8 +1,5 @@
using System.Collections.Generic; using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.MediaItems;
using MediatR;
namespace ErsatzTV.Application.Artists.Queries namespace ErsatzTV.Application.Artists;
{
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>; public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
}
@@ -1,29 +1,22 @@
using System.Collections.Generic; using ErsatzTV.Application.MediaItems;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaItems.Mapper; using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.Artists.Queries namespace ErsatzTV.Application.Artists;
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
{ {
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>> private readonly IArtistRepository _artistRepository;
{
private readonly IArtistRepository _artistRepository;
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository; public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
public Task<List<NamedMediaItemViewModel>> Handle( public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request, GetAllArtists request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_artistRepository.GetAllArtists() _artistRepository.GetAllArtists()
.Map( .Map(
list => list.Filter( list => list.Filter(
a => !string.IsNullOrWhiteSpace( a => !string.IsNullOrWhiteSpace(
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty)))) a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).ToList());
} }
}
@@ -1,7 +1,3 @@
using LanguageExt; namespace ErsatzTV.Application.Artists;
using MediatR;
namespace ErsatzTV.Application.Artists.Queries public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
{
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
}
@@ -1,38 +1,32 @@
using System.Collections.Generic; using ErsatzTV.Core.Domain;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Artists.Mapper; using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists.Queries namespace ErsatzTV.Application.Artists;
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
{ {
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>> private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{ {
private readonly IArtistRepository _artistRepository; _artistRepository = artistRepository;
private readonly ISearchRepository _searchRepository; _searchRepository = searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
} }
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
}
@@ -1,14 +1,17 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels namespace ErsatzTV.Application.Channels;
{
public record ChannelViewModel( public record ChannelViewModel(
int Id, int Id,
string Number, string Number,
string Name, string Name,
int FFmpegProfileId, string Group,
string Logo, string Categories,
string PreferredLanguageCode, int FFmpegProfileId,
StreamingMode StreamingMode, string Logo,
int? WatermarkId); string PreferredLanguageCode,
} StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
int PlayoutCount);
@@ -1,17 +1,17 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
{
public record CreateChannel public record CreateChannel
( (
string Name, string Name,
string Number, string Number,
int FFmpegProfileId, string Group,
string Logo, string Categories,
string PreferredLanguageCode, int FFmpegProfileId,
StreamingMode StreamingMode, string Logo,
int? WatermarkId) : IRequest<Either<BaseError, CreateChannelResult>>; string PreferredLanguageCode,
} StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
@@ -1,135 +1,155 @@
using System; using System.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{ {
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>> private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory; return await LanguageExtensions.Apply(validation, c => PersistChannel(dbContext, c));
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request),
await WatermarkMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private static Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
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);
return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"),
() =>
{
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
TvContext dbContext,
CreateChannel createChannel) =>
dbContext.FFmpegProfiles
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => createChannel.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
private static async Task<Validation<BaseError, Option<int>>> WatermarkMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.WatermarkId is null)
{
return Option<int>.None;
}
return await dbContext.ChannelWatermarks
.CountAsync(w => w.Id == createChannel.WatermarkId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.WatermarkId))
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
}
} }
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request),
await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private static Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
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);
return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"),
() =>
{
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
TvContext dbContext,
CreateChannel createChannel) =>
dbContext.FFmpegProfiles
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => createChannel.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
private static async Task<Validation<BaseError, Option<int>>> WatermarkMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.WatermarkId is null)
{
return Option<int>.None;
}
return await dbContext.ChannelWatermarks
.CountAsync(w => w.Id == createChannel.WatermarkId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.WatermarkId))
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
}
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
{
return Option<int>.None;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
{
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId); public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
}
@@ -1,9 +1,5 @@
using System.Threading.Tasks; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
{
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>; public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
}
@@ -1,28 +1,23 @@
using System.Threading; using ErsatzTV.Core;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>>
{ {
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>> private readonly IChannelRepository _channelRepository;
{
private readonly IChannelRepository _channelRepository;
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) => public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) =>
(await ChannelMustExist(request)) (await ChannelMustExist(request))
.Map(DoDeletion) .Map(DoDeletion)
.ToEither<Task>(); .ToEither<Task>();
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId); private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId);
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) => private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) =>
(await _channelRepository.Get(deleteChannel.ChannelId)) (await _channelRepository.Get(deleteChannel.ChannelId))
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.") .ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
.Map(c => c.Id); .Map(c => c.Id);
} }
}
@@ -1,18 +1,18 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
{
public record UpdateChannel public record UpdateChannel
( (
int ChannelId, int ChannelId,
string Name, string Name,
string Number, string Number,
int FFmpegProfileId, string Group,
string Logo, string Categories,
string PreferredLanguageCode, int FFmpegProfileId,
StreamingMode StreamingMode, string Logo,
int? WatermarkId) : IRequest<Either<BaseError, ChannelViewModel>>; string PreferredLanguageCode,
} StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
@@ -1,121 +1,115 @@
using System; using System.Globalization;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper; using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands namespace ErsatzTV.Application.Channels;
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{ {
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>> private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken)
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
UpdateChannel updateChannel) =>
dbContext.Channels
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
int matchId = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
.Match(c => c.Id, () => updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
}
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
} }
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.Group = update.Group;
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
UpdateChannel updateChannel) =>
dbContext.Channels
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
int matchId = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
.Match(c => c.Id, () => updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
}
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
}
+40 -23
View File
@@ -1,28 +1,45 @@
using System.Linq; using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels namespace ErsatzTV.Application.Channels;
internal static class Mapper
{ {
internal static class Mapper internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
{ new(
internal static ChannelViewModel ProjectToViewModel(Channel channel) => channel.Id,
new( channel.Number,
channel.Id, channel.Name,
channel.Number, channel.Group,
channel.Name, channel.Categories,
channel.FFmpegProfileId, channel.FFmpegProfileId,
GetLogo(channel), GetLogo(channel),
channel.PreferredLanguageCode, channel.PreferredLanguageCode,
channel.StreamingMode, channel.StreamingMode,
channel.WatermarkId); channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0);
private static string GetLogo(Channel channel) => internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo)) new(
.Match(a => a.Path, string.Empty); channel.Id,
channel.Number,
channel.Name,
channel.FFmpegProfile.Name,
channel.PreferredLanguageCode,
GetStreamingMode(channel));
private static string GetWatermark(Channel channel) => private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark)) Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty); .Match(a => a.Path, string.Empty);
}
} private static string GetStreamingMode(Channel channel) =>
channel.StreamingMode switch
{
StreamingMode.TransportStream => "MPEG-TS (Legacy)",
StreamingMode.TransportStreamHybrid => "MPEG-TS",
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
_ => throw new ArgumentOutOfRangeException()
};
}
@@ -1,7 +1,3 @@
using System.Collections.Generic; namespace ErsatzTV.Application.Channels;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries public record GetAllChannels : IRequest<List<ChannelViewModel>>;
{
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.Channels;
namespace ErsatzTV.Application.Channels;
public record GetAllChannelsForApi : IRequest<List<ChannelResponseModel>>;
@@ -0,0 +1,21 @@
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi, List<ChannelResponseModel>>
{
private readonly IChannelRepository _channelRepository;
public GetAllChannelsForApiHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelResponseModel>> Handle(
GetAllChannelsForApi request,
CancellationToken cancellationToken)
{
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
return channels.Map(ProjectToResponseModel).ToList();
}
}
@@ -1,21 +1,14 @@
using System.Collections.Generic; using ErsatzTV.Core.Interfaces.Repositories;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
using static ErsatzTV.Application.Channels.Mapper; using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{ {
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>> private readonly IChannelRepository _channelRepository;
{
private readonly IChannelRepository _channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) => public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList(); Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
} }
}
@@ -1,7 +1,3 @@
using LanguageExt; namespace ErsatzTV.Application.Channels;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
{
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
}
@@ -1,20 +1,15 @@
using System.Threading; using ErsatzTV.Core.Interfaces.Repositories;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Channels.Mapper; using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{ {
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>> private readonly IChannelRepository _channelRepository;
{
private readonly IChannelRepository _channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) => public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id) _channelRepository.Get(request.Id)
.MapT(ProjectToViewModel); .MapT(ProjectToViewModel);
} }
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelByNumber(string ChannelNumber) : IRequest<Option<ChannelViewModel>>;
@@ -0,0 +1,14 @@
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
public GetChannelByNumberHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;
@@ -0,0 +1,102 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Channels;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<GetChannelFramerateHandler> _logger;
public GetChannelFramerateHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<GetChannelFramerateHandler> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
if (result < 24)
{
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
24,
result);
return 24;
}
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
return None;
}
private int ParseFrameRate(string frameRate)
{
if (!int.TryParse(frameRate, out int fr))
{
string[] split = (frameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
}
}
return fr;
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Iptv; using ErsatzTV.Core.Iptv;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
{
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>; public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
}
@@ -1,20 +1,15 @@
using System.Threading; using ErsatzTV.Core.Interfaces.Repositories;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv; using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
{ {
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide> private readonly IChannelRepository _channelRepository;
{
private readonly IChannelRepository _channelRepository;
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) => public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide() _channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels)); .Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
} }
}
@@ -1,8 +1,5 @@
using System.Collections.Generic; using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Hdhr;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
{
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>; public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
}
@@ -1,22 +1,15 @@
using System.Collections.Generic; using ErsatzTV.Core.Hdhr;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
{ {
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>> private readonly IChannelRepository _channelRepository;
{
private readonly IChannelRepository _channelRepository;
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository; public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) => public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll() _channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList()); .Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
} }
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Iptv; using ErsatzTV.Core.Iptv;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries 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>;
}
@@ -1,48 +1,50 @@
using System.Collections.Generic; using ErsatzTV.Core.Domain;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv; using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries namespace ErsatzTV.Application.Channels;
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
{ {
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist> private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{ {
private readonly IChannelRepository _channelRepository; var result = new List<Channel>();
foreach (Channel channel in channels)
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{ {
var result = new List<Channel>(); switch (mode.ToLowerInvariant())
foreach (Channel channel in channels)
{ {
switch (mode.ToLowerInvariant()) case "segmenter":
{ channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
case "hls-direct": result.Add(channel);
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect; break;
result.Add(channel); case "hls-direct":
break; channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
case "ts": result.Add(channel);
channel.StreamingMode = StreamingMode.TransportStream; break;
result.Add(channel); case "ts-legacy":
break; channel.StreamingMode = StreamingMode.TransportStream;
default: result.Add(channel);
result.Add(channel); break;
break; case "ts":
} channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default:
result.Add(channel);
break;
} }
return result;
} }
return result;
} }
} }
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
{
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>; public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
}
@@ -1,21 +1,17 @@
using System.Threading; using ErsatzTV.Core.Interfaces.Repositories;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
{ {
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit> private readonly IConfigElementRepository _configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{ {
private readonly IConfigElementRepository _configElementRepository; await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
} }
} }
@@ -1,7 +1,5 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
{
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>; public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,33 +1,28 @@
using System.Threading; using ErsatzTV.Core;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{ {
public class UpdateLibraryRefreshIntervalHandler : private readonly IConfigElementRepository _configElementRepository;
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public Task<Either<BaseError, Unit>> Handle( public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request, UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval)) .MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) => private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval) Optional(request.LibraryRefreshInterval)
.Filter(lri => lri > 0) .Where(lri => lri > 0)
.Map(_ => Unit.Default) .Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero") .ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask(); .AsTask();
} }
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
{
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>; public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,66 +1,59 @@
using System.Collections.Generic; using System.Threading.Channels;
using System.Linq; using ErsatzTV.Application.Playouts;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands namespace ErsatzTV.Application.Configuration;
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{ {
public class private readonly IConfigElementRepository _configElementRepository;
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>> private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
private readonly IConfigElementRepository _configElementRepository; _configElementRepository = configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory; _dbContextFactory = dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; _workerChannel = workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Filter(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
} }
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Configuration namespace ErsatzTV.Application.Configuration;
{
public record ConfigElementViewModel(string Key, string Value); public record ConfigElementViewModel(string Key, string Value);
}
+6 -7
View File
@@ -1,10 +1,9 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Configuration namespace ErsatzTV.Application.Configuration;
internal static class Mapper
{ {
internal static class Mapper internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
{ new(element.Key, element.Value);
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) => }
new(element.Key, element.Value);
}
}
@@ -1,8 +1,5 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries namespace ErsatzTV.Application.Configuration;
{
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>; public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
}
@@ -1,22 +1,17 @@
using System.Threading; using ErsatzTV.Core.Interfaces.Repositories;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Configuration.Mapper; using static ErsatzTV.Application.Configuration.Mapper;
namespace ErsatzTV.Application.Configuration.Queries namespace ErsatzTV.Application.Configuration;
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{ {
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>> private readonly IConfigElementRepository _configElementRepository;
{
private readonly IConfigElementRepository _configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) => public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public Task<Option<ConfigElementViewModel>> Handle( public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request, GetConfigElementByKey request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel); _configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
} }
}
@@ -1,6 +1,3 @@
using MediatR; namespace ErsatzTV.Application.Configuration;
namespace ErsatzTV.Application.Configuration.Queries public record GetLibraryRefreshInterval : IRequest<int>;
{
public record GetLibraryRefreshInterval : IRequest<int>;
}
@@ -1,21 +1,16 @@
using System.Threading; using ErsatzTV.Core.Domain;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries namespace ErsatzTV.Application.Configuration;
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{ {
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int> private readonly IConfigElementRepository _configElementRepository;
{
private readonly IConfigElementRepository _configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) => public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) => public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval) _configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6)); .Map(result => result.IfNone(6));
} }
}
@@ -1,6 +1,3 @@
using MediatR; namespace ErsatzTV.Application.Configuration;
namespace ErsatzTV.Application.Configuration.Queries public record GetPlayoutDaysToBuild : IRequest<int>;
{
public record GetPlayoutDaysToBuild : IRequest<int>;
}
@@ -1,21 +1,16 @@
using System.Threading; using ErsatzTV.Core.Domain;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries namespace ErsatzTV.Application.Configuration;
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{ {
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int> private readonly IConfigElementRepository _configElementRepository;
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) => public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository; _configElementRepository = configElementRepository;
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) => public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild) _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2)); .Map(result => result.IfNone(2));
} }
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>; public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,46 +1,41 @@
using System.Collections.Generic; using ErsatzTV.Core;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
{ {
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>> private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{ {
private readonly IEmbySecretStore _embySecretStore; _mediaSourceRepository = mediaSourceRepository;
private readonly IEntityLocker _entityLocker; _embySecretStore = embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository; _entityLocker = entityLocker;
private readonly ISearchIndex _searchIndex; _searchIndex = searchIndex;
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_entityLocker = entityLocker;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
return Unit.Default;
}
} }
}
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
return Unit.Default;
}
}
@@ -1,8 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>; public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,60 +1,56 @@
using System.Threading; using System.Threading.Channels;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
{ {
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>> private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{ {
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel; _embySecretStore = embySecretStore;
private readonly IEmbyApiClient _embyApiClient; _embyApiClient = embyApiClient;
private readonly IEmbySecretStore _embySecretStore; _mediaSourceRepository = mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository; _channel = channel;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
} }
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
}
@@ -1,8 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>, public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest; IEmbyBackgroundServiceRequest;
}
@@ -1,118 +1,111 @@
using System.Collections.Generic; using ErsatzTV.Core;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
{ {
public class private readonly IEmbyApiClient _embyApiClient;
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>> private readonly IEmbySecretStore _embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
{ {
private readonly IEmbyApiClient _embyApiClient; _mediaSourceRepository = mediaSourceRepository;
private readonly IEmbySecretStore _embySecretStore; _embySecretStore = embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger; _embyApiClient = embyApiClient;
private readonly IMediaSourceRepository _mediaSourceRepository; _logger = logger;
private readonly ISearchIndex _searchIndex; _searchIndex = searchIndex;
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_logger = logger;
_searchIndex = searchIndex;
}
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries 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(embyMediaSource, 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)
.Filter(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
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);
return Task.CompletedTask;
});
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
} }
}
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries 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(embyMediaSource, 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<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
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);
return Task.CompletedTask;
});
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
@@ -1,23 +1,20 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
IEmbyBackgroundServiceRequest
{ {
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>, int EmbyLibraryId { get; }
IEmbyBackgroundServiceRequest bool ForceScan { get; }
{
int EmbyLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
} }
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
@@ -1,181 +1,184 @@
using System; using ErsatzTV.Core;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyLibraryByIdHandler :
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{ {
public class SynchronizeEmbyLibraryByIdHandler : private readonly IConfigElementRepository _configElementRepository;
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>, private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyMovieLibraryScanner embyMovieLibraryScanner,
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
ILibraryRepository libraryRepository,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{ {
private readonly IConfigElementRepository _configElementRepository; _mediaSourceRepository = mediaSourceRepository;
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner; _embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_logger = logger;
}
private readonly IEmbySecretStore _embySecretStore; public Task<Either<BaseError, string>> Handle(
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner; ForceSynchronizeEmbyLibraryById request,
private readonly IEntityLocker _entityLocker; CancellationToken cancellationToken) => Handle(request);
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository; public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
public SynchronizeEmbyLibraryByIdHandler( private Task<Either<BaseError, string>>
IMediaSourceRepository mediaSourceRepository, Handle(ISynchronizeEmbyLibraryById request) =>
IEmbySecretStore embySecretStore, Validate(request)
IEmbyMovieLibraryScanner embyMovieLibraryScanner, .MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner, .Bind(v => v.ToEitherAsync());
ILibraryRepository libraryRepository,
IEntityLocker entityLocker, private async Task<Unit> Synchronize(RequestParameters parameters)
IConfigElementRepository configElementRepository, {
ILogger<SynchronizeEmbyLibraryByIdHandler> logger) var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{ {
_mediaSourceRepository = mediaSourceRepository; switch (parameters.Library.MediaKind)
_embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
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)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{ {
switch (parameters.Library.MediaKind) case LibraryMediaKind.Movies:
{ await _embyMovieLibraryScanner.ScanLibrary(
case LibraryMediaKind.Movies: parameters.ConnectionParameters.ActiveConnection.Address,
await _embyMovieLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ApiKey,
parameters.ConnectionParameters.ActiveConnection.Address, parameters.Library,
parameters.ConnectionParameters.ApiKey, parameters.FFmpegPath,
parameters.Library, parameters.FFprobePath);
parameters.FFprobePath); break;
break; case LibraryMediaKind.Shows:
case LibraryMediaKind.Shows: await _embyTelevisionLibraryScanner.ScanLibrary(
await _embyTelevisionLibraryScanner.ScanLibrary( parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ActiveConnection.Address, parameters.ConnectionParameters.ApiKey,
parameters.ConnectionParameters.ApiKey, parameters.Library,
parameters.Library, parameters.FFmpegPath,
parameters.FFprobePath); parameters.FFprobePath);
break; break;
}
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
} }
_entityLocker.UnlockLibrary(parameters.Library.Id); parameters.Library.LastScan = DateTime.UtcNow;
return Unit.Default; await _libraryRepository.UpdateLastScan(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
} }
private async Task<Validation<BaseError, RequestParameters>> Validate( _entityLocker.UnlockLibrary(parameters.Library.Id);
ISynchronizeEmbyLibraryById request) => return Unit.Default;
(await ValidateConnection(request), await EmbyLibraryMustExist(request), }
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply( private async Task<Validation<BaseError, RequestParameters>> Validate(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters( ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFmpegPath(), await ValidateFFprobePath())
.Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffmpegPath, ffprobePath) =>
new RequestParameters(
connectionParameters, connectionParameters,
embyLibrary, embyLibrary,
request.ForceScan, request.ForceScan,
libraryRefreshInterval, libraryRefreshInterval,
ffmpegPath,
ffprobePath ffprobePath
)); ));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection( private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizeEmbyLibraryById request) => ISynchronizeEmbyLibraryById request) =>
EmbyMediaSourceMustExist(request) EmbyMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection) .BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey); .BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist( private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
ISynchronizeEmbyLibraryById request) => ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId) _mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId)
.Map( .Map(
v => v.ToValidation<BaseError>( v => v.ToValidation<BaseError>(
$"Emby media source for library {request.EmbyLibraryId} does not exist.")); $"Emby media source for library {request.EmbyLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection( private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource) EmbyMediaSource embyMediaSource)
{ {
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone(); Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection)) return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active 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)
.Filter(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
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(
ConnectionParameters ConnectionParameters,
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath);
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
} }
}
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 Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFmpegPath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(
ffmpegPath =>
ffmpegPath.ToValidation<BaseError>("FFmpeg path does not exist on the file system"));
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(
ConnectionParameters ConnectionParameters,
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
string FFmpegPath,
string FFprobePath);
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
@@ -1,11 +1,7 @@
using System.Collections.Generic; using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>, public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest; IEmbyBackgroundServiceRequest;
}
@@ -1,41 +1,35 @@
using System.Collections.Generic; using System.Threading.Channels;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
{ {
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources, private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
Either<BaseError, List<EmbyMediaSource>>> private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{ {
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel; _mediaSourceRepository = mediaSourceRepository;
private readonly IMediaSourceRepository _mediaSourceRepository; _channel = channel;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
return mediaSources;
}
} }
}
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
return mediaSources;
}
}
@@ -1,11 +1,8 @@
using System.Collections.Generic; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems); public record UpdateEmbyLibraryPreferences
} (List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
@@ -1,42 +1,36 @@
using System.Collections.Generic; using ErsatzTV.Core;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search; using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
{ {
public class private readonly IMediaSourceRepository _mediaSourceRepository;
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences, private readonly ISearchIndex _searchIndex;
Either<BaseError, Unit>>
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{ {
private readonly IMediaSourceRepository _mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
private readonly ISearchIndex _searchIndex; _searchIndex = searchIndex;
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
return Unit.Default;
}
} }
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
return Unit.Default;
}
}
@@ -1,12 +1,9 @@
using System.Collections.Generic; using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
{
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath); public record UpdateEmbyPathReplacements(
} int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
@@ -1,55 +1,49 @@
using System.Collections.Generic; using ErsatzTV.Core;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands namespace ErsatzTV.Application.Emby;
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
{ {
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements, private readonly IMediaSourceRepository _mediaSourceRepository;
Either<BaseError, Unit>>
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateEmbyPathReplacements request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
private Task<Unit> MergePathReplacements(
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{ {
private readonly IMediaSourceRepository _mediaSourceRepository; embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) => var incoming = request.PathReplacements.Map(Project).ToList();
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, Unit>> Handle( var toAdd = incoming.Filter(r => r.Id < 1).ToList();
UpdateEmbyPathReplacements request, var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
CancellationToken cancellationToken) => var toUpdate = incoming.Except(toAdd).ToList();
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
private Task<Unit> MergePathReplacements( return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
var incoming = request.PathReplacements.Map(Project).ToList();
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
} }
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Emby namespace ErsatzTV.Application.Emby;
{
public record EmbyConnectionParametersViewModel(string Address); public record EmbyConnectionParametersViewModel(string Address);
}
@@ -1,8 +1,7 @@
using ErsatzTV.Application.Libraries; using ErsatzTV.Application.Libraries;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby namespace ErsatzTV.Application.Emby;
{
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems) public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
: LibraryViewModel("Emby", Id, Name, MediaKind); : LibraryViewModel("Emby", Id, Name, MediaKind);
}
@@ -1,9 +1,8 @@
using ErsatzTV.Application.MediaSources; using ErsatzTV.Application.MediaSources;
namespace ErsatzTV.Application.Emby namespace ErsatzTV.Application.Emby;
{
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel( public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
Id, Id,
Name, Name,
Address); Address);
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Emby namespace ErsatzTV.Application.Emby;
{
public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath); public record EmbyPathReplacementViewModel(int Id, string EmbyPath, string LocalPath);
}
+13 -14
View File
@@ -1,19 +1,18 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby namespace ErsatzTV.Application.Emby;
internal static class Mapper
{ {
internal static class Mapper internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) =>
{ new(
internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) => embyMediaSource.Id,
new( embyMediaSource.ServerName,
embyMediaSource.Id, embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
embyMediaSource.ServerName,
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) => internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems); new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) => internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath); new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
} }
}
@@ -1,7 +1,3 @@
using System.Collections.Generic; namespace ErsatzTV.Application.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
{
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
}
@@ -1,24 +1,17 @@
using System.Collections.Generic; using ErsatzTV.Core.Interfaces.Repositories;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Emby.Mapper; using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>>
{ {
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>> private readonly IMediaSourceRepository _mediaSourceRepository;
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) => public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyMediaSourceViewModel>> Handle( public Task<List<EmbyMediaSourceViewModel>> Handle(
GetAllEmbyMediaSources request, GetAllEmbyMediaSources request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList()); _mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
} }
}
@@ -1,8 +1,5 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
{
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>; public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
}
@@ -1,73 +1,66 @@
using System; using ErsatzTV.Core;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
{ {
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters, private readonly IMediaSourceRepository _mediaSourceRepository;
Either<BaseError, EmbyConnectionParametersViewModel>> private readonly IMemoryCache _memoryCache;
public GetEmbyConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{ {
private readonly IMediaSourceRepository _mediaSourceRepository; _memoryCache = memoryCache;
private readonly IMemoryCache _memoryCache; _mediaSourceRepository = mediaSourceRepository;
public GetEmbyConnectionParametersHandler(
IMemoryCache memoryCache,
IMediaSourceRepository mediaSourceRepository)
{
_memoryCache = memoryCache;
_mediaSourceRepository = mediaSourceRepository;
}
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
GetEmbyConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
} }
}
public async Task<Either<BaseError, EmbyConnectionParametersViewModel>> Handle(
GetEmbyConnectionParameters request,
CancellationToken cancellationToken)
{
if (_memoryCache.TryGetValue(request, out EmbyConnectionParametersViewModel parameters))
{
return parameters;
}
Either<BaseError, EmbyConnectionParametersViewModel> maybeParameters =
await Validate()
.MapT(cp => new EmbyConnectionParametersViewModel(cp.ActiveConnection.Address))
.Map(v => v.ToEither<EmbyConnectionParametersViewModel>());
return maybeParameters.Match(
p =>
{
_memoryCache.Set(request, p, TimeSpan.FromHours(1));
return maybeParameters;
},
error => error);
}
private Task<Validation<BaseError, ConnectionParameters>> Validate() =>
EmbyMediaSourceMustExist()
.BindT(MediaSourceMustHaveActiveConnection);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.FirstOrDefault();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection);
}
@@ -1,7 +1,3 @@
using System.Collections.Generic; namespace ErsatzTV.Application.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
{
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
}
@@ -1,26 +1,19 @@
using System.Collections.Generic; using ErsatzTV.Core.Interfaces.Repositories;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Emby.Mapper; using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
{ {
public class private readonly IMediaSourceRepository _mediaSourceRepository;
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyLibraryViewModel>> Handle( public Task<List<EmbyLibraryViewModel>> Handle(
GetEmbyLibrariesBySourceId request, GetEmbyLibrariesBySourceId request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId) _mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).ToList());
} }
}
@@ -1,7 +1,3 @@
using LanguageExt; namespace ErsatzTV.Application.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
{
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
}
@@ -1,23 +1,18 @@
using System.Threading; using ErsatzTV.Core.Interfaces.Repositories;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Emby.Mapper; using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
{ {
public class private readonly IMediaSourceRepository _mediaSourceRepository;
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) => public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
public Task<Option<EmbyMediaSourceViewModel>> Handle( public Task<Option<EmbyMediaSourceViewModel>> Handle(
GetEmbyMediaSourceById request, GetEmbyMediaSourceById request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel); _mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
} }
}
@@ -1,8 +1,4 @@
using System.Collections.Generic; namespace ErsatzTV.Application.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries public record GetEmbyPathReplacementsBySourceId
{ (int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
public record GetEmbyPathReplacementsBySourceId
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
}
@@ -1,26 +1,19 @@
using System.Collections.Generic; using ErsatzTV.Core.Interfaces.Repositories;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Emby.Mapper; using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId,
List<EmbyPathReplacementViewModel>>
{ {
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId, private readonly IMediaSourceRepository _mediaSourceRepository;
List<EmbyPathReplacementViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) => public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository; _mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyPathReplacementViewModel>> Handle( public Task<List<EmbyPathReplacementViewModel>> Handle(
GetEmbyPathReplacementsBySourceId request, GetEmbyPathReplacementsBySourceId request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId) _mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList()); .Map(list => list.Map(ProjectToViewModel).ToList());
} }
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Emby; using ErsatzTV.Core.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
{
public record GetEmbySecrets : IRequest<EmbySecrets>; public record GetEmbySecrets : IRequest<EmbySecrets>;
}
@@ -1,19 +1,15 @@
using System.Threading; using ErsatzTV.Core.Emby;
using System.Threading.Tasks;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries namespace ErsatzTV.Application.Emby;
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets>
{ {
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets> private readonly IEmbySecretStore _embySecretStore;
{
private readonly IEmbySecretStore _embySecretStore;
public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) => public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) =>
_embySecretStore = embySecretStore; _embySecretStore = embySecretStore;
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) => public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
_embySecretStore.ReadSecrets(); _embySecretStore.ReadSecrets();
} }
}
+3 -4
View File
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application namespace ErsatzTV.Application;
{
public record EntityIdResult(int Id); public record EntityIdResult(int Id);
}
@@ -1,19 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn> <NoWarn>VSTHRD200</NoWarn>
<DebugType>embedded</DebugType> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1"> <PackageReference Include="Bugsnag" Version="3.0.0" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="CliWrap" Version="3.4.2" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="MediatR" Version="10.0.1" />
</PackageReference> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="MediatR" Version="9.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.1.46">
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.10.56">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
@@ -0,0 +1,43 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=emby_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=emby_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=ffmpegprofiles_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=filler_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=hdhr_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=hdhr_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=health_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=images_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=images_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=jellyfin_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=jellyfin_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=libraries_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=libraries_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=logs_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=maintenance_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacards_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacollections_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediacollections_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediaitems_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=mediasources_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=movies_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playouts_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playouts_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plex_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<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/=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>
@@ -1,9 +1,6 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles;
{
public record CopyFFmpegProfile public record CopyFFmpegProfile
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>; (int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
}
@@ -1,37 +1,32 @@
using System.Threading; using ErsatzTV.Core;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper; using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles;
public class
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{ {
public class private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) => public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository; _ffmpegProfileRepository = ffmpegProfileRepository;
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle( public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CopyFFmpegProfile request, CopyFFmpegProfile request,
CancellationToken cancellationToken) => CancellationToken cancellationToken) =>
Validate(request) Validate(request)
.MapT(PerformCopy) .MapT(PerformCopy)
.Bind(v => v.ToEitherAsync()); .Bind(v => v.ToEitherAsync());
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) => private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name) _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel); .Map(ProjectToViewModel);
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) => private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request); ValidateName(request).AsTask().MapT(_ => request);
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) => private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name) request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name)); .Bind(_ => request.NotLongerThan(50)(x => x.Name));
} }
}
@@ -1,25 +1,24 @@
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using LanguageExt; using ErsatzTV.Core.FFmpeg;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands namespace ErsatzTV.Application.FFmpegProfiles;
{
public record CreateFFmpegProfile( public record CreateFFmpegProfile(
string Name, string Name,
int ThreadCount, int ThreadCount,
bool Transcode, HardwareAccelerationKind HardwareAcceleration,
HardwareAccelerationKind HardwareAcceleration, VaapiDriver VaapiDriver,
int ResolutionId, string VaapiDevice,
bool NormalizeVideo, int ResolutionId,
string VideoCodec, FFmpegProfileVideoFormat VideoFormat,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
string AudioCodec, FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate, int AudioBitrate,
int AudioBufferSize, int AudioBufferSize,
bool NormalizeLoudness, bool NormalizeLoudness,
int AudioChannels, int AudioChannels,
int AudioSampleRate, int AudioSampleRate,
bool NormalizeAudio) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>; bool NormalizeFramerate,
} bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;

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