Compare commits

...

472 Commits

Author SHA1 Message Date
Jason Dove
ab1f294c1f update changelog for release v0.4.1-alpha 2022-02-10 20:49:26 -06:00
Jason Dove
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 Dove
87deaa6f3a nvidia improvements (#628) 2022-02-10 15:39:45 -06:00
Jason Dove
9d99c19ea4 fix playback with unknown pixel format (#627) 2022-02-10 08:37:25 -06:00
Jason Dove
49d14b05f6 update more dependencies (#626) 2022-02-09 11:08:49 -06:00
Jason Dove
a8ba9edf2b update dependencies (#624)
* update dependencies

* include refit xml serializer
2022-02-09 10:10:12 -06:00
Jason Dove
89811a1203 wait for one segment by default (#617) 2022-02-07 12:44:12 -06:00
Jason Dove
534e2c4512 add hls segmenter initial segment count (#616) 2022-02-07 12:18:58 -06:00
dependabot[bot]
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 Dove
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]
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 Dove
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 Dove
0c53a4509c show collection name in some error messages (#612) 2022-02-04 20:17:17 -06:00
Jason Dove
5fd315ead8 change framerate normalization method (#611) 2022-02-04 14:39:27 -06:00
Jason Dove
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 Dove
70ca5bf050 fix bug with watermark and short content (#608) 2022-01-31 14:07:12 -06:00
Jason Dove
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 Dove
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 Dove
90d6a59d3f normalize smart quotes in search queries (#605) 2022-01-30 11:09:32 -06:00
Jason Dove
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 Dove
37c52c4cb4 update docs and dependencies (#603) 2022-01-29 18:08:05 -06:00
Jason Dove
33ba58aa68 add windows launcher (#602) 2022-01-29 17:58:11 -06:00
Jason Dove
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 Dove
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 Dove
8d4e18ed2f update mac app icon (#599) 2022-01-28 19:46:34 -06:00
Jason Dove
1ee01c1d78 fix hls timestamps (#598) 2022-01-27 23:51:18 -06:00
Jason Dove
7de50dd916 minor hls segmenter improvements (#593) 2022-01-26 20:12:29 -06:00
Jason Dove
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 Dove
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 Dove
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 Dove
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 Dove
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 Dove
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 Dove
685f78bef8 fix search results bug (#583) 2022-01-21 14:05:09 -06:00
Jason Dove
3ce267863b fix hls segmenter in some cultures (#582) 2022-01-21 10:42:33 -06:00
Jason Dove
e4231cb57d upgrade from ffmpeg 4.4 to 5.0 (#581) 2022-01-20 20:57:38 -06:00
Jason Dove
03946b13ca always use a single ffmpeg thread with realtime (#580) 2022-01-20 14:53:13 -06:00
Jason Dove
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 Dove
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 Dove
0440f7643b add videotoolbox acceleration (#575) 2022-01-17 15:05:23 -06:00
Jason Dove
0f4219f731 properly unlock libraries after failed scans (#574) 2022-01-14 13:03:15 -06:00
Jason Dove
cbe5d47611 fix trakt list sync when show does not contain a year (#572) 2022-01-12 21:09:26 -06:00
Jason Dove
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 Dove
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 Dove
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 Dove
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 Dove
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 Dove
8036e46966 update streaming mode docs (#558) [no docker] 2022-01-06 09:10:12 -06:00
Jason Dove
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 Dove
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 Dove
7346808059 update dependencies (#555) 2022-01-04 22:35:14 -06:00
Jason Dove
4210d97ee2 optimize setsar filter (#553) 2022-01-02 23:47:07 -06:00
Jason Dove
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 Dove
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 Dove
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 Dove
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 Dove
85a9afb51c update dependencies (#538) 2021-12-12 23:51:57 -06:00
Jason Dove
246b4d7591 properly sort channels in m3u (#537) 2021-12-10 20:22:52 -06:00
Jason Dove
ae2c6350e1 sync virtual shows and season from jellyfin (#536) 2021-12-10 14:41:47 -06:00
Jason Dove
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 Dove
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 Dove
abc103308b optimize song artwork scanning (#527) 2021-12-03 13:40:55 -06:00
Jason Dove
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 Dove
e223d6a43f remove unused cli project (#525) [no ci] 2021-12-02 09:01:47 -06:00
Jason Dove
8369111e31 update dependencies (#524) 2021-12-02 08:45:43 -06:00
Jason Dove
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 Dove
094ed71ad0 fix docker builds with custom (local) nuget package (#520) 2021-11-30 20:49:02 -06:00
Jason Dove
89e24b2b78 use custom log database backend that is more portable (#519) 2021-11-30 20:34:14 -06:00
Jason Dove
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 Dove
56f94f489a fix filler playout crash (#517) 2021-11-30 10:38:42 -06:00
Jason Dove
475dc7660b fix artwork uploads (#516) 2021-11-29 14:34:18 -06:00
Jason Dove
db3dfbd446 disambiguate song search results (#515) 2021-11-27 21:37:55 -06:00
Jason Dove
b4c9cdbbfa use embedded song cover art (#514) 2021-11-27 21:08:18 -06:00
Jason Dove
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 Dove
1e35e9a5b0 use subtitles to display errors (#512)
* use subtitles to display errors

* fix margin calculation
2021-11-27 12:25:30 -06:00
Jason Dove
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 Dove
919325033d use subtitles instead of drawtext for songs (#510) 2021-11-26 21:39:10 -06:00
Jason Dove
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 Dove
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 Dove
af51b790b6 randomize cover art placement (#507) 2021-11-26 09:21:00 -06:00
Jason Dove
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 Dove
a6b15f68c9 randomize default backgrounds (#504)
* randomize default song backgrounds

* update docs
2021-11-25 20:19:26 -06:00
Jason Dove
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 Dove
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 Dove
fd3b72525d fix vaapi songs (#500) 2021-11-25 13:35:57 -06:00
Jason Dove
55d1871d94 re-enable hardware acceleration for songs (#499) 2021-11-25 13:04:13 -06:00
Jason Dove
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 Dove
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 Dove
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 Dove
fb8c3a0453 disable autoscale when looping with vaapi or qsv (#494) 2021-11-23 13:25:23 -06:00
Jason Dove
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 Dove
3a40d6ce77 fix local library locking when adding paths (#492) 2021-11-23 10:54:34 -06:00
Jason Dove
ac048b72ae add cover art watermark source (#491)
* add cover art watermark source

* update changelog
2021-11-23 10:02:36 -06:00
Jason Dove
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 Dove
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 Dove
a4dc9bfb31 Ignore local plex guids (#488)
* ignore local plex guids

* update dependencies
2021-11-21 06:25:56 -06:00
Jason Dove
184c21a91b optimize trakt matching (#487) 2021-11-21 06:13:28 -06:00
Jason Dove
6ea3191cf8 fix playout building (#486) 2021-11-20 22:36:15 -06:00
Jason Dove
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 Dove
b0c85b6478 use scale_cuda instead of scale_npp (#481) 2021-11-13 09:06:02 -06:00
Jason Dove
f1356563da fix ef shared table warnings (#480) 2021-11-10 18:03:28 -06:00
Jason Dove
c0aad028a8 more dotnet 6 updates (#479) 2021-11-09 13:09:57 -06:00
Jason Dove
dae06ec0ef upgrade to dotnet 6 (#475) 2021-11-09 07:44:34 -06:00
Jason Dove
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 Dove
08a18daf23 movie scanner should respect .etvignore files (#468) 2021-11-03 05:47:29 -05:00
Jason Dove
90c1c61a09 fix bug with flood playout mode (#467) 2021-11-02 21:47:46 -05:00
Jason Dove
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 Dove
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 Dove
53a7570ba3 fix epg for multiple playout mode (#461) 2021-10-30 12:16:39 -05:00
Jason Dove
0e789fd6d8 update dependencies and fix languageext deprecation warnings (#460) 2021-10-30 11:57:50 -05:00
Jason Dove
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 Dove
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 Dove
417f35a834 fix saving dynamic start time (#453) 2021-10-24 18:18:22 -05:00
Jason Dove
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 Dove
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 Dove
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 Dove
bc9d17ca25 show path replacement logs by default (#445) 2021-10-19 06:32:00 -05:00
Jason Dove
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 Dove
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 Dove
33aaadae68 multiple fixes to duration mode (#442) 2021-10-18 16:21:32 -05:00
Jason Dove
fe3f8e391e fix updating jellyfin and emby artwork (#440) 2021-10-17 05:36:00 -05:00
Jason Dove
1a68dd040a find working plex connection on startup (#438) 2021-10-16 11:55:54 -05:00
Jason Dove
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 Dove
1802f9d797 fix database migration (#436) 2021-10-15 11:31:40 -05:00
Jason Dove
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 Dove
71658c448f update docs (#431) 2021-10-14 14:41:12 -05:00
Jason Dove
3ecdd741a5 add guide mode to schedule items (#430) 2021-10-14 13:24:54 -05:00
Jason Dove
0daeb844b9 add other videos library kind (#429) 2021-10-14 12:58:37 -05:00
Jason Dove
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 Dove
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 Dove
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 Dove
c43ca2837d support radeon vaapi acceleration (#420) 2021-10-12 17:51:55 -05:00
Jason Dove
992121f308 add more watermark locations (#419) 2021-10-12 07:19:52 -05:00
Jason Dove
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 Dove
1fc905c6ad upgrade vaapi to ffmpeg 4.4 (#417) 2021-10-11 22:26:32 -05:00
Jason Dove
4b5dff2159 ffnvcodec fixes (#416) 2021-10-11 22:14:43 -05:00
Jason Dove
2a5edf8214 ffmpeg 4.4 llvm nvidia fixes (#415) 2021-10-11 21:31:59 -05:00
Jason Dove
69912c8cae support ffmpeg 4.4 (#414)
* support ffmpeg 4.4

* update changelog
2021-10-11 20:16:15 -05:00
Jason Dove
fd3de2d82a nvidia 10 bit fixes (#413) 2021-10-11 16:00:35 -05:00
Jason Dove
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 Dove
9e531a82d7 add some hls playlist filter tests (#411) 2021-10-10 12:33:02 -05:00
Jason Dove
d84bd2b948 upgrade nvidia docker image (#410) 2021-10-10 11:45:02 -05:00
Jason Dove
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 Dove
819b55e21f increase max hls segments (#408) 2021-10-10 06:47:24 -05:00
Jason Dove
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 Dove
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 Dove
d3086264c7 unraid doc fixes (#405) 2021-10-08 18:31:22 -05:00
Jason Dove
8cd9b23787 fix transcode folder preparation (#404) 2021-10-08 15:29:19 -05:00
dependabot[bot]
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 Dove
2dd267e4db fix xmltv generation with missing episode metadata (#402) 2021-10-07 22:31:46 -05:00
Jason Dove
b069a21473 allow hls segmenter to run before framerate is known (#401) 2021-10-07 22:07:54 -05:00
Jason Dove
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 Dove
b5de5e2b7f fix statistic updates (#399) 2021-10-07 19:56:41 -05:00
Jason Dove
4b7da4e468 speed up builds by using base images (#398) 2021-10-06 06:06:13 -05:00
Jason Dove
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 Dove
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 Dove
fc3175591e use libx264 for all errors (#395) 2021-10-05 15:13:35 -05:00
Jason Dove
3363d2c9d7 update plex paths when they are changed (#394) 2021-10-05 06:27:05 -05:00
Jason Dove
1d5217fa84 support imdb ids from plex (#393) 2021-10-05 06:01:53 -05:00
Jason Dove
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 Dove
13cfb9728f include season zero episode-num in xmltv (#391) 2021-10-01 13:42:46 -05:00
Jason Dove
60b82876ea mudblazor updates (#390) 2021-09-30 17:40:26 -05:00
Jason Dove
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 Dove
21e53532c1 trakt season bug fixes (#386) 2021-09-25 14:55:42 -05:00
Jason Dove
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 Dove
e6446f9983 better trakt lists (#384)
* better trakt list support

* update dependencies

* revert unneeded brackets
2021-09-25 09:12:25 -05:00
Jason Dove
ad40213f90 fix synchronizing trakt lists that contain unreleased movies (#382) 2021-09-21 21:14:27 -05:00
Jason Dove
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 Dove
5439db89a7 nvidia fixes (#380)
* nvidia fixes

* fix tests
2021-09-19 21:39:36 -05:00
Jason Dove
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 Dove
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 Dove
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 Dove
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 Dove
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 Dove
1eb51ad2f4 add some health checks to home page (#374) 2021-09-17 17:59:59 -05:00
Jason Dove
c3e0aaf0b7 missing metadata fixes (#373) 2021-09-17 08:53:41 -05:00
Jason Dove
b9912b47df update changelog for release 58 [no ci] 2021-09-15 21:53:34 -05:00
Jason Dove
55fb2624e7 add multi-part grouping tooltip (#371) 2021-09-15 21:18:41 -05:00
Jason Dove
8ced20dc39 dont offset collections during shuffle in order (#370) 2021-09-15 20:54:32 -05:00
Jason Dove
e718cb0faf fix building playouts in timezones with positive offsets (#368) 2021-09-15 09:07:35 -05:00
Jason Dove
e218ff9a6d fix watermark when no video filters are required (#367) 2021-09-15 05:12:23 -05:00
Jason Dove
c2a49cbaea update dependencies (#365) 2021-09-14 18:10:59 -05:00
Jason Dove
17e74f7314 add more release date search options (#362) 2021-09-12 18:32:38 -05:00
Long-Man
2032bb4777 Update search.md (#361)
Add release_date and released_nointhelast to music video search
2021-09-12 12:18:07 -05:00
Jason Dove
7877ec641e update changelog for release 57 [no ci] 2021-09-11 10:37:23 -05:00
Jason Dove
767a9779bb more kodi artwork fixes (#360) 2021-09-11 10:08:33 -05:00
Jason Dove
bb9127e546 fix artwork in kodi (#359) 2021-09-11 09:45:09 -05:00
Jason Dove
c932577cb8 allow adding smart collections to multi collections (#358) 2021-09-11 09:29:20 -05:00
Jason Dove
ad2685fb2e add released_inthelast queries (#357) 2021-09-10 21:11:14 -05:00
Jason Dove
96bc2c28f2 update changelog for release 56 [no ci] 2021-09-10 13:59:14 -05:00
Jason Dove
a076b3eb30 add shuffle-in-order support to all collections (#356) 2021-09-10 13:33:04 -05:00
Jason Dove
fc360602ad add smart collections (#355)
* start to add smart collections

* add smart collection table; delete smart collection

* overwrite smart collections

* support scheduling smart collections

* update changelog
2021-09-10 11:58:24 -05:00
Jason Dove
d8b4d00a73 clarify changelog [no ci] 2021-09-09 08:22:46 -05:00
Jason Dove
0638ac8a5e more missing metadata fixes (#354)
* more missing metadata fixes

* update mudblazor
2021-09-09 06:38:46 -05:00
Jason Dove
f1f09bd4cb fix sorting episodes without metadata (#353) 2021-09-08 22:12:47 -05:00
Jason Dove
f6680f29e7 try to fix doc formatting [no docker] 2021-09-07 13:36:25 -05:00
Jason Dove
1c0413452b fix m3u xmltv mapping 2021-09-07 06:34:17 -05:00
Jason Dove
77308a9ac5 generate valid xmltv (#351) 2021-09-07 06:12:13 -05:00
Jason Dove
3ea8193bb3 update changelog for release 55 [no ci] 2021-09-03 08:56:25 -05:00
Jason Dove
8ad8680027 update dependencies; fix unnecessary table scrolling (#347) 2021-09-03 06:22:50 -05:00
Jason Dove
640044814c ignore dot-underscore files (#346) 2021-09-03 06:22:33 -05:00
Jason Dove
18b5313a53 update docs [no docker] 2021-08-22 20:17:39 -05:00
Jason Dove
8417c3f6cd update changelog for release 54 [no ci] 2021-08-21 13:19:13 -05:00
Jason Dove
32fdb414fa add "shuffle in order" playback order for multi-collections (#338)
* add "shuffle in order" option for multi-collections

* use balanced shuffle instead of random
2021-08-21 12:47:22 -05:00
Jason Dove
d3fc820aef update dependencies (#336)
* update dependencies

* fix fluent assertions
2021-08-21 06:23:43 -05:00
Jason Dove
9d07627781 fix ffprobe parsing in some cultures (#337) 2021-08-21 05:57:39 -05:00
Jason Dove
d3c8914758 update dependencies (#331) 2021-08-14 07:20:09 -05:00
Jason Dove
3d7ec59088 update changelog for release 53 [no ci] 2021-08-01 12:37:10 -05:00
Jason Dove
d78976f80a update changelog [no ci] 2021-08-01 12:22:32 -05:00
Jason Dove
0f5fee99c6 always proxy jellyfin and emby artwork (#323) 2021-08-01 12:19:50 -05:00
dependabot[bot]
d5bfd1a254 Bump MudBlazor from 5.0.15 to 5.1.0 (#321)
Bumps [MudBlazor](https://github.com/Garderoben/MudBlazor) from 5.0.15 to 5.1.0.
- [Release notes](https://github.com/Garderoben/MudBlazor/releases)
- [Changelog](https://github.com/Garderoben/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/Garderoben/MudBlazor/compare/v5.0.15...v5.1.0)

---
updated-dependencies:
- dependency-name: MudBlazor
  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-07-28 05:37:38 -05:00
Jason Dove
cd2558e3e6 fix jellyfin and emby links (#320) 2021-07-27 18:26:12 -05:00
Jason Dove
c39654ca40 update changelog for release 52 [no ci] 2021-07-22 11:26:05 -05:00
Jason Dove
f17151bd20 fix multi-collection bug (#318) 2021-07-22 11:12:14 -05:00
Jason Dove
6aeaf65a13 update docs [no docker] 2021-07-20 20:13:21 -05:00
Jason Dove
9fbe950e6e support multiple local libraries (#317)
* allow multiple local libraries

* add "move library path" function
2021-07-20 07:47:12 -05:00
Jason Dove
c9baff2cd5 add codeql workflow [no docker] 2021-07-18 07:36:41 -05:00
Jason Dove
447829385f update changelog for release 51 [no docker] 2021-07-18 07:11:11 -05:00
Jason Dove
a94d831866 add playout days to build setting (#316) 2021-07-18 06:10:54 -05:00
Jason Dove
632753ea93 add multi collections (#315)
* start to add multi-collections

* create multi collection with no items

* edit multi collections

* fix plex credentials threading issue

* add playback order to multi collection items

* group episodes outside of shuffled enumerator

* move playback order onto each schedule item

* fix multi collection grouping

* update changelog
2021-07-17 21:45:41 -05:00
Jason Dove
4000c6bc0a update changelog [no docker] 2021-07-15 04:40:39 -05:00
Jason Dove
1521469b2f add sqlite linux-arm artifacts (#314) 2021-07-15 04:39:04 -05:00
Jason Dove
d6272c54a0 fix nuget references 2021-07-14 09:09:26 -05:00
dependabot[bot]
d5039dc4fc Bump Serilog.Sinks.Console from 3.1.1 to 4.0.0 (#303)
Bumps [Serilog.Sinks.Console](https://github.com/serilog/serilog-sinks-console) from 3.1.1 to 4.0.0.
- [Release notes](https://github.com/serilog/serilog-sinks-console/releases)
- [Commits](https://github.com/serilog/serilog-sinks-console/compare/v3.1.1...v4.0.0)

---
updated-dependencies:
- dependency-name: Serilog.Sinks.Console
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-07-14 07:10:06 -05:00
Jason Dove
eba50523a9 fix release notes on homepage with alpha suffix (#312) 2021-07-14 05:35:11 -05:00
Jason Dove
1c1c1e7812 update changelog for release 50 [no docker] 2021-07-13 20:29:00 -05:00
Jason Dove
5f802c7484 include linux-arm release artifacts [no docker] (#306) 2021-07-13 20:21:37 -05:00
Jason Dove
7a06ac71e2 fix movie fallback metadata titles (#305) 2021-07-13 20:15:52 -05:00
Jason Dove
3b9b8796b9 fix movie fallback metadata (#302) 2021-07-12 07:44:48 -05:00
dependabot[bot]
a72d91507e Bump FluentValidation.AspNetCore from 10.2.3 to 10.3.0 (#301)
Bumps [FluentValidation.AspNetCore](https://github.com/JeremySkinner/fluentvalidation) from 10.2.3 to 10.3.0.
- [Release notes](https://github.com/JeremySkinner/fluentvalidation/releases)
- [Changelog](https://github.com/FluentValidation/FluentValidation/blob/main/Changelog.txt)
- [Commits](https://github.com/JeremySkinner/fluentvalidation/compare/10.2.3...10.3.0)

---
updated-dependencies:
- dependency-name: FluentValidation.AspNetCore
  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-07-12 06:05:54 -05:00
dependabot[bot]
45bfbfc179 Bump FluentValidation from 10.2.3 to 10.3.0 (#300)
Bumps [FluentValidation](https://github.com/JeremySkinner/fluentvalidation) from 10.2.3 to 10.3.0.
- [Release notes](https://github.com/JeremySkinner/fluentvalidation/releases)
- [Changelog](https://github.com/FluentValidation/FluentValidation/blob/main/Changelog.txt)
- [Commits](https://github.com/JeremySkinner/fluentvalidation/compare/10.2.3...10.3.0)

---
updated-dependencies:
- dependency-name: FluentValidation
  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-07-12 05:51:29 -05:00
Jason Dove
7d24701a82 update changelog for release 49 [no docker] 2021-07-11 18:00:50 -05:00
Jason Dove
286580d5aa more sorting fixes (#299) 2021-07-11 14:40:31 -05:00
Jason Dove
d9457c01e5 add special case to multiple playout mode (#298) 2021-07-11 14:25:42 -05:00
Jason Dove
22cf759a29 sorting fixes (#297)
* sorting fixes

* use natural sort for add to schedule dialog
2021-07-11 13:20:42 -05:00
Jason Dove
0733a3d8d7 fix loading playout anchors (#296) 2021-07-11 10:47:25 -05:00
Jason Dove
5f28707cce include audio language metadata in all streaming modes (#295)
* include audio language metadata in all streaming modes

* cleanup
2021-07-10 06:16:46 -05:00
Jason Dove
45f1c6b22a properly flood with fixed start time (#294) 2021-07-09 14:16:24 -05:00
dependabot[bot]
3bed81aee9 Bump MudBlazor from 5.0.14 to 5.0.15 (#292)
Bumps [MudBlazor](https://github.com/Garderoben/MudBlazor) from 5.0.14 to 5.0.15.
- [Release notes](https://github.com/Garderoben/MudBlazor/releases)
- [Changelog](https://github.com/Garderoben/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/Garderoben/MudBlazor/compare/v5.0.14...v5.0.15)

---
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>
2021-07-09 13:49:52 -05:00
Jason Dove
f2eda3033c update dependencies (#289) 2021-06-24 06:35:50 -05:00
dependabot[bot]
8ce989c3c9 Bump Microsoft.VisualStudio.Threading.Analyzers from 16.9.60 to 16.10.56 (#282)
Bumps [Microsoft.VisualStudio.Threading.Analyzers](https://github.com/microsoft/vs-threading) from 16.9.60 to 16.10.56.
- [Release notes](https://github.com/microsoft/vs-threading/releases)
- [Commits](https://github.com/microsoft/vs-threading/compare/v16.9.60...v16.10.56)

---
updated-dependencies:
- dependency-name: Microsoft.VisualStudio.Threading.Analyzers
  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-06-23 13:06:41 -05:00
dependabot[bot]
b5ba0dff27 Bump Blazored.LocalStorage from 4.1.1 to 4.1.2 (#286)
Bumps [Blazored.LocalStorage](https://github.com/Blazored/LocalStorage) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/Blazored/LocalStorage/releases)
- [Commits](https://github.com/Blazored/LocalStorage/compare/v4.1.1...v4.1.2)

---
updated-dependencies:
- dependency-name: Blazored.LocalStorage
  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>
2021-06-23 13:06:30 -05:00
dependabot[bot]
de3e2ea754 Bump NUnit3TestAdapter from 3.17.0 to 4.0.0 (#283)
Bumps [NUnit3TestAdapter](https://github.com/nunit/nunit3-vs-adapter) from 3.17.0 to 4.0.0.
- [Release notes](https://github.com/nunit/nunit3-vs-adapter/releases)
- [Commits](https://github.com/nunit/nunit3-vs-adapter/compare/V3.17...V4.0.0)

---
updated-dependencies:
- dependency-name: NUnit3TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-23 05:44:05 -05:00
dependabot[bot]
2ac840b4bd Bump Markdig from 0.24.0 to 0.25.0 (#284)
Bumps [Markdig](https://github.com/xoofx/markdig) from 0.24.0 to 0.25.0.
- [Release notes](https://github.com/xoofx/markdig/releases)
- [Changelog](https://github.com/xoofx/markdig/blob/master/changelog.md)
- [Commits](https://github.com/xoofx/markdig/compare/0.24.0...0.25.0)

---
updated-dependencies:
- dependency-name: Markdig
  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-06-23 05:43:53 -05:00
dependabot[bot]
c8ccb5b0a0 Bump Microsoft.NET.Test.Sdk from 16.9.4 to 16.10.0 (#285)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.9.4 to 16.10.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Commits](https://github.com/microsoft/vstest/compare/v16.9.4...v16.10.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  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-06-23 05:43:42 -05:00
Jason Dove
23c4fcf42c update changelog for release 48 [no docker] 2021-06-22 20:05:43 -05:00
Jason Dove
e2f3e86fd6 fix adding jellyfin emby seasons episodes (#281)
* fix adding new seasons and episodes with emby and jellyfin

* update changelog

* update dependencies
2021-06-22 18:55:44 -05:00
dependabot[bot]
fd9f4a8f4e Bump MudBlazor from 5.0.10 to 5.0.14 (#278)
Bumps [MudBlazor](https://github.com/Garderoben/MudBlazor) from 5.0.10 to 5.0.14.
- [Release notes](https://github.com/Garderoben/MudBlazor/releases)
- [Changelog](https://github.com/Garderoben/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/Garderoben/MudBlazor/compare/v5.0.10...v5.0.14)

---
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>
2021-06-22 06:28:01 -05:00
dependabot[bot]
d5a0951a9b Bump FluentValidation from 10.1.0 to 10.2.3 (#275)
Bumps [FluentValidation](https://github.com/JeremySkinner/fluentvalidation) from 10.1.0 to 10.2.3.
- [Release notes](https://github.com/JeremySkinner/fluentvalidation/releases)
- [Changelog](https://github.com/FluentValidation/FluentValidation/blob/main/Changelog.txt)
- [Commits](https://github.com/JeremySkinner/fluentvalidation/compare/10.1.0...10.2.3)

---
updated-dependencies:
- dependency-name: FluentValidation
  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-06-22 06:21:28 -05:00
Jason Dove
56d9724efd remove BOM from dependabot.yml [no docker] 2021-06-22 06:08:04 -05:00
Jason Dove
f91b5ab3b5 assign dependabot prs [no docker] 2021-06-22 06:06:20 -05:00
Jason Dove
4b8e81ff06 add dependabot config [no docker] 2021-06-22 06:00:54 -05:00
Jason Dove
1a7e6dda54 support 10-bit content with nvidia acceleration (#273)
* use ffprobe for plex statistics

* emby and jellyfin respect library refresh interval

* support 10-bit content with nvidia acceleration
2021-06-19 21:25:44 -05:00
Jason Dove
9fc6cdd0b7 update changelog for release 47 [no docker] 2021-06-15 16:25:01 -05:00
Jason Dove
cebab33d79 table improvements (#272)
* log viewer improvements

* playout detail table improvements

* schedule items table improvements

* remove schedule items pager
2021-06-15 16:10:24 -05:00
Jason Dove
b580125e86 fix searching when queries include non-ascii characters (#271) 2021-06-15 10:33:02 -05:00
Jason Dove
b38ba14c40 fix languages that have multiple codes (#270) 2021-06-15 06:12:40 -05:00
Jason Dove
c10bc6b184 fix blazor error font color (#269)
* fix blazor error font color

* changelog
2021-06-14 20:32:20 -05:00
Jason Dove
a75737a032 clear playout detail on delete (#266) 2021-06-14 18:44:10 -05:00
Jason Dove
57aa14b764 fix adding channel with no watermark (#265) 2021-06-14 18:31:50 -05:00
Jason Dove
6e6d5a133f update changelog for release 46 [no docker] 2021-06-14 14:03:10 -05:00
Jason Dove
b4ba37f778 reset watermarks (#263) 2021-06-14 13:42:13 -05:00
Jason Dove
275f82fcc9 save schedules and playouts table page sizes (#262) 2021-06-14 13:11:49 -05:00
Jason Dove
72d967946d rework watermarks (#261)
* rework watermarks to be separate from channels

* update changelog
2021-06-14 10:29:58 -05:00
Jason Dove
a0740de972 add global and channel watermark overrides (#260)
* add global watermark setting

* add channel watermark override

* update changelog
2021-06-13 21:45:52 -05:00
Jason Dove
e69569ea46 fix docker builds 2021-06-13 20:55:40 -05:00
Jason Dove
679feb6d21 add watermark opacity (#259) 2021-06-13 20:45:23 -05:00
Jason Dove
0fb5bfde58 refactor dbcontext lifetime (#258)
* refactor create playout handler

* refactor get all playouts handler

* refactor delete playout handler

* remove dead code

* ignore unnamed artists for collections

* more repository cleanup

* more schedule items refactoring

* more playout refactoring

* refactor playout builder

* refactor ffmpeg profiles

* more ffmpeg profile refactoring

* rework resolutions

* refactor media collections

* refactor config elements

* update changelog

* more cleanup
2021-06-13 20:19:10 -05:00
Jason Dove
4172074ac4 update changelog for release 45 [no docker] 2021-06-12 11:46:07 -05:00
Jason Dove
e9889cefd6 skip empty content rating (#257) 2021-06-12 11:31:32 -05:00
Jason Dove
fc59c9c284 include all content ratings in xmltv (#256) 2021-06-12 11:14:27 -05:00
Jason Dove
0750a0712f allow animated channel watermarks (#255) 2021-06-12 06:16:52 -05:00
Jason Dove
0365d4c8f8 add channel watermark (#254)
* wip

* wip

* implement watermark settings

* code cleanup

* update changelog
2021-06-11 21:42:06 -05:00
Jason Dove
5b36252dd0 remove framerate normalization (#253) 2021-06-11 18:04:16 -05:00
Jason Dove
7d852bc960 add hls hybrid mode (#252)
* fix serving channels.m3u with missing content ratings

* add hls hybrid mode
2021-06-10 20:42:58 -05:00
Jason Dove
cdf10b0535 changelog for 44 again [no docker] 2021-06-09 18:41:11 -05:00
Jason Dove
f0b429efb5 update changelog for release 44 [no docker] 2021-06-09 18:39:52 -05:00
Jason Dove
da5148affd quickly skip missing files during plex library scan (#251) 2021-06-07 20:34:24 -05:00
Jason Dove
cec5a09839 add us content ratings to xmltv (#250) 2021-06-07 18:58:38 -05:00
Jason Dove
e20f9be702 exclude strm files from jellyfin scanners (#249)
* exclude strm files from jellyfin scanners

* update changelog
2021-06-07 07:41:59 -05:00
Jason Dove
3bc3faa7c4 artist schedule doc update [no docker] 2021-06-06 20:25:39 -05:00
Jason Dove
db24ba84f7 add artists directly to schedules (#248) 2021-06-06 20:12:17 -05:00
Jason Dove
8346a02747 ignore unsupported plex guids (#246) 2021-06-05 15:53:38 -05:00
Jason Dove
c3b33c184f fix changelog [no docker] 2021-06-05 13:39:29 -05:00
Jason Dove
6bec9c5f07 update docs for 0.0.43-prealpha [no docker] 2021-06-05 13:36:08 -05:00
Jason Dove
0ef03d66f3 improve hls direct compatibility with channels dvr (#245)
* rename HttpLiveStreaming to HttpLiveStreamingDirect

* improve hls direct compatibility with channels dvr

* code cleanup
2021-06-05 13:15:39 -05:00
Jason Dove
10c422a3eb save channels table page size (#244)
* save channel table page size

* update changelog
2021-06-05 10:15:34 -05:00
Jason Dove
6c867d0d51 support multi-episode files from plex (#243)
* minor fallback metadata bug fixes

* support multi-episode files from plex
2021-06-04 15:06:19 -05:00
Jason Dove
ed0796ad58 force scan local season folders to pick up multi-episode files (#242) 2021-06-04 11:42:03 -05:00
Jason Dove
49109ac121 fix missing season metadata (#241) 2021-06-04 10:37:56 -05:00
Jason Dove
3e3bbcf38e support multi-episode files in local libraries (#240)
* add unused episode nfo reader

* move episode number from episode to episode metadata

* first pass at loading multi-episode metadata from nfo files

* fix episode scanning

* local multi-part episode fixes

* code cleanup
2021-06-04 06:00:35 -05:00
Jason Dove
ce9ef72799 support (part #) names for multi-episode grouping (#238) 2021-06-01 07:15:17 -05:00
Jason Dove
f8631a1f12 release 0.0.42-prealpha 2021-05-31 13:08:55 -05:00
Jason Dove
c70f153241 keep crossover episodes together (#237) 2021-05-31 11:58:56 -05:00
Jason Dove
eee10dee22 skip zero duration items when building playouts (#236) 2021-05-31 07:31:15 -05:00
Jason Dove
9f575dbd94 fix stuck playout builds (#235)
* fix stuck playout builds

* code cleanup
2021-05-31 06:38:54 -05:00
Jason Dove
539285d81e update docs for 0.0.41-prealpha release [no docker] 2021-05-30 18:01:13 -05:00
Jason Dove
f8c986472a more grouping fixes (#234)
* more grouping fixes

* update changelog
2021-05-30 11:54:02 -05:00
Jason Dove
442d73150e link to full changelog on home page [no docker] 2021-05-30 10:47:33 -05:00
Jason Dove
d6cee14143 add changelog (#232) 2021-05-30 09:40:59 -05:00
Jason Dove
c20c0b231e fix updating local movies (#231) 2021-05-30 06:32:26 -05:00
Jason Dove
e506dd38a8 merge latest develop (#230)
* sync guids/provider ids (#227)

* sync guids from plex

* cleanup

* sync local guids

* sync jellyfin and emby guids

* add episodes to search index (#228)

* sync episode directors and writers

* display episode writers and directors

* remove missing episodes from search index

* show episodes in search results

* fix emby and jellyfin episode updates

* fix updating plex episodes

* don't delete channel logos on startup

* add episodes page; fix adding episodes to collection

* cleanup

* multi-part episode grouping fixes (#229)
2021-05-30 06:08:33 -05:00
Jason Dove
bbd8bc6c7e add button to refresh list of libraries (#226)
* add button to refresh list of libraries

* code cleanup
2021-05-28 15:28:45 -05:00
Jason Dove
e841c9c53b fix missing artwork (#225) 2021-05-28 09:34:47 -05:00
Jason Dove
4c78f41c5a fix incorrect search items count (#224) 2021-05-28 06:13:40 -05:00
Jason Dove
95cceb95b9 regularly delete orphaned artwork from db (#223) 2021-05-28 05:38:58 -05:00
Jason Dove
58d6f81d2e recursively retrieve jellyfin and emby items (#221) 2021-05-27 21:01:25 -05:00
Jason Dove
fe5cedfcdc disable ffmpeg reports on windows (#220)
* disable ffmpeg reports for windows

* code cleanup
2021-05-27 16:36:02 -05:00
Jason Dove
0bbed69e85 add movie directors and writers (#219) 2021-05-27 09:24:47 -05:00
Jason Dove
68123a2f9c add content rating (#218)
* add new columns

* store local content ratings

* display and search content ratings

* add content_rating to search docs

* sync content rating from jellyfin, emby, plex

* force sync content rating for all libraries

* code cleanup
2021-05-27 06:41:24 -05:00
Jason Dove
6504ca10a8 cache local artwork on disk (#217) 2021-05-26 19:49:08 -05:00
Jason Dove
84770ed250 use artwork for schedule items with custom title when all media items are from same show (#216) 2021-05-26 15:34:54 -05:00
Jason Dove
466d33f808 sync tv show thumb art (#214)
* sync thumb art from local, jellyfin, emby

* code cleanup
2021-05-26 12:46:23 -05:00
Jason Dove
8e81d5f197 fix add to schedule dialog (#213) 2021-05-26 08:43:11 -05:00
Jason Dove
da43e6f7cf embed debug symbols (#212) 2021-05-26 07:26:14 -05:00
Jason Dove
c9905d0542 fix resources (offline background and font) (#211) 2021-05-25 15:42:03 -05:00
Jason Dove
c9e20e28df proxy jellyfin and emby artwork for xmltv (#210)
* fix xmltv artwork for jf and emby

* proxy jellyfin and emby artwork for xmltv
2021-05-25 15:15:15 -05:00
Jason Dove
f9427cac99 use multiple docker tags again (#209)
* Revert "disable framerate normalization (#208)"

This reverts commit 141a34933d.

* Revert "use linuxserver base docker image (#207)"

This reverts commit 0962a1429a.

* fix playback that only uses fps filter

* nvidia needs privileged
2021-05-25 05:13:51 -05:00
Jason Dove
141a34933d disable framerate normalization (#208)
* disable framerate normalization

* fix test
2021-05-24 21:47:26 -05:00
Jason Dove
0962a1429a use linuxserver base docker image (#207)
* use one base docker image

* remove nvidia and vaapi tags

* fix playback that only uses fps filter
2021-05-24 21:12:55 -05:00
Jason Dove
f8b45ed9db fix unc path replacements from jellyfin and emby (#205)
* fix UNC path replacements from non-windows JF and Emby servers

* use emby path replacements for playback
2021-05-24 09:03:39 -05:00
Jason Dove
266bfbad23 add link to release notes (#203) 2021-05-23 10:02:26 -05:00
Jason Dove
60a9640009 use ffmpeg 4.3 in docker (#202)
* Revert "fix ffmpeg 4.4 compatibility"

This reverts commit 1ca0df038c.

* use ffmpeg 4.3 in docker
2021-05-23 09:55:40 -05:00
Jason Dove
9291a6b6ed add emby docs 2021-05-23 03:50:00 -05:00
Jason Dove
9afec19888 cleanup 2021-05-23 03:27:20 -05:00
Jason Dove
50529ee6ad add emby media source (#201)
* properly scope jellyfin disconnect

* add emby entities

* add emby media source page

* add emby media source editor

* sync emby libraries

* enable emby library sync toggle

* add emby path replacements editor

* add emby movie synchronization

* fix emby artwork

* sync emby television

* code cleanup

* add jellyfin/emby address placeholder

* tweak jellyfin/emby address form
2021-05-23 03:05:23 -05:00
Jason Dove
0b105bf6e1 fix schedule item duration under one hour (#200) 2021-05-22 07:45:44 -05:00
Jason Dove
5356f7f293 update dependencies 2021-05-22 05:59:30 -05:00
Jason Dove
1d35efa429 fix jellyfin artwork (#198) 2021-05-21 21:21:32 -05:00
Jason Dove
04da4b2964 single-file app publishing (#197)
* attempt to fix single file app publishing

* update release workflow
2021-05-21 15:17:24 -05:00
Jason Dove
0799fe25d1 optimize local library scanning by using etags (#196)
* use etags to optimize local movie scanner

* use etags to optimize local television scanner

* use etags to optimize local music video scanner

* code cleanup
2021-05-21 06:18:07 -05:00
Jason Dove
c0b5ecd388 custom binding and port number (#195)
* allow custom bindings

* reorganize

* cleanup
2021-05-20 20:09:14 -05:00
Jason Dove
5fd0cc5469 only initialize search index on startup (#193) 2021-05-19 21:09:01 -05:00
Jason Dove
34ebe9b006 handle "other" jellyfin libraries (#192) 2021-05-19 20:15:16 -05:00
Jason Dove
d7c080cafd optimize plex tv scanner (#190) 2021-05-19 07:22:42 -05:00
Jason Dove
23bab01f2d add multi-part episode tests (#189) 2021-05-18 11:33:34 -05:00
Jason Dove
c7fdacf30f another multi-episode bugfix 2021-05-18 11:00:21 -05:00
Jason Dove
6e6d53d847 multi-episode grouping bugfix 2021-05-18 09:56:04 -05:00
Jason Dove
47e9a319ce add option to keep multi-part episodes together when shuffling (#188)
* add setting to keep multi-part episodes together

* keep multi-part episodes together when shuffling
2021-05-18 08:23:08 -05:00
Jason Dove
9112cb3c1f only scale to even dimensions (#187) 2021-05-16 15:47:44 -05:00
Jason Dove
3ec838da68 handle unauthorized jellyfin server (#186) 2021-05-15 15:47:43 -05:00
Jason Dove
fc5bedc70b update docs for jellyfin [no docker] 2021-05-15 15:39:33 -05:00
Jason Dove
4d86250630 add jellyfin media source (#185)
* wip

* start to add jellyfin tables to db

* code cleanup

* finish adding jellyfin media source

* sync jellyfin libraries

* display list of jellyfin libraries

* toggle jellyfin library sync

* edit jellyfin path replacements

* noop jellyfin scanners

* get jellyfin admin user id on startup

* implement jellyfin disconnect

* add jellyfin libraries to list; start to query jellyfin library items

* code cleanup

* start to project jellyfin movies

* save new jellyfin movies to db

* basic jellyfin movie update

* load jellyfin actor artwork

* load jellyfin movie poster and fan art

* more jellyfin artwork fixes, sync audio streams

* jellyfin playback sort of works

* skip jellyfin movies that are inaccessible

* use ffprobe for jellyfin movie statistics

* code cleanup

* store jellyfin operating system

* more jellyfin movie updates

* update jellyfin movie poster and fan art

* add jellyfin tv types

* sync jellyfin shows

* sync jellyfin seasons

* sync jellyfin episodes

* remove missing jellyfin television items

* delete empty jellyfin seasons and shows

* fix jellyfin updates

* fix indexing jellyfin movie and show languages
2021-05-15 13:14:17 -05:00
Jason Dove
27e0a70d93 add configurable library refresh interval (#184)
* add configurable library refresh interval

* code cleanup
2021-05-14 06:43:44 -05:00
Jason Dove
198e595bc6 add button to copy/clone ffmpeg profile (#183) 2021-05-01 07:45:33 -05:00
Jason Dove
b178b7402b upgrade to ffmpeg 4.4 (#182)
* bump docker images from ffmpeg 4.3 to 4.4 (#181)

* fix ffmpeg 4.4 compatibility
2021-04-28 18:28:07 -05:00
Jason Dove
1c51aed162 Revert "bump docker images from ffmpeg 4.3 to 4.4 (#181)"
This reverts commit ff6a4c5ea2.
2021-04-28 16:15:47 -05:00
Jason Dove
ff6a4c5ea2 bump docker images from ffmpeg 4.3 to 4.4 (#181) 2021-04-28 11:05:41 -05:00
Jason Dove
e515df93fd fix local movie scanner optimization (#180) 2021-04-27 20:13:19 -05:00
Jason Dove
fedc18f7db only show "movie" and "show" libraries from Plex (#179) 2021-04-25 04:24:38 -05:00
Jason Dove
59d75fe08f revert library_name index change, add library_id index (#178) 2021-04-23 08:37:48 -05:00
Jason Dove
49d9b1c714 add library search buttons (#177) 2021-04-22 21:31:19 -05:00
Jason Dove
2f066d5b62 update search docs [no docker] 2021-04-17 21:03:58 -05:00
Jason Dove
63db2edb99 fix plex actor artwork for first few actors (#176) 2021-04-17 15:39:24 -05:00
Jason Dove
5d01276ef3 fix plex actor artwork with newly added media items (#175) 2021-04-17 15:19:23 -05:00
Jason Dove
050aaaa288 fix updating music videos (#174) 2021-04-17 10:24:16 -05:00
Jason Dove
7c07c5f522 fix odd resolution padding; fix updating plex episode artwork (#173)
* fix padding odd resolutions

* fix updating plex episode artwork only as needed
2021-04-17 10:08:20 -05:00
Jason Dove
d8d21996b4 add actors to movies and shows (#172)
* add actor metadata

* show actors in ui

* get full movie/show metadata from plex

* store actor thumbnail url

* rework movie detail page

* metadata fixes

* rework show detail page

* rework artist page

* code cleanup
2021-04-17 09:36:57 -05:00
Jason Dove
e368d4a075 fix collections paging (#171) 2021-04-16 15:59:31 -05:00
Jason Dove
466059e2aa fix add to collection typing lag (#170) 2021-04-15 20:35:07 -05:00
Jason Dove
e951ecb650 fix lag when typing in search bar (#169) 2021-04-15 19:54:19 -05:00
Jason Dove
1d1f53da01 enter to submit all dialogs (#168) 2021-04-14 14:48:08 -05:00
Jason Dove
a854294cb6 allow enter key to submit add to collection dialog (#167) 2021-04-14 14:37:37 -05:00
Jason Dove
f89f3d2225 fix music videos in epg (#166) 2021-04-13 06:20:37 -05:00
Jason Dove
a2700e087c show release notes on home page (#165) 2021-04-11 12:34:41 -05:00
Jason Dove
34fbfce0a5 limit to one playout per channel (#164) 2021-04-11 05:55:26 -05:00
Jason Dove
993293c104 add search docs [no docker] 2021-04-11 05:34:20 -05:00
Jason Dove
ececa62446 fix synchronizing plex show metadata (#163) 2021-04-10 20:57:09 -05:00
Jason Dove
237729e79d add movie, show, artist language buttons. search by english language name (#162) 2021-04-10 20:40:54 -05:00
Jason Dove
9c0ada2df5 fix television metadata (#161) 2021-04-10 18:40:18 -05:00
Jason Dove
dee264597b fix search index warning 2021-04-10 13:07:58 -05:00
Jason Dove
a8db294043 add local libraries doc [no docker] 2021-04-09 18:33:28 -05:00
Jason Dove
a2a63e0120 add creative commons attribution [no docker] 2021-04-09 15:11:22 -05:00
Jason Dove
c7881aec14 remove screenshots [no docker] 2021-04-09 14:39:10 -05:00
Jason Dove
558bdcb6b0 client setup doc updates (#160)
* update jellyfin client setup

* update channels-dvr setup
2021-04-09 13:11:49 -05:00
Jason Dove
24f2b4b727 force music video library scan (#159) 2021-04-09 07:42:13 -05:00
Jason Dove
667887f387 fix television show and season playouts (#158) 2021-04-09 07:24:12 -05:00
Jason Dove
98eb72fcfe skip docker with [no docker] 2021-04-09 06:42:16 -05:00
Jason Dove
c2f92fd054 add github release links to docs [no ci] 2021-04-09 06:39:22 -05:00
Jason Dove
f04ddd3a40 save collection page size (#157) 2021-04-09 06:30:46 -05:00
Jason Dove
aa0942384d fix removing deleted music videos (#156) 2021-04-09 05:34:28 -05:00
Jason Dove
cd100be3a2 relax music video naming requirements (#155) 2021-04-09 05:22:02 -05:00
Jason Dove
2b26a5411c add artists as owners of music videos (#154)
* clean up genre, tag, studio orphans

* enforce foreign keys at connection level

* wip

* fix fragment scroll offset

* fix see all link for music videos

* add fake artist metadata

* not null artist id

* add artist scanning

* remove improperly named music videos

* code cleanup

* add artists to search results and collections

* clean up music video metadata / artist

* add artist view

* show music videos on artist page

* add music video artwork placeholder
2021-04-09 05:10:58 -05:00
Jason Dove
baf81f31cd fix plex server and connection sync (#153) 2021-04-08 18:48:12 -05:00
Jason Dove
bfa290790b trim parsed music video titles (#152) 2021-04-07 16:36:20 -05:00
Jason Dove
b975922a77 only index video stream languages for movies and music videos (#151) 2021-04-07 16:12:39 -05:00
Jason Dove
1a39978a77 try to fix windows builds 2021-04-07 05:34:02 -05:00
Jason Dove
436c9119fa add all search results to collection (#150) 2021-04-06 20:43:44 -05:00
Jason Dove
33642a13ce allow manual ci trigger 2021-04-06 09:21:54 -05:00
1289 changed files with 357211 additions and 22264 deletions

View File

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

26
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
version: 2
updates:
- package-ecosystem: nuget
directory: "/"
schedule:
interval: daily
assignees:
- 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

235
.github/workflows/artifacts.yml vendored Normal file
View File

@@ -0,0 +1,235 @@
name: Build Artifacts
on:
workflow_call:
inputs:
release_tag:
description: 'Release tag'
required: true
type: string
release_version:
description: 'Release version number (e.g. v0.3.7-alpha)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
secrets:
apple_developer_certificate_p12_base64:
required: true
apple_developer_certificate_password:
required: true
ac_username:
required: true
ac_password:
required: true
gh_token:
required: true
jobs:
build_and_upload_mac:
name: Mac Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: macos-latest
kind: macOS
target: osx-x64
- os: macos-latest
kind: macOS
target: osx-arm64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
p12-password: ${{ secrets.apple_developer_certificate_password }}
- name: Calculate Release Name
shell: bash
run: |
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
- name: Build
shell: bash
run: dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
- name: Bundle
shell: bash
run: |
brew install coreutils
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
scripts/macOS/bundle.sh
- name: Sign
shell: bash
run: scripts/macOS/sign.sh
- name: Create DMG
shell: bash
run: |
brew install create-dmg
create-dmg \
--volname "ErsatzTV" \
--volicon "artwork/ErsatzTV.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "ErsatzTV.app" 200 190 \
--hide-extension "ErsatzTV.app" \
--app-drop-link 600 185 \
"ErsatzTV.dmg" \
"ErsatzTV.app/"
- name: Notarize
shell: bash
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
gon -log-level=debug -log-json ./gon.json
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Cleanup
shell: bash
run: |
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
rm -r publish
rm -r ErsatzTV.app
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: ubuntu-latest
kind: linux
target: linux-arm64
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- uses: suisei-cn/actions-download-file@v1
if: ${{ matrix.kind }} == "windows"
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/GyanD/codexffmpeg/releases/download/5.0/ffmpeg-5.0-full_build.7z"
target: ffmpeg/
- name: Build
shell: bash
run: |
# Define some variables for things we need
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
# Build Windows launcher
if [ "${{ matrix.kind }}" == "windows" ]; then
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
fi
# Download ffmpeg
if [ "${{ matrix.kind }}" == "windows" ]; then
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
rm -f "$release_name/ffplay.exe"
fi
# Pack files
if [ "${{ matrix.kind }}" == "windows" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
env:
AC_USERNAME: ${{ secrets.ac_username }}
AC_PASSWORD: ${{ secrets.ac_password }}
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}

View File

@@ -1,109 +1,58 @@
name: Build
on:
pull_request:
workflow_dispatch:
push:
branches:
- main
jobs:
build_and_test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
calculate_version:
name: Calculate version information
runs-on: ubuntu-latest
steps:
- name: Get the sources
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 ci]')
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Extract Git Tag
- name: Extract Docker Tag
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
tag2="${tag:1}"
short=$(git rev-parse --short HEAD)
final="${tag2/prealpha/$short}"
final="${tag2/alpha/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV
- 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=${{ env.GIT_TAG }}-docker
tags: |
jasongdove/ersatztv:develop
jasongdove/ersatztv:${{ github.sha }}
- 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=${{ 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
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
final="${tag/alpha/$short}"
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
git_tag: ${{ env.GIT_TAG }}
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: develop
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: develop
info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ github.sha }}
secrets:
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

88
.github/workflows/docker.yml vendored Normal file
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

View File

@@ -3,14 +3,13 @@ on:
push:
branches:
- main
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master

30
.github/workflows/pr.yml vendored Normal file
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

View File

@@ -1,141 +1,53 @@
name: Publish
name: Release
on:
release:
types: [ published ]
jobs:
release:
name: Release
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: windows-latest
kind: windows
target: win-x64
- os: macos-latest
kind: maxOS
target: osx-x64
runs-on: ${{ matrix.os }}
calculate_version:
name: Calculate version information
runs-on: ubuntu-latest
steps:
- name: Get the sources
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 }}"
#release_name_cli="ErsatzTV.CommandLine-$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 }}"
#dotnet publish ErsatzTV.CommandLine/ErsatzTV.CommandLine.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name_cli" /property:InformationalVersion="${tag:1}-${{ matrix.target }}"
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
#7z a -tzip "${release_name_cli}.zip" "./${release_name_cli}/*"
else
tar czvf "${release_name}.tar.gz" "$release_name"
#tar czvf "${release_name_cli}.tar.gz" "$release_name_cli"
fi
# Delete output directory
rm -r "$release_name"
#rm -r "$release_name_cli"
- 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:
fetch-depth: 0
- name: Extract Git Tag
- name: Extract Docker Tag
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-prealpha/}" >> $GITHUB_ENV
- 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=${{ env.GIT_TAG }}-docker
tags: |
jasongdove/ersatztv:latest
jasongdove/ersatztv:${{ env.DOCKER_TAG }}
- 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=${{ 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
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
git_tag: ${{ env.GIT_TAG }}
docker_tag: ${{ env.DOCKER_TAG }}
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: jasongdove/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: jasongdove/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: latest
info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
secrets:
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:jasongdove/ErsatzTV-macOS.git

1053
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<RootNamespace>ErsatzTV_Windows</RootNamespace>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>Ersatztv.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="Ersatztv.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Asmichi.ChildProcess" Version="0.11.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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());
}
}

View File

@@ -0,0 +1,77 @@
using ErsatzTV.Core;
using System.Diagnostics;
using Asmichi.ProcessManagement;
using System.Reflection;
namespace ErsatzTV_Windows;
public class TrayApplicationContext : ApplicationContext
{
private readonly NotifyIcon _trayIcon;
private readonly IChildProcess? _childProcess;
public TrayApplicationContext()
{
_trayIcon = new NotifyIcon
{
Icon = new Icon("./Ersatztv.ico"),
ContextMenuStrip = new ContextMenuStrip(),
Visible = true
};
AddMenuItem("Launch Web UI", LaunchWebUI);
AddMenuItem("Show Logs", ShowLogs);
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
AddMenuItem("Exit", Exit);
string folder = AppContext.BaseDirectory;
string exe = Path.Combine(folder, "ErsatzTV.exe");
if (File.Exists(exe))
{
var si = new ChildProcessStartInfo(exe);
_childProcess = ChildProcess.Start(si);
}
}
private void AddMenuItem(string name, EventHandler action)
{
var item = new ToolStripMenuItem(name);
item.Click += action;
_trayIcon.ContextMenuStrip.Items.Add(item);
}
private void LaunchWebUI(object? sender, EventArgs e)
{
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = "http://localhost:8409";
process.Start();
}
private void ShowLogs(object? sender, EventArgs e)
{
if (!Directory.Exists(FileSystemLayout.LogsFolder))
{
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
}
var process = new Process();
process.StartInfo.UseShellExecute = true;
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
process.Start();
}
protected override void Dispose(bool disposing)
{
_childProcess?.Dispose();
base.Dispose(disposing);
}
private void Exit(object? sender, EventArgs e)
{
// Hide tray icon, otherwise it will remain shown until user mouses over it
_trayIcon.Visible = false;
Application.Exit();
}
}

1
ErsatzTV-macOS Submodule

Submodule ErsatzTV-macOS added at 2f3ee16f11

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Globalization;
namespace ErsatzTV.Application.Artists
{
public record ArtistViewModel(
string Name,
string Disambiguation,
string Biography,
string Thumbnail,
string FanArt,
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ErsatzTV.Core.Domain;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Artists
{
internal static class Mapper
{
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
return new ArtistViewModel(
metadata.Title,
metadata.Disambiguation,
metadata.Biography,
Artwork(metadata, ArtworkKind.Thumbnail),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(),
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();
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using MediatR;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
}

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.Artists.Queries
{
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
{
private readonly IArtistRepository _artistRepository;
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
CancellationToken cancellationToken) =>
_artistRepository.GetAllArtists()
.Map(
list => list.Filter(
a => !string.IsNullOrWhiteSpace(
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists.Queries
{
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
{
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _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));
}
}
}

View File

@@ -9,5 +9,8 @@ namespace ErsatzTV.Application.Channels
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode);
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
int PlayoutCount);
}

View File

@@ -12,5 +12,7 @@ namespace ErsatzTV.Application.Channels.Commands
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
}

View File

@@ -7,42 +7,46 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Channels.Mapper;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands
{
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, ChannelViewModel>>
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{
private readonly IChannelRepository _channelRepository;
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(
IChannelRepository channelRepository,
IFFmpegProfileRepository ffmpegProfileRepository)
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
_channelRepository = channelRepository;
_ffmpegProfileRepository = ffmpegProfileRepository;
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
public Task<Either<BaseError, ChannelViewModel>> Handle(
CreateChannel request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistChannel)
.Bind(v => v.ToEitherAsync());
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private Task<ChannelViewModel> PersistChannel(Channel c) =>
_channelRepository.Add(c).Map(ProjectToViewModel);
private async Task<Validation<BaseError, Channel>> Validate(CreateChannel request) =>
(ValidateName(request), await ValidateNumber(request), await FFmpegProfileMustExist(request),
ValidatePreferredLanguage(request))
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) =>
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
@@ -57,7 +61,7 @@ namespace ErsatzTV.Application.Channels.Commands
});
}
return new Channel(Guid.NewGuid())
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
@@ -66,22 +70,35 @@ namespace ErsatzTV.Application.Channels.Commands
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
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 async Task<Validation<BaseError, string>> ValidateNumber(CreateChannel createChannel)
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
{
Option<Channel> maybeExistingChannel = await _channelRepository.GetByNumber(createChannel.Number);
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"),
() =>
@@ -95,9 +112,51 @@ namespace ErsatzTV.Application.Channels.Commands
});
}
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(CreateChannel createChannel) =>
(await _ffmpegProfileRepository.Get(createChannel.FFmpegProfileId))
.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist.")
.Map(c => c.Id);
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."));
}
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
}

View File

@@ -13,5 +13,7 @@ namespace ErsatzTV.Application.Channels.Commands
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode) : IRequest<Either<BaseError, ChannelViewModel>>;
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
}

View File

@@ -7,9 +7,11 @@ using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
@@ -17,28 +19,30 @@ namespace ErsatzTV.Application.Channels.Commands
{
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Either<BaseError, ChannelViewModel>> Handle(
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
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(Channel c, UpdateChannel update)
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))
{
c.Artwork ??= new List<Artwork>();
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
@@ -60,29 +64,41 @@ namespace ErsatzTV.Application.Channels.Commands
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
await _channelRepository.Update(c);
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(UpdateChannel request) =>
(await ChannelMustExist(request), ValidateName(request), await ValidateNumber(request),
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 Task<Validation<BaseError, Channel>> ChannelMustExist(UpdateChannel updateChannel) =>
_channelRepository.Get(updateChannel.ChannelId)
.Map(v => v.ToValidation<BaseError>("Channel does not exist."));
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 Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private async Task<Validation<BaseError, string>> ValidateNumber(UpdateChannel updateChannel)
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
Option<Channel> match = await _channelRepository.GetByNumber(updateChannel.Number);
int matchId = await match.Map(c => c.Id).IfNoneAsync(updateChannel.ChannelId);
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))
@@ -96,7 +112,7 @@ namespace ErsatzTV.Application.Channels.Commands
return BaseError.New("Channel number must be unique");
}
private Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(

View File

@@ -14,10 +14,17 @@ namespace ErsatzTV.Application.Channels
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode);
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
private static string GetWatermark(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark))
.Match(a => a.Path, string.Empty);
}
}

View File

@@ -0,0 +1,6 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;

View File

@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<GetChannelFramerateHandler> _logger;
public GetChannelFramerateHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<GetChannelFramerateHandler> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
return None;
}
private int ParseFrameRate(string frameRate)
{
if (!int.TryParse(frameRate, out int fr))
{
string[] split = (frameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
}
}
return fr;
}
}

View File

@@ -3,5 +3,5 @@ using MediatR;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelPlaylist(string Scheme, string Host) : IRequest<ChannelPlaylist>;
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
}

View File

@@ -1,5 +1,7 @@
using System.Threading;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using LanguageExt;
@@ -16,6 +18,39 @@ namespace ErsatzTV.Application.Channels.Queries
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>();
foreach (Channel channel in channels)
{
switch (mode.ToLowerInvariant())
{
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
}
return result;
}
}
}

View File

@@ -0,0 +1,7 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
}

View File

@@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
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)
{
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
}
}

View File

@@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
}

View File

@@ -0,0 +1,33 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Where(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
}
}

View File

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

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _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)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Configuration
{
public record ConfigElementViewModel(string Key, string Value);
}

View File

@@ -0,0 +1,10 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Configuration
{
internal static class Mapper
{
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
}

View File

@@ -0,0 +1,22 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Configuration.Mapper;
namespace ErsatzTV.Application.Configuration.Queries
{
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{
private readonly IConfigElementRepository _configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
}
}

View File

@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetLibraryRefreshInterval : IRequest<int>;
}

View File

@@ -0,0 +1,21 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
private readonly IConfigElementRepository _configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
}

View File

@@ -0,0 +1,6 @@
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetPlayoutDaysToBuild : IRequest<int>;
}

View File

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

View File

@@ -0,0 +1,7 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
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)
{
_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;
}
}
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
}

View File

@@ -0,0 +1,60 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
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)
{
_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);
}
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;
}

View File

@@ -0,0 +1,118 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Emby.Commands
{
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
{
private readonly IEmbyApiClient _embyApiClient;
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)
{
_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)
.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; }
}
}
}

View File

@@ -0,0 +1,23 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
{
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
IEmbyBackgroundServiceRequest
{
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;
}
}

View File

@@ -0,0 +1,181 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Emby.Commands
{
public class SynchronizeEmbyLibraryByIdHandler :
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
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)
{
_mediaSourceRepository = mediaSourceRepository;
_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(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
case LibraryMediaKind.Shows:
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
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);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters,
embyLibrary,
request.ForceScan,
libraryRefreshInterval,
ffprobePath
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizeEmbyLibraryById request) =>
EmbyMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source for library {request.EmbyLibraryId} 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 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; }
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
{
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IMediaSourceRepository _mediaSourceRepository;
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;
}
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _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;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
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)
{
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."));
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.Emby
{
public record EmbyConnectionParametersViewModel(string Address);
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby
{
public record EmbyLibraryViewModel(int Id, string Name, LibraryMediaKind MediaKind, bool ShouldSyncItems)
: LibraryViewModel("Emby", Id, Name, MediaKind);
}

View File

@@ -0,0 +1,9 @@
using ErsatzTV.Application.MediaSources;
namespace ErsatzTV.Application.Emby
{
public record EmbyMediaSourceViewModel(int Id, string Name, string Address) : RemoteMediaSourceViewModel(
Id,
Name,
Address);
}

View File

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

View File

@@ -0,0 +1,19 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Emby
{
internal static class Mapper
{
internal static EmbyMediaSourceViewModel ProjectToViewModel(EmbyMediaSource embyMediaSource) =>
new(
embyMediaSource.Id,
embyMediaSource.ServerName,
embyMediaSource.Connections.HeadOrNone().Match(c => c.Address, string.Empty));
internal static EmbyLibraryViewModel ProjectToViewModel(EmbyLibrary library) =>
new(library.Id, library.Name, library.MediaKind, library.ShouldSyncItems);
internal static EmbyPathReplacementViewModel ProjectToViewModel(EmbyPathReplacement pathReplacement) =>
new(pathReplacement.Id, pathReplacement.EmbyPath, pathReplacement.LocalPath);
}
}

View File

@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetAllEmbyMediaSources : IRequest<List<EmbyMediaSourceViewModel>>;
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
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;
namespace ErsatzTV.Application.Emby.Queries
{
public class GetAllEmbyMediaSourcesHandler : IRequestHandler<GetAllEmbyMediaSources, List<EmbyMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetAllEmbyMediaSourcesHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyMediaSourceViewModel>> Handle(
GetAllEmbyMediaSources request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetAllEmby().Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyConnectionParameters : IRequest<Either<BaseError, EmbyConnectionParametersViewModel>>;
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Caching.Memory;
namespace ErsatzTV.Application.Emby.Queries
{
public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnectionParameters,
Either<BaseError, EmbyConnectionParametersViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly IMemoryCache _memoryCache;
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);
}
}

View File

@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyLibrariesBySourceId(int EmbyMediaSourceId) : IRequest<List<EmbyLibraryViewModel>>;
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
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;
namespace ErsatzTV.Application.Emby.Queries
{
public class
GetEmbyLibrariesBySourceIdHandler : IRequestHandler<GetEmbyLibrariesBySourceId, List<EmbyLibraryViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyLibrariesBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyLibraryViewModel>> Handle(
GetEmbyLibrariesBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyLibraries(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -0,0 +1,7 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyMediaSourceById(int EmbyMediaSourceId) : IRequest<Option<EmbyMediaSourceViewModel>>;
}

View File

@@ -0,0 +1,23 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Emby.Mapper;
namespace ErsatzTV.Application.Emby.Queries
{
public class
GetEmbyMediaSourceByIdHandler : IRequestHandler<GetEmbyMediaSourceById, Option<EmbyMediaSourceViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyMediaSourceByIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Option<EmbyMediaSourceViewModel>> Handle(
GetEmbyMediaSourceById request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId).MapT(ProjectToViewModel);
}
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbyPathReplacementsBySourceId
(int EmbyMediaSourceId) : IRequest<List<EmbyPathReplacementViewModel>>;
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
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;
namespace ErsatzTV.Application.Emby.Queries
{
public class GetEmbyPathReplacementsBySourceIdHandler : IRequestHandler<GetEmbyPathReplacementsBySourceId,
List<EmbyPathReplacementViewModel>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
public GetEmbyPathReplacementsBySourceIdHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<List<EmbyPathReplacementViewModel>> Handle(
GetEmbyPathReplacementsBySourceId request,
CancellationToken cancellationToken) =>
_mediaSourceRepository.GetEmbyPathReplacements(request.EmbyMediaSourceId)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}

View File

@@ -0,0 +1,7 @@
using ErsatzTV.Core.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public record GetEmbySecrets : IRequest<EmbySecrets>;
}

View File

@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using MediatR;
namespace ErsatzTV.Application.Emby.Queries
{
public class GetEmbySecretsHandler : IRequestHandler<GetEmbySecrets, EmbySecrets>
{
private readonly IEmbySecretStore _embySecretStore;
public GetEmbySecretsHandler(IEmbySecretStore embySecretStore) =>
_embySecretStore = embySecretStore;
public Task<EmbySecrets> Handle(GetEmbySecrets request, CancellationToken cancellationToken) =>
_embySecretStore.ReadSecrets();
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application
{
public record EntityIdResult(int Id);
}

View File

@@ -1,27 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AsyncFixer" Version="1.5.1">
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MediatR" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CopyFFmpegProfile
(int FFmpegProfileId, string Name) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
}

View File

@@ -0,0 +1,37 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CopyFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformCopy)
.Bind(v => v.ToEitherAsync());
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel);
private Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request);
private Validation<BaseError, string> ValidateName(CopyFFmpegProfile request) =>
request.NotEmpty(x => x.Name)
.Bind(_ => request.NotLongerThan(50)(x => x.Name));
}
}

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using LanguageExt;
using MediatR;
@@ -10,6 +11,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
@@ -22,5 +25,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
bool NormalizeFramerate) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;
}

View File

@@ -2,39 +2,42 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
CreateFFmpegProfileHandler : IRequestHandler<CreateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(PersistFFmpegProfile)
.Bind(v => v.ToEitherAsync());
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
private Task<FFmpegProfileViewModel> PersistFFmpegProfile(FFmpegProfile ffmpegProfile) =>
_ffmpegProfileRepository.Add(ffmpegProfile).Map(ProjectToViewModel);
private async Task<Validation<BaseError, FFmpegProfile>> Validate(CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(request))
private async Task<Validation<BaseError, FFmpegProfile>> Validate(TvContext dbContext, CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply(
(name, threadCount, resolutionId) => new FFmpegProfile
{
@@ -42,6 +45,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
ThreadCount = threadCount,
Transcode = request.Transcode,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
ResolutionId = resolutionId,
NormalizeVideo = request.NormalizeVideo,
VideoCodec = request.VideoCodec,
@@ -54,19 +59,22 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeAudio = request.NormalizeAudio,
FrameRate = request.FrameRate
NormalizeFramerate = request.NormalizeFramerate
});
private Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(CreateFFmpegProfile createFFmpegProfile) =>
(await _resolutionRepository.Get(createFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, int>> ResolutionMustExist(
TvContext dbContext,
CreateFFmpegProfile createFFmpegProfile) =>
dbContext.Resolutions
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId)
.MapT(r => r.Id)
.Map(o => o.ToValidation<BaseError>($"[Resolution] {createFFmpegProfile.ResolutionId} does not exist"));
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record CreateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}

View File

@@ -1,9 +1,7 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record DeleteFFmpegProfile(int FFmpegProfileId) : IRequest<Either<BaseError, Task>>;
public record DeleteFFmpegProfile(int FFmpegProfileId) : MediatR.IRequest<Either<BaseError, Unit>>;
}

View File

@@ -1,32 +1,43 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Task>>
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, LanguageExt.Unit>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Task>> Handle(
public async Task<Either<BaseError, LanguageExt.Unit>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken) =>
(await FFmpegProfileMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await validation.Apply(p => DoDeletion(dbContext, p));
}
private Task DoDeletion(int channelId) => _ffmpegProfileRepository.Delete(channelId);
private static async Task<LanguageExt.Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
return LanguageExt.Unit.Default;
}
private async Task<Validation<BaseError, int>> FFmpegProfileMustExist(
DeleteFFmpegProfile deleteFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(deleteFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>($"FFmpegProfile {deleteFFmpegProfile.FFmpegProfileId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
DeleteFFmpegProfile request) =>
dbContext.FFmpegProfiles
.SelectOneAsync(p => p.Id, p => p.Id == request.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {request.FFmpegProfileId} does not exist"));
}
}

View File

@@ -2,9 +2,11 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
@@ -12,24 +14,21 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class NewFFmpegProfileHandler : IRequestHandler<NewFFmpegProfile, FFmpegProfileViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public NewFFmpegProfileHandler(
IResolutionRepository resolutionRepository,
IConfigElementRepository configElementRepository)
{
_resolutionRepository = resolutionRepository;
_configElementRepository = configElementRepository;
}
public NewFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<FFmpegProfileViewModel> Handle(NewFFmpegProfile request, CancellationToken cancellationToken)
{
int defaultResolutionId = await _configElementRepository
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
int defaultResolutionId = await dbContext.ConfigElements
.GetValue<int>(ConfigElementKey.FFmpegDefaultResolutionId)
.IfNoneAsync(0);
List<Resolution> allResolutions = await _resolutionRepository.GetAll();
List<Resolution> allResolutions = await dbContext.Resolutions
.ToListAsync(cancellationToken);
Option<Resolution> maybeDefaultResolution = allResolutions.Find(r => r.Id == defaultResolutionId);
Resolution defaultResolution = maybeDefaultResolution.Match(identity, () => allResolutions.Head());

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using LanguageExt;
using MediatR;
@@ -11,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
int ResolutionId,
bool NormalizeVideo,
string VideoCodec,
@@ -23,5 +26,5 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
string FrameRate) : IRequest<Either<BaseError, FFmpegProfileViewModel>>;
bool NormalizeFramerate) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;
}

View File

@@ -2,77 +2,86 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IResolutionRepository _resolutionRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateFFmpegProfileHandler(
IFFmpegProfileRepository ffmpegProfileRepository,
IResolutionRepository resolutionRepository)
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_resolutionRepository = resolutionRepository;
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, FFmpegProfile> validation = await Validate(dbContext, request);
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
UpdateFFmpegProfile request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(c => ApplyUpdateRequest(c, request))
.Bind(v => v.ToEitherAsync());
private async Task<FFmpegProfileViewModel> ApplyUpdateRequest(FFmpegProfile p, UpdateFFmpegProfile update)
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
{
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.Transcode = update.Transcode;
p.HardwareAcceleration = update.HardwareAcceleration;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
p.ResolutionId = update.ResolutionId;
p.NormalizeVideo = update.NormalizeVideo;
p.NormalizeVideo = update.Transcode && update.NormalizeVideo;
p.VideoCodec = update.VideoCodec;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioCodec = update.AudioCodec;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.NormalizeLoudness;
p.NormalizeLoudness = update.Transcode && update.NormalizeLoudness;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeAudio = update.NormalizeAudio;
p.FrameRate = update.FrameRate;
await _ffmpegProfileRepository.Update(p);
return ProjectToViewModel(p);
p.NormalizeAudio = update.Transcode && update.NormalizeAudio;
p.NormalizeFramerate = update.Transcode && update.NormalizeFramerate;
await dbContext.SaveChangesAsync();
return new UpdateFFmpegProfileResult(p.Id);
}
private async Task<Validation<BaseError, FFmpegProfile>> Validate(UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(request))
private static async Task<Validation<BaseError, FFmpegProfile>> Validate(
TvContext dbContext,
UpdateFFmpegProfile request) =>
(await FFmpegProfileMustExist(dbContext, request), ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(dbContext, request))
.Apply((ffmpegProfileToUpdate, _, _, _) => ffmpegProfileToUpdate);
private async Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
private static Task<Validation<BaseError, FFmpegProfile>> FFmpegProfileMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
(await _ffmpegProfileRepository.Get(updateFFmpegProfile.FFmpegProfileId))
.ToValidation<BaseError>("FFmpegProfile does not exist.");
dbContext.FFmpegProfiles
.SelectOneAsync(p => p.Id, p => p.Id == updateFFmpegProfile.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>("FFmpegProfile does not exist."));
private Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
private static Validation<BaseError, string> ValidateName(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => updateFFmpegProfile.NotLongerThan(50)(x => x.Name));
private Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
private static Validation<BaseError, int> ValidateThreadCount(UpdateFFmpegProfile updateFFmpegProfile) =>
updateFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private async Task<Validation<BaseError, int>> ResolutionMustExist(UpdateFFmpegProfile updateFFmpegProfile) =>
(await _resolutionRepository.Get(updateFFmpegProfile.ResolutionId))
.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist.")
.Map(c => c.Id);
private static Task<Validation<BaseError, int>> ResolutionMustExist(
TvContext dbContext,
UpdateFFmpegProfile updateFFmpegProfile) =>
dbContext.Resolutions
.SelectOneAsync(r => r.Id, r => r.Id == updateFFmpegProfile.ResolutionId)
.MapT(r => r.Id)
.Map(o => o.ToValidation<BaseError>($"[Resolution] {updateFFmpegProfile.ResolutionId} does not exist"));
}
}

View File

@@ -0,0 +1,4 @@
namespace ErsatzTV.Application.FFmpegProfiles.Commands
{
public record UpdateFFmpegProfileResult(int FFmpegProfileId) : EntityIdResult(FFmpegProfileId);
}

View File

@@ -71,32 +71,59 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
private async Task<Unit> ApplyUpdate(UpdateFFmpegSettings request)
{
await Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await Upsert(ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString());
await Upsert(ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString());
await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath);
await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegDefaultProfileId,
request.Settings.DefaultFFmpegProfileId.ToString());
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSaveReports,
request.Settings.SaveReports.ToString());
if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder))
{
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
await Upsert(ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode);
if (request.Settings.GlobalWatermarkId is not null)
{
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegGlobalWatermarkId,
request.Settings.GlobalWatermarkId.Value);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId);
}
if (request.Settings.GlobalFallbackFillerId is not null)
{
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegGlobalFallbackFillerId,
request.Settings.GlobalFallbackFillerId.Value);
}
else
{
await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId);
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegSegmenterTimeout,
request.Settings.HlsSegmenterIdleTimeout);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegWorkAheadSegmenters,
request.Settings.WorkAheadSegmenterLimit);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegInitialSegmentCount,
request.Settings.InitialSegmentCount);
return Unit.Default;
}
private Task Upsert(ConfigElementKey key, string value) =>
_configElementRepository.Get(key).Match(
ce =>
{
ce.Value = value;
return _configElementRepository.Update(ce);
},
() =>
{
var ce = new ConfigElement { Key = key.Key, Value = value };
return _configElementRepository.Add(ce);
});
}
}

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Application.Resolutions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
namespace ErsatzTV.Application.FFmpegProfiles
{
@@ -9,6 +10,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
int ThreadCount,
bool Transcode,
HardwareAccelerationKind HardwareAcceleration,
VaapiDriver VaapiDriver,
string VaapiDevice,
ResolutionViewModel Resolution,
bool NormalizeVideo,
string VideoCodec,
@@ -21,5 +24,5 @@ namespace ErsatzTV.Application.FFmpegProfiles
int AudioChannels,
int AudioSampleRate,
bool NormalizeAudio,
string FrameRate);
bool NormalizeFramerate);
}

View File

@@ -7,5 +7,10 @@
public int DefaultFFmpegProfileId { get; set; }
public string PreferredLanguageCode { get; set; }
public bool SaveReports { get; set; }
public int? GlobalWatermarkId { get; set; }
public int? GlobalFallbackFillerId { get; set; }
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
}
}

View File

@@ -12,6 +12,8 @@ namespace ErsatzTV.Application.FFmpegProfiles
profile.ThreadCount,
profile.Transcode,
profile.HardwareAcceleration,
profile.VaapiDriver,
profile.VaapiDevice,
Project(profile.Resolution),
profile.NormalizeVideo,
profile.VideoCodec,
@@ -24,7 +26,7 @@ namespace ErsatzTV.Application.FFmpegProfiles
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeAudio,
profile.FrameRate);
profile.NormalizeVideo && profile.NormalizeFramerate);
private static ResolutionViewModel Project(Resolution resolution) =>
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);

View File

@@ -2,22 +2,30 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetAllFFmpegProfilesHandler : IRequestHandler<GetAllFFmpegProfiles, List<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllFFmpegProfilesHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public GetAllFFmpegProfilesHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<List<FFmpegProfileViewModel>> Handle(
GetAllFFmpegProfiles request,
CancellationToken cancellationToken) =>
(await _ffmpegProfileRepository.GetAll()).Map(ProjectToViewModel).ToList();
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());
}
}
}

View File

@@ -1,23 +1,30 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles.Queries
{
public class GetFFmpegProfileByIdHandler : IRequestHandler<GetFFmpegProfileById, Option<FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFFmpegProfileByIdHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
_ffmpegProfileRepository = ffmpegProfileRepository;
public GetFFmpegProfileByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public Task<Option<FFmpegProfileViewModel>> Handle(
public async Task<Option<FFmpegProfileViewModel>> Handle(
GetFFmpegProfileById request,
CancellationToken cancellationToken) =>
_ffmpegProfileRepository.Get(request.Id)
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.FFmpegProfiles
.Include(p => p.Resolution)
.SelectOneAsync(p => p.Id, p => p.Id == request.Id)
.MapT(ProjectToViewModel);
}
}
}

View File

@@ -26,15 +26,40 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalFallbackFillerId);
Option<int> hlsSegmenterIdleTimeout =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegSegmenterTimeout);
Option<int> workAheadSegmenterLimit =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
return new FFmpegSettingsViewModel
var result = new FFmpegSettingsViewModel
{
FFmpegPath = await ffmpegPath.IfNoneAsync(string.Empty),
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
SaveReports = await saveReports.IfNoneAsync(false),
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng")
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
};
foreach (int watermarkId in watermark)
{
result.GlobalWatermarkId = watermarkId;
}
foreach (int fallbackFillerId in fallbackFiller)
{
result.GlobalFallbackFillerId = fallbackFillerId;
}
return result;
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using LanguageExt;
using MediatR;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public record CreateFillerPreset(
string Name,
FillerKind FillerKind,
FillerMode FillerMode,
TimeSpan? Duration,
int? Count,
int? PadToNearestMinute,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
) : IRequest<Either<BaseError, Unit>>;
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Filler.Commands
{
public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFillerPresetHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle(CreateFillerPreset request, CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
var fillerPreset = new FillerPreset
{
Name = request.Name,
FillerKind = request.FillerKind,
FillerMode = request.FillerMode,
Duration = request.Duration,
Count = request.Count,
PadToNearestMinute = request.PadToNearestMinute,
CollectionType = request.CollectionType,
CollectionId = request.CollectionId,
MediaItemId = request.MediaItemId,
MultiCollectionId = request.MultiCollectionId,
SmartCollectionId = request.SmartCollectionId
};
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
return Unit.Default;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}
}

View File

@@ -0,0 +1,8 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Filler.Commands
{
public record DeleteFillerPreset(int FillerPresetId) : IRequest<Either<BaseError, LanguageExt.Unit>>;
}

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