Compare commits

...

533 Commits

Author SHA1 Message Date
Jason Dove
fd7c3fc25a prep for release v25.4.0 [no ci] 2025-08-05 12:08:31 -05:00
Jason Dove
93dca6e0e0 fix framerate check for remote streams (#2264) 2025-08-05 14:35:13 +00:00
Jason Dove
e34368bf07 fix some yaml schema oneOf => anyOf (#2263) 2025-08-05 13:35:26 +00:00
Jason Dove
a4b485f562 add yaml validation tool (#2259)
* reorganize troubleshooting page

* add yaml troubleshooting tool
2025-08-05 03:07:26 +00:00
Jason Dove
6159b6a5b2 support more music video thumbnail filenames (#2258) 2025-08-04 23:17:16 +00:00
Jason Dove
11100a788b fix yaml guid validation (#2257) 2025-08-04 22:22:37 +00:00
Jason Dove
b40ac9ef52 replace channel active mode with is enabled and show in epg (#2256)
* add channel enabled setting

* remove channel active mode
2025-08-04 21:24:26 +00:00
Jason Dove
c055e59723 add channel transcode mode and idle behavior (#2255)
* add channel transcode mode and idle behavior

* allow custom_title on all yaml content instructions
2025-08-04 20:25:31 +00:00
Jason Dove
b52159e8db rename channel progress mode to playout mode (#2254) 2025-08-04 19:27:22 +00:00
Jason Dove
a728c5e31e add smart collection editor to support renaming (#2253) 2025-08-04 16:24:08 +00:00
Jason Dove
61ce1bad08 always schedule full duration (#2252) 2025-08-04 15:31:04 +00:00
Jason Dove
ab2b926de0 add searching log category (#2251) 2025-08-04 14:51:13 +00:00
Jason Dove
3b955255ce fix building yaml playouts with no imports (#2249) 2025-08-04 03:43:30 +00:00
Jason Dove
16dd2c2d81 add yaml import section (#2248) 2025-08-04 02:21:56 +00:00
Chris Simpson
48f93b8af8 Support individual chapters as filler (#2208)
* Use chapters in duration filler

* add new option, migrations, and update filler preset editor

* Revert "Use chapters in duration filler"

This reverts commit d87a8a240a78c1cbca7b311125f8d3a84645d296.

* scaffold splitting filler by chapter

* implement chapters as filler

* update changelog

* re-add migrations

* Add duration for ChapterMediaItem

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2025-08-04 00:18:14 +00:00
Jason Dove
8b12ee459a fix transitions on nvidia, vaapi, qsv (#2247) 2025-08-03 20:26:16 +00:00
Jason Dove
b3d0b44e77 fix qsv transitions (#2246)
* fix qsv transitions

* revert unintended change
2025-08-03 16:01:48 +00:00
Jason Dove
163fd0c1f3 restore noautoscale in nvidia pipeline (#2245) 2025-08-03 14:57:30 +00:00
Jason Dove
b6ec16c6a7 fix transitions using nvidia accel (#2244) 2025-08-03 14:14:36 +00:00
Jason Dove
aa3bd3b750 add yaml playout rewind instruction (#2243) 2025-08-03 13:29:44 +00:00
Jason Dove
f04b7ead09 fix yaml playout builds (#2241) 2025-08-03 01:18:27 +00:00
Jason Dove
8921273900 detect some videotoolbox decoders (#2240) 2025-08-02 18:38:49 +00:00
Jason Dove
0489741123 add videotoolbox capabilities (#2239)
* implement videotoolbox hardware capabilities

* add videotoolbox troubleshooting info

* update changelog
2025-08-02 17:16:24 +00:00
Jason Dove
c3e882085b remove extra windows Resources folder 2025-08-02 10:08:45 -05:00
Jason Dove
3ab9112c15 fix folders in windows artifact 2025-08-02 09:30:17 -05:00
Jason Dove
33b789db67 remove unneeded commands from windows build 2025-08-02 09:17:14 -05:00
Jason Dove
ed5206b855 rework windows artifact builds (#2238) 2025-08-02 14:11:54 +00:00
Jason Dove
baf7aa20d1 build windows artifacts on linux (#2237) 2025-08-02 13:50:54 +00:00
Jason Dove
7bd0de99e1 fix gaps in yaml playouts (#2235)
* dont run multiple dotnet builds in background

* fix gaps in yaml playouts
2025-08-02 03:42:42 +00:00
Jason Dove
96093c8cc8 build artifacts as background processes (#2234) 2025-08-02 03:27:18 +00:00
Jason Dove
8430a3048c fix yaml playout builds after refactor (#2233) 2025-08-02 03:12:37 +00:00
Jason Dove
06d9e59a7a add yaml mid roll instruction (#2232)
* refactor filler expression

* add yaml mid roll instruction

* schedule midroll for yaml count and all instructions

* update changelog
2025-08-02 00:16:50 +00:00
Jason Dove
9c434079d5 add playlist support to filler preset (#2231) 2025-08-01 19:13:18 +00:00
Jason Dove
12c88a006d add yaml post_roll instruction (#2230) 2025-08-01 17:02:36 +00:00
Jason Dove
f0ca358c2b fully validate yaml playouts (#2229) 2025-08-01 16:20:53 +00:00
Jason Dove
093abf7ad8 add yaml playout pre_roll instruction (#2228)
* add yaml playout pre_roll instruction

* add basic yaml validation

* validate all yaml playout content items

* fix yaml to json conversion

* update changelog
2025-08-01 15:20:10 +00:00
Jason Dove
f768093df7 update dependencies (#2226) 2025-07-31 17:09:39 +00:00
Jason Dove
3830db60bf another small update for a new build 2025-07-31 12:00:36 -05:00
Jason Dove
5984b38ce0 small change to get new build 2025-07-30 16:51:26 -05:00
Jason Dove
e0175fc4e5 add light mode (#2223) 2025-07-29 18:48:12 +00:00
Jason Dove
4f104cff5b some fixes for alternate schedules (#2222) 2025-07-29 11:55:34 +00:00
Jason Dove
a2f678fe8e fix adding new items from plex libraries (#2220) 2025-07-28 22:55:53 +00:00
Jason Dove
b3ac0c68a8 fix green padding with 10-bit content on i965 vaapi (#2219) 2025-07-28 21:42:23 +00:00
Jason Dove
605d8a98ab fix adding new items from jellyfin and emby (#2218) 2025-07-28 20:37:34 +00:00
Jason Dove
00f40c2568 fix migrations for new databases (#2217) 2025-07-28 20:18:08 +00:00
Jason Dove
74733a8026 fix duplicate database migration; fix ssa subtitles (#2216) 2025-07-28 19:23:59 +00:00
Jason Dove
1df9104854 add subtitle selection to playback troubleshooting (#2215) 2025-07-28 18:44:49 +00:00
Jason Dove
6c6ccfa94b fix seeking with text subtitles (#2214) 2025-07-28 16:19:20 +00:00
Jason Dove
e9d494c24e add troubleshoot playback to media card overflow menu (#2210) 2025-07-27 13:06:57 +00:00
Jason Dove
deff33c76c fix pad_to_next always running over time (#2207) 2025-07-26 20:22:40 +00:00
Jason Dove
b5d1839d55 always tell ffmpeg to stop transcoding at duration (#2206) 2025-07-26 19:28:43 +00:00
Jason Dove
ab0f431c85 fix app startup with mysql (#2205)
* don't run pragma command on mysql

* add not required pathhash

* make media file path hash required

* update changelog
2025-07-26 17:48:03 +00:00
Jason Dove
9511e6e6a7 prep for release v25.3.1 [no ci] 2025-07-24 22:36:56 -05:00
Jason Dove
7f2b5ba47f fix fallback filler playback (#2202) 2025-07-25 03:26:56 +00:00
Jason Dove
478d19405d remove docker tag suffixes (#2201) 2025-07-25 03:00:26 +00:00
Jason Dove
e363ab00bb prep for release v25.3.0 [no ci] 2025-07-24 20:43:49 -05:00
Jason Dove
dd9a6d5a06 add chapters to search index (#2199) 2025-07-24 21:03:58 +00:00
Jason Dove
fde05a0299 fix docker tag typo 2025-07-24 15:29:12 -05:00
Jason Dove
d3f8163580 use updated ersatztv-ffmpeg base images (#2198) 2025-07-24 20:27:06 +00:00
Jason Dove
07e4ff907f include docker-arm in unified image health check (#2196)
* include docker-arm in unified image health check

* update
2025-07-24 20:20:00 +00:00
Jason Dove
34874ac548 try to fix docker manifest step 2025-07-24 15:03:47 -05:00
Jason Dove
03e4c0207b use multi-platform docker images (#2195) 2025-07-24 19:56:46 +00:00
Jason Dove
b9faf87887 don't use arm64 runner for arm32 builds (#2194) 2025-07-24 03:21:21 +00:00
Jason Dove
2257d26173 fix some issues with live stream playback (#2193) 2025-07-24 01:58:32 +00:00
Jason Dove
8f6d208e31 use arm64 runners for arm builds (#2192)
* use arm64 runners for arm builds

* use matrix for linux builds on prs

* remove unused "kind"
2025-07-23 21:36:20 +00:00
Jason Dove
5ccea53131 fix media file path length for mysql (#2191) 2025-07-23 03:10:13 +00:00
Jason Dove
da6cb09658 fix tonemapping with amd vaapi (#2187)
* fix amd vaapi tonemap

* fixes
2025-07-22 17:35:06 +00:00
Jason Dove
260949893c fix some stream continuity issues (#2186) 2025-07-22 15:56:14 +00:00
Jason Dove
89b495dc90 qsv and pts fixes (#2184)
* try to fix qsv freezing

* update changelog

* fix unit tests
2025-07-21 19:00:07 +00:00
Jason Dove
74d6b32828 change how qsv is initialized on windows (#2183) 2025-07-21 17:23:30 +00:00
Jason Dove
626af6876b add start from beginning option to playback troubleshooting (#2182) 2025-07-21 16:17:16 +00:00
Jason Dove
2a05cc6e32 add remote stream is_live property (#2181) 2025-07-21 13:19:51 +00:00
Jason Dove
7a4c832156 add media card overflow menu (#2180)
* add media card overflow menu

* remove commented code
2025-07-21 11:00:39 +00:00
Jason Dove
011f16da9f fix variant selection for hls remote streams (#2177) 2025-07-20 18:24:04 +00:00
Jason Dove
79496e688b fix video stream selection for remote streams (#2176) 2025-07-20 17:31:21 +00:00
Jason Dove
5c43ae47b1 add basic remote stream library (#2175)
* initial remote stream library support; scanning seems to work ok

* flood schedule remote streams kind of works

* switch remote stream definitions to yaml files

* implement remote stream script playback

* update changelog
2025-07-20 16:10:32 +00:00
Jason Dove
c29788bc3f add movie nfo country to search index (#2173) 2025-07-19 21:56:13 +00:00
Jason Dove
3501e7c8d5 disable multiple mode select when not using playout mode multiple (#2172) 2025-07-19 20:42:05 +00:00
Jason Dove
867c88d8fc add trakt playlist option (#2171)
* add generate playlist option; add system playlists

* fix official lists; sync items to playlist
2025-07-19 16:56:25 +00:00
Jason Dove
70fbd4c746 add option to auto refresh trakt lists (#2169) 2025-07-19 14:19:07 +00:00
Jason Dove
1cbd48cea0 log nfo file name with nfo parsing errors (#2168) 2025-07-19 02:22:06 +00:00
Jason Dove
c953176cee change watermark width and margins to allow decimals (#2167) 2025-07-18 21:28:32 +00:00
Jason Dove
e0cef62969 fix block playout epg time zone (#2166) 2025-07-18 17:14:11 +00:00
Jason Dove
9e56f6552f support more multi-part grouping names (#2165) 2025-07-18 16:48:08 +00:00
Jason Dove
6a84c564d6 add multi-episode group size (#2164) 2025-07-18 14:46:00 +00:00
Jason Dove
54be3761dd add multiple mode to schedule items (#2163) 2025-07-18 14:03:56 +00:00
Jason Dove
cf6b9cf29a enable write-ahead logging on all sqlite databases (#2162) 2025-07-18 11:17:21 +00:00
Jason Dove
464c1e2ea8 fix bugs with playout mode multiple (#2160) 2025-07-18 01:53:19 +00:00
dependabot[bot]
107e8cfded Bump Jint and System.CommandLine (#2152)
Bumps Jint from 4.3.0 to 4.4.0
Bumps System.CommandLine from 2.0.0-beta5.25306.1 to 2.0.0-beta6.25358.103

---
updated-dependencies:
- dependency-name: Jint
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: System.CommandLine
  dependency-version: 2.0.0-beta6.25358.103
  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>
2025-07-17 18:23:04 +00:00
Jason Dove
837f824660 include hardware info in troubleshooting archive (#2159)
* add cpu and gpu info to troubleshooting general

* include capabilities in troubleshooting archive
2025-07-17 16:23:57 +00:00
Jason Dove
223bdff8d6 playback troubleshooting improvements (#2157) 2025-07-17 14:53:11 +00:00
Jason Dove
578cdb1e14 add playback troubleshooting tool (#2155)
* support media info for more content types

* add playback troubleshooting page

* reorganize playback troubleshooting

* fix watermarks and delay

* update changelog
2025-07-17 03:51:36 +00:00
Jason Dove
848b88bd2d link ffmpeg health check to ersatztv-ffmpeg release (#2154)
* link ffmpeg health check to ersatztv-ffmpeg release

* bump windows ffmpeg to use the same version as linux
2025-07-16 17:13:25 +00:00
Jason Dove
b85571b159 allow uploading large watermarks (#2151) 2025-07-15 13:51:26 +00:00
Jason Dove
43e1cbd919 yaml playout watermarks (#2149) 2025-07-14 03:25:20 +00:00
Jason Dove
39b107eb0f add matched_points to filler expression (#2148) 2025-07-13 15:14:13 +00:00
Jason Dove
0ee62dbc7d fix recent regression to health check links (#2147) 2025-07-13 13:15:16 +00:00
Jason Dove
833bf3506a rework schedule items editor (#2146) 2025-07-13 00:57:27 +00:00
Jason Dove
cd75046348 rework playlist editor (#2145) 2025-07-12 23:17:47 +00:00
Jason Dove
448d29546c rework block editor (#2143) 2025-07-12 19:47:25 +00:00
Jason Dove
f2c49bd0fd rework alternate schedule and playout template editors (#2142)
* rework alternate schedules editor

* rework playout templates editor
2025-07-12 18:51:44 +00:00
Jason Dove
174c743cb7 more mobile layout updates (#2141)
* update trash layout

* cleanup block and yaml playout editors

* spacing cleanup

* rework multi-collection editor

* rework deco template editor

* rework template editor
2025-07-12 15:50:44 +00:00
Jason Dove
2a9f23cce6 update layout for group editors (#2140)
* update block group editor

* update playlist group editor

* update template group editor

* update deco group editor

* update deco template group editor

* update deco editor

* update logs layout

* update changelog
2025-07-12 13:45:50 +00:00
Jason Dove
451c534062 allow block items to disable watermarks (#2139)
* allow block items to disable watermarks

* fix test
2025-07-12 03:55:18 +00:00
Jason Dove
e16cb30ab1 add mid-roll filler expression (#2138) 2025-07-12 01:23:16 +00:00
Jason Dove
e0df454ac6 more layout updates for mobile (#2137)
* update trakt, filler, filler editor ui

* update schedules and playouts

* update playout editor

* update dependencies

* update yaml playout editor

* update path replacement editor
2025-07-11 21:08:37 +00:00
Jason Dove
e79a03b522 fix displaying local tv artwork in ui (#2136) 2025-07-11 14:44:16 +00:00
Jason Dove
1a09bb26d7 lots of mobile updates including detail pages (#2135)
* update artist page layout

* update season page layout

* rework collection view

* cleanup

* update collection editor
2025-07-11 14:21:39 +00:00
Jason Dove
ffd3e3604c rework many media pages (#2134)
* rework many list pages

* refactor

* rework movie details and season list
2025-07-11 02:47:30 +00:00
Jason Dove
7e40a809ff improve search results ui on mobile (#2133)
* show brief health check messages on mobile

* update libraries layout

* improve search results ui on mobile
2025-07-10 19:19:34 +00:00
Jason Dove
cecf18a7b5 improve mobile layout for some media source pages (#2132) 2025-07-10 17:28:51 +00:00
Jason Dove
7df33425fa improve health check display on mobile (#2131) 2025-07-10 15:23:10 +00:00
Jason Dove
5dfaa1a7ad improve mobile layout for some pages with tables (#2130) 2025-07-10 14:38:17 +00:00
Jason Dove
28a65e74bb use new form layout for local library editor (#2129) 2025-07-10 11:48:20 +00:00
Jason Dove
4a66f0ae43 use new form layout for watermark editor (#2127)
* use new form layout for watermark editor

* cleanup
2025-07-09 21:24:03 +00:00
Jason Dove
fb2466d32d vaapi tonemap fixes (#2125) 2025-07-07 20:58:45 +00:00
Jason Dove
beaaa62ed9 fix nvidia edge case with missing bit depth info (#2123)
* fix nvidia edge case with missing bit depth info

* revert docker-compose changes
2025-07-07 16:31:11 +00:00
Jason Dove
0b445f8cfd cache bust more css (#2119) 2025-07-07 02:50:34 +00:00
Jason Dove
7e30444857 dependencies and code cleanup (#2117)
* fix validation in new form layout

* pin mediatr to last oss version

* update dependencies

* cleanup code in core

* cleanup code in ffmpeg

* cleanup code in infra

* cleanup code in scanner

* cleanup code in application

* cleanup main code

* cleanup test code

* solution-wide code cleanup
2025-07-06 15:56:17 +00:00
Jason Dove
fa6a31b4fc use new form layout for schedule editor (#2116) 2025-07-06 11:32:11 +00:00
Jason Dove
b01ad9dbae restore incorrectly deleted file (#2114) 2025-07-05 17:40:00 +00:00
Jason Dove
d324967afa use new form layout for ffmpeg profile editor (#2113) 2025-07-05 13:07:01 +00:00
Jason Dove
aff4fb0deb use new form layout for channel editor (#2112) 2025-07-05 11:20:53 +00:00
Jason Dove
93afcd2f57 more settings updates (#2111)
* update logging settings layout

* update hdhomerun settings layout

* update scanner settings layout

* update playout settings layout

* update xmltv settings layout

* update changelog
2025-07-05 00:45:32 +00:00
Jason Dove
921a108684 ui updates (#2109)
* split settings into multiple pages

* show health check badge in nav menu

* undo transcoding test changes
2025-07-04 18:20:22 +00:00
Jason Dove
a6fa93d44e fix nvidia compatibility with ffmpeg 7.2+ (#2108)
* tweak random seed

* fix dotnet install in docker test

* fix nvidia compatibility with ffmpeg 7.2+
2025-07-03 15:08:18 +00:00
Jason Dove
a42234a7c3 update plex plot during deep scan (#2105) 2025-07-02 17:14:21 +00:00
Jason Dove
7c5137a4af remove some decode threading limits (#2103) 2025-07-02 00:34:07 +00:00
Jason Dove
5a9d27e196 make yaml playout count an expression (#2102) 2025-07-01 16:24:11 +00:00
Jason Dove
cd4a9c1d16 fix hdhr endpoint classification (#2101) 2025-07-01 15:13:16 +00:00
Jason Dove
f6249d9fa4 channel logo and watermark fixes (#2100)
* channel logo and watermark fixes

* update changelog
2025-07-01 13:40:30 +00:00
Jason Dove
e2ffa70529 support episodedetails and musicvideo as top-level other video nfo tags (#2098) 2025-07-01 02:59:18 +00:00
Jason Dove
3e07bc6136 fix history for playlists in yaml playouts (#2097) 2025-07-01 00:47:53 +00:00
Jason Dove
d6bfc2fd05 marathon playout history fixes (#2096) 2025-06-30 21:39:31 +00:00
Jason Dove
35116c64cd fix potential crash with recent marathon updates (#2095) 2025-06-30 19:23:31 +00:00
Jason Dove
037cee873f yaml marathon history (#2094)
* better playlist tests

* fix history for marathon content in yaml playouts
2025-06-30 19:05:09 +00:00
Jason Dove
cd28afcd91 dont reload appsettings.json at runtime (#2093)
* dont reload appsettings.json at runtime

* also disable here
2025-06-29 17:15:34 +00:00
Jason Dove
7457301d3e yaml playout skip items expression (#2092) 2025-06-29 14:31:56 +00:00
Jason Dove
7b7d378df7 run all mac builds on macos-14 (#2091) 2025-06-29 13:35:57 +00:00
Jason Dove
f6dcaf9108 fix qsv audio sync (#2090) 2025-06-29 03:49:04 +00:00
Jason Dove
6cc2f1de17 yaml playout improvements (#2088)
* add stop_before_end

* more yaml playout improvements
2025-06-28 16:27:58 +00:00
Jason Dove
c6ee41484e allow other videos and images to use the same folders (#2087) 2025-06-28 14:50:33 +00:00
Jason Dove
36d38c740f only scan plex networks on plex show libraries (#2086) 2025-06-28 12:56:49 +00:00
Jason Dove
0f795e4e2f add plex network metadata (#2085)
* initial plumbing

* scan for plex networks

* save network contents to db as tags

* eliminate network tag id churn

* add network and show_network to search index

* update last networks scan

* show networks on tv show page

* update changelog
2025-06-28 12:49:26 +00:00
Jason Dove
583cbf7b14 add channel active mode (#2083) 2025-06-27 21:19:26 +00:00
Jason Dove
27c701b936 fix software tonemap with nvidia (#2082) 2025-06-27 20:47:09 +00:00
Jason Dove
6e2c19d354 process missing language as und (#2081) 2025-06-27 14:45:30 +00:00
Jason Dove
4d83dc019c don't return stream selection when subtitles don't match (#2080) 2025-06-27 14:25:07 +00:00
Jason Dove
462057a4b1 prioritize stream selection by language (#2079) 2025-06-27 13:31:50 +00:00
Jason Dove
a04c72788f fix arm64 docker build (#2078) 2025-06-27 11:48:21 +00:00
Jason Dove
f94a440b62 stream selector improvements (#2077)
* add tests for audio blocklist and audio allowlist

* add subtitle allow list and block list

* add subtitle condition

* add audio condition

* cache bust mudblazor css
2025-06-27 11:40:06 +00:00
Jason Dove
f80069bb97 add custom channel stream selector system (#2076)
* add some basic channel stream selector models

* change windows ffmpeg url

* implement basic stream selection

* fixes
2025-06-27 03:00:59 +00:00
Jason Dove
c2769a08b4 stop building hwaccel-specific images (#2075)
* stop building hwaccel images

* update changelog
2025-06-26 17:26:11 +00:00
Jason Dove
e679fee940 update CHANGELOG for clarity [no ci] 2025-06-24 19:11:04 -05:00
Jason Dove
2271d5497b prep for release v25.2.0 [no ci] 2025-06-24 12:03:56 -05:00
Jason Dove
f71b6527c0 allow external channel logo urls (#2067) 2025-06-24 00:43:24 +00:00
Jason Dove
20d2fe71cd update macos app (#2066) 2025-06-23 21:19:42 +00:00
Jason Dove
1994f171d5 update windows launcher to respect ETV_UI_PORT (#2065) 2025-06-23 20:15:03 +00:00
Jason Dove
76f7c88375 add etv env vars to troubleshooting > general info (#2064) 2025-06-23 14:54:06 +00:00
Jason Dove
3805b9e48c update dependencies that require api migrations (#2063)
* migrate system.commandline to 2.0.0-beta5 api

* bump sixlabors.imagesharp

* migrate skiasharp to 3.x api
2025-06-23 13:49:24 +00:00
Jason Dove
9267edbcc9 fix scanning libraries (#2062) 2025-06-23 13:09:14 +00:00
Jason Dove
c4c164df6a detect cycles in yaml sequence definitions (#2060) 2025-06-23 02:55:35 +00:00
Jason Dove
b90463e3af detect cycles in smart collection queries (#2059) 2025-06-23 01:45:17 +00:00
Jason Dove
1fb27e3cfa fix nuget issues with transitive dependencies (#2058)
* try to fix nuget issues

* another attempt at surfacing warnings

* restore proper runtime

* remove old an unneeded dependencies

* upgrade transitive dep

* use newer dotnet in github
2025-06-22 20:22:37 +00:00
Jason Dove
06f233e5bd upgrade to dotnet 9 (#2056) 2025-06-22 19:04:39 +00:00
Jason Dove
9917774671 allow searching by smart collection (#2055) 2025-06-22 15:43:12 +00:00
Jason Dove
5be929da18 add collection (name) to search index (#2054) 2025-06-22 14:51:17 +00:00
Jason Dove
25f4fb22e5 yaml sequence improvements (#2053) 2025-06-21 19:28:41 +00:00
Jason Dove
b04b517f7b add dockerfile to run transcoding test suite (#2052) 2025-06-21 18:08:23 +00:00
Jason Dove
d756c0c7c0 properly filter vaapi driver health check to vaapi ffmpeg profile (#2050) 2025-06-21 11:36:02 +00:00
Jason Dove
20e5b8a11a add button to clone schedule item (#2048) 2025-06-20 21:52:45 +00:00
Jason Dove
5c8489cbed improve vaapi driver health check (#2047)
* improve vaapi driver health check

* fix duplicate check

* cleanup again
2025-06-20 19:16:52 +00:00
Jason Dove
7cfa298c72 fix xmltv grouping with post-roll filler (#2046) 2025-06-20 14:53:04 +00:00
Jason Dove
4b0faf4da1 unify hardware acceleration in docker (#2045) 2025-06-20 02:52:05 +00:00
Jason Dove
07cbf9936b fix shuffle in order and fill with group mode incompatibility (#2044) 2025-06-19 22:10:30 +00:00
Jason Dove
2138d6437c use noautoscale when also using hwaccel cuda (#2043) 2025-06-18 17:06:19 +00:00
Jason Dove
5b9601a57b maintain cuda pixel format throughout nvidia pipeline (#2042) 2025-06-18 02:59:10 +00:00
Jason Dove
aeda5050d3 nvidia decoder fixes (#2041)
* replace FluentAssertions with Shouldly

* fix song transcoding tests

* only specify hwaccel when hardware decode is required

* update changelog
2025-06-17 18:26:49 +00:00
Jason Dove
ea46a7a5ca add tonemap algorithm setting to ffmpeg profile (#2039) 2025-06-15 00:38:37 +00:00
Jason Dove
69a1e718df use ffmpeg 7.1.1 for nvidia docker (#2038) 2025-06-14 23:20:22 +00:00
Jason Dove
4a59dafe51 optimize tonemapping performance (#2037)
* add env var to disable vulkan

* tonemap after scaling

* vulkan tonemapping still needs to happen before scaling
2025-06-14 21:32:38 +00:00
Jason Dove
6b90da8982 add pad_vaapi filter (#2036) 2025-06-14 13:36:24 +00:00
Jason Dove
1184dc565c use ffmpeg 7.1.1 for base, arm, arm64 docker (#2035)
* use ffmpeg 7.1.1 for base, arm, arm64 docker

* keep newline
2025-06-14 11:16:30 +00:00
Jason Dove
d82ccf8fb5 use hardware-accelerated tonemapping with qsv (#2034)
* add tonemap for qsv

* update changelog
2025-06-14 01:19:35 +00:00
Jason Dove
4d83cc705f use ffmpeg 7.1.1 for vaapi docker (#2033) 2025-06-14 00:34:17 +00:00
Jason Dove
f80addacba use the clip algorithm for software tonemapping (#2032) 2025-06-13 20:12:31 +00:00
Jason Dove
18c2a816dc use ffmpeg 7.1.1 on windows (#2031) 2025-06-13 13:36:19 +00:00
Jason Dove
4f085c1950 fix detecting nvidia capabilities on blackwell gpus (#2030) 2025-06-13 03:14:22 +00:00
Jason Dove
dad0662fa6 use libplacebo to tonemap with nvidia/vulkan (#2029) 2025-06-12 20:49:06 +00:00
Jason Dove
dfdfa6f349 use hardware-accelerated tonemapping with vaapi (#2028)
* add tonemap_vaapi filter

* let vaapi pipeline handle hdr content

* use tonemap_opencl with vaapi

* update changelog
2025-06-12 16:13:43 +00:00
Jason Dove
5fe3e97b31 add software tonemap filter to support hdr content (#2027) 2025-06-12 14:47:59 +00:00
Jason Dove
da1cfab5f4 remove jellyfin admin user id requirement (#2025) 2025-06-09 13:33:47 +00:00
Jason Dove
6d152e4b4a use more accurate BANDWIDTH value in multi-variant playlist (#2023) 2025-06-07 20:45:07 +00:00
Jason Dove
2ca722523b improvements to plex connection management (#2020) 2025-05-31 18:44:55 +00:00
Jason Dove
d9a3496bf5 add fixed start time behavior option to schedules and schedule items (#2017) 2025-05-30 09:30:09 +00:00
Jason Dove
c8f5b51d93 use cache busting to avoid ui errors after upgrading mudblazor (#2016) 2025-05-30 00:22:49 +00:00
Jason Dove
956734ce39 globalization fixes (#2014)
* fix crashes caused by decimal separator

* improvements to playout reset ui

* remove code quality workflow
2025-05-26 16:32:25 +00:00
Jason Dove
e44a391f00 fix navigation when using ETV_BASE_URL (#2013) 2025-05-25 15:29:36 +00:00
Jason Dove
b43b66dd35 start to make ui minimally responsive (#2004) 2025-04-28 21:39:48 +00:00
Jason Dove
140a663da4 update dependencies (#2003)
* update dependencies

* more dependency updates
2025-04-28 17:20:45 +00:00
Jason Dove
876d79c11c update changelog [no ci] 2025-04-22 15:34:28 -05:00
Jason Dove
57a4480c3f try to push docker images to ghcr.io (#2000) 2025-04-22 20:24:14 +00:00
Jason Dove
8f1b57eb88 another attempt at fixing separate ports behind reverse proxy (#1994) 2025-04-14 18:53:25 +00:00
Jason Dove
70472ac84e add public port env vars; allow streaming through ui port (#1993) 2025-04-14 18:21:05 +00:00
Jason Dove
6aab8f53b8 fix changelog [no ci] 2025-04-14 11:57:01 -05:00
Jason Dove
b30b458574 allow ui and streaming to run on different ports (#1992)
* allow ui and streaming to run on different ports

* revert global.json change
2025-04-14 16:48:19 +00:00
Jason Dove
6fe6382485 add remote ip and user agent to http request logging (#1990) 2025-04-07 18:57:08 +00:00
Jason Dove
100ae0adda add api endpoint to empty trash (#1988) 2025-04-06 14:26:40 +00:00
Jason Dove
f0d5200843 bump image sharp lib to fix build (#1986) 2025-04-04 02:48:12 +00:00
Jason Dove
af36218cc2 add linux-musl-x64 artifact (#1985) 2025-04-04 02:45:09 +00:00
Jason Dove
eca62e8bec fix error synchronizing collections from plex server that has zero collections (#1964) 2025-01-15 21:42:22 -06:00
Jason Dove
03d4ab8c72 no longer mark releases as pre-release [no ci] 2025-01-10 12:26:53 -06:00
Jason Dove
57b590cd0f prep for release v25.1.0 [no ci] 2025-01-10 12:12:39 -06:00
Jason Dove
f0a5d89f73 fix song progress with 4:3 resolutions (#1961) 2024-12-27 16:09:58 -06:00
Jason Dove
17a77694a0 add tag_full field to search index (#1956) 2024-12-09 13:55:47 -06:00
Jason Dove
838c2a1661 fix default song background for 4:3 resolutions (#1951)
* fix default song background for 4:3 resolutions

* use 16:9 progress overlay for now
2024-11-28 08:56:38 -06:00
Jason Dove
375a306edc fix vaapi capabilities detection when display is not drm (#1949)
* fix vainfo device syntax

* update logging

* don't pass vaapi_device when display is not drm

* fix hw accel

* proper accel fix

* Revert "proper accel fix"

This reverts commit 5dbadb1f79.

* Revert "fix hw accel"

This reverts commit b6b132d7b9.

* Revert "don't pass vaapi_device when display is not drm"

This reverts commit 0459210718.

* still show vaapi device
2024-11-25 14:27:21 -06:00
Jason Dove
759052c725 add vaapi display option to ffmpeg profile (#1948)
* add vaapi display option

* fix vaapi capabilities cache key

* update logging
2024-11-25 13:14:21 -06:00
Jason Dove
dc112f0c7d add default song album art; adjust default blurhashes (#1946)
* fix song progress on white backgrounds

* remove yellow from default song backgrounds

* add default album art for generated song images

* update changelog
2024-11-20 22:27:09 -06:00
Jason Dove
c8ec87b01f add optional progress bar to generated song videos (#1945)
* optionally include progress bar in generated song video

* update progress bar size/location

* move everything up 10% when song progress is enabled

* add watermark border to song progress bar

* always show accurate progress bar

* lower progress bar to 90% alpha

* update changelog
2024-11-20 15:57:00 -06:00
Jason Dove
fbb7a661fb fix guide group generation for duration yaml instruction (#1943) 2024-11-19 06:26:19 -06:00
Jason Dove
37ceac5651 add advance option to epg_group yaml playout instruction (#1942) 2024-11-18 22:07:21 -06:00
Jason Dove
0953e258a5 add rewind_on_reset option wait_until yaml playout instruction (#1941) 2024-11-18 19:31:51 -06:00
Jason Dove
fdbd8a07b6 update macos runner to macos-13; macos-12 is no longer supported (#1940) 2024-11-18 09:59:37 -06:00
Jason Dove
5465c45a51 fix plex collection sync (#1939) 2024-11-18 09:41:52 -06:00
Jason Dove
a0bef3568b fix selecting audio stream with preferred title (#1937)
* update dependencies to fix build errors

* fix selecting audio stream with preferred title
2024-11-16 19:28:30 -06:00
Jason Dove
f75bb25a1a fix building playouts with certain playlist changes (#1935)
* fix building playouts with certain playlist changes

* update changelog
2024-11-13 14:38:07 -06:00
Jason Dove
e4ff825ae8 fix displaying playout item durations that are greater than 24 hours (#1932)
* fix displaying playout item durations that are greater than 24 hours

* upgrade refit to make build happy

* upgrade all dependencies
2024-11-08 10:11:57 -06:00
Jason Dove
6c4f63ad91 fix scaling behavior crop (#1928) 2024-10-26 08:38:44 -05:00
Jason Dove
c0a14ba81c add button to reset all playouts (#1927) 2024-10-24 11:23:47 -05:00
Jason Dove
c063720169 use new channel identifiers in M3U and XMLTV to disambiguate in Plex (#1920) 2024-10-16 09:37:33 -05:00
Jason Dove
5d0f40978d fix deleting local libraries with mysql (#1919) 2024-10-16 08:46:45 -05:00
Jason Dove
063f463951 update dependencies (#1918) 2024-10-16 08:40:03 -05:00
Jason Dove
e7817b1460 fix deco default filler hardware acceleration (#1916)
* fix hw accel for deco default filler

* use custom filler kind for deco default filler
2024-10-11 14:33:30 -05:00
Jason Dove
7d6faee27b properly destroy channel preview (#1910) 2024-10-04 12:29:40 -05:00
Jason Dove
7bba422880 fix emptying trash (#1905) 2024-09-25 10:27:34 -05:00
Jason Dove
db6ae27384 fix mysql database cleaner (#1902) 2024-09-20 08:42:52 -05:00
Jason Dove
4b9fc5004f update changelog for release v0.8.8-beta [no ci] 2024-09-19 11:15:41 -05:00
embolon
f40eaef898 [scheduling] Add a new mode RandomRotation that randomly picks an item from a randomly choosen group (show/artist) for block schedule (#1885)
* init

* minor naming change

* address to comments round 1

* update dependencies

* formatting

* make sure it rotates

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-09-10 13:36:20 -05:00
embolon
91e85cc9c1 [Filler] Add random count for filler preset (#1886)
* init

* minor update

* clean up

* minor cleanup

* update changelog

* update changelog again

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-09-03 10:25:20 -05:00
Jason Dove
2c44efb971 update dependencies (#1891) 2024-09-03 10:09:37 -05:00
Jason Dove
c2b7be66af restart hls session in some cases (#1880) 2024-08-23 12:59:48 -05:00
Jason Dove
8b911332a6 fix watermark opacity for transparent watermarks (#1877) 2024-08-22 11:35:44 -05:00
Jason Dove
4130f7316c fix block playout history loading (#1876) 2024-08-22 09:05:21 -05:00
Jason Dove
3f6eb5a121 fix some collection related bugs with images (#1874) 2024-08-21 10:36:42 -05:00
Jason Dove
1209c54eb9 prevent saving overlapping blocks (#1872) 2024-08-15 10:37:57 -05:00
Jason Dove
94db4bf679 fix local subtitle scans for non-lowercase extensions (#1865)
* fix local subtitle scans for non-lowercase extensions

* remove some unneeded changes
2024-08-07 20:47:27 -05:00
Jason Dove
2977590a14 add deco setting to use watermarks during filler (#1861) 2024-08-05 13:40:24 -05:00
Jason Dove
b4c168e85e use trakt user slug for proper url generation (#1859) 2024-08-05 09:41:24 -05:00
Jason Dove
55b7a35689 fix missing movie metadata (#1854) 2024-08-03 08:05:50 -05:00
Jason Dove
a24592a8c4 add database cleaner (#1853)
* fix broken tests

* add database cleaner
2024-08-02 10:46:52 -05:00
Jason Dove
9b60ff0863 optionally shuffle marathon groups (#1850) 2024-07-31 17:33:08 -05:00
Jason Dove
efdf0bb6d4 group music videos by album (#1849) 2024-07-31 16:13:13 -05:00
Sylvain
39ca27cb3d Overlay Generated Channel Logo when active but no artwork is found (#1848) 2024-07-31 10:04:42 -05:00
Jason Dove
9e2f7b7815 fix deco selection for watermark and filler (#1847) 2024-07-30 21:57:21 -05:00
Jason Dove
101d46e283 dont remove block items that are filler (#1846) 2024-07-30 20:38:13 -05:00
Jason Dove
521e4eac41 add yaml marathon search content source (#1845)
* use search queries to populate marathons

* group marathon by artist

* add marathon group by album
2024-07-30 20:30:13 -05:00
Jason Dove
894fc284b2 fix deco template name display (#1844)
* fix deco template name display

* try to fix mac build
2024-07-30 19:45:25 -05:00
Jason Dove
a8cf22e43e group marathon by season (#1843) 2024-07-30 19:24:56 -05:00
Jason Dove
4c9c047530 add basic marathon content (#1842) 2024-07-30 18:21:24 -05:00
Jason Dove
912f79097d add collection, smart collection, multi collection, playlist content sources to yaml playouts (#1841)
* add collection content to yaml playout

* add smart_collection content

* add multi_collection content

* add playlist content
2024-07-30 10:46:08 -05:00
Jason Dove
8aa55fdfce replace new_epg_group instruction with epg_group; copy sequence custom title to sequence items 2024-07-30 08:36:14 -05:00
Jason Dove
8dc1cab222 fix media card selection (#1840) 2024-07-30 06:21:38 -05:00
Jason Dove
961fe8bbf2 improve shuffling behavior; add custom_title (#1838)
* improve yaml shuffling behavior

* add custom_title to playout instructions
2024-07-29 19:53:06 -05:00
Jason Dove
75f991d670 yaml history fix (#1836) 2024-07-29 16:21:51 -05:00
Jason Dove
e3c981004b show all items in epg by default (#1835) 2024-07-29 15:48:20 -05:00
Jason Dove
befaa037e2 default duration to make a new epg group per item; default duration to NOT use offline tail (#1834) 2024-07-29 15:26:13 -05:00
Jason Dove
5e0fb31069 add reset playout and scan library api endpoints (#1833)
* add api to reset playout

* add library scan api

* update changelog
2024-07-29 13:50:33 -05:00
Jason Dove
7d83e66ba6 add yaml playout history; allow yaml playouts to be extended (#1832)
* add multi_part; refactor skipping items

* save and apply history for yaml playouts

* do not remove history on yaml playout reset
2024-07-29 13:09:14 -05:00
Jason Dove
391528cd94 add pad_until instruction (#1831)
* revert dotnet workaround

* add pad_until instruction
2024-07-29 06:36:14 -05:00
Jason Dove
b737775f9a add yaml skip to item instruction (#1830)
* work around MSB3374 error

* add skip to item instruction
2024-07-28 22:12:54 -05:00
Jason Dove
728c5130b5 try without quotes 2024-07-28 17:34:25 -05:00
Jason Dove
e4253276e0 let's try completely separate folders 2024-07-28 16:49:07 -05:00
Jason Dove
1fc55bc693 try to fix build again 2024-07-28 16:44:38 -05:00
Jason Dove
4ad22e402f use global.json dotnet version in workflows (#1829)
* use global.json dotnet version in workflows

* list output files

* work around weird folder emptying behavior on windows
2024-07-28 16:39:19 -05:00
Jason Dove
ec99d5976d add shuffle sequence instruction (#1827) 2024-07-28 13:51:26 -05:00
Jason Dove
59f11f1a1a add yaml playout sequences (#1826) 2024-07-28 13:22:01 -05:00
Jason Dove
694f25f8b3 upgrade to mudblazor 7 (#1825) 2024-07-28 11:31:04 -05:00
Jason Dove
5947555e86 improve trakt list url validation (#1824)
* improve trakt url validation and logging

* update changelog
2024-07-28 09:24:54 -05:00
Jason Dove
fb63116b36 more subtitles fixes (#1823) 2024-07-27 22:20:44 -05:00
Jason Dove
56a58d7a84 fix missing audio and subtitle language codes (#1822) 2024-07-27 20:44:49 -05:00
Jason Dove
6f66909957 add "all" instruction (#1821)
* add "all" instruction

* use string for value we don't care about
2024-07-27 14:26:33 -05:00
Sylvain
01090f62e6 Fixing URL Encoding for logo generation (#1818) 2024-07-27 10:37:37 -05:00
Jason Dove
e4e4f68eb4 refactor yaml playout builder (#1820)
* update changelog

* refactor some handlers

* refactor skip items instruction

* more refactoring
2024-07-27 10:33:21 -05:00
Sylvain
8488fe5d3d Used a UUID in HDHomeRun config to allow multiple instances on a same network (#1810)
* Used a UUID in HDHomeRun config to allow multiple instances on a same network

* tweak some async calls

* try to fix line endings

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-07-27 08:51:45 -05:00
Jason Dove
f06ef5262a add new_epg_group instruction; add filler_kind propery (#1819) 2024-07-27 08:38:00 -05:00
Jason Dove
ae6bcc4933 add yaml playout skip items instruction (#1816) 2024-07-26 20:37:02 -05:00
Jason Dove
b83fe53ef1 add wait until instruction (#1815) 2024-07-26 19:52:43 -05:00
Jason Dove
d50f2ace07 fix regression selecting subtitle streams for certain languages (#1814) 2024-07-26 17:44:17 -05:00
Sylvain
23684f607a Generating Channel Logo when no logo is provided (#1807)
* Generating Channel Logo when none is provided

* Moved TTF in the cached Resources folder

* Using WebUtility.UrlEncode instead of Raw String Replace

* Fixed mistyping

* Moved Channel Logo Generator to etv.core

* Return 301 to static logo if there is any error during Logo generation

* minor fixes

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-07-26 15:13:00 -05:00
Jason Dove
fa20c5e01e template playout => yaml playout (#1813) 2024-07-26 14:33:55 -05:00
Jason Dove
53bd745678 add playout template show content (#1812) 2024-07-26 13:10:27 -05:00
Jason Dove
f3e5a4e7d8 add playout template duration scheduler (#1811)
* fix loop with missing content

* implement template duration scheduler
2024-07-26 12:03:46 -05:00
Jason Dove
0b29bb32b1 playout template pad to next fixes (#1809)
* prevent loop

* add discard attempts and fallback to pad_to_next
2024-07-26 10:23:38 -05:00
Jason Dove
d9a7615cf6 add experimental playout template system (#1808)
* add template playout kind

* add template scheduler count

* implement pad to next

* only allow resetting template playouts

* update changelog
2024-07-26 08:34:18 -05:00
Jason Dove
50f2cb7a33 fix adding pad filler to short content (#1806) 2024-07-25 19:14:16 -05:00
Jason Dove
b1b2c2a1e0 add deco default filler trim to fit setting (#1800)
* add deco default filler trim to fit setting

* implement trim to fit

* update changelog
2024-07-22 14:14:02 -05:00
Jason Dove
d842cd57f6 fix building block playouts without default filler (#1799) 2024-07-22 09:05:31 -05:00
Jason Dove
4f393d7b06 fix two letter language code stream selection (#1798) 2024-07-22 06:54:08 -05:00
Jason Dove
46f7289db8 add deco default filler (#1797)
* first pass at default filler for block scheduling

* configure default filler in ui

* update changelog
2024-07-19 13:29:42 -05:00
Jason Dove
80ccbbf299 fix duration playout loop (#1796) 2024-07-18 12:18:49 -05:00
Jason Dove
3765894cb7 remove invalid values from filler preset editor (#1793) 2024-07-17 16:51:39 -05:00
Jason Dove
a8b658a5ea add "on demand" channel progress mode (#1790)
* update dependencies

* add channel progress mode

* implement on demand channel progress

* update changelog
2024-07-16 12:21:52 -05:00
Sylvain
0e3c32bd83 Adding more HEAD handling (https://github.com/ErsatzTV/ErsatzTV/pull/1786) (#1787) 2024-07-13 06:28:28 -05:00
Jason Dove
9dd4a85bf9 fix adding items to empty playlists (#1784) 2024-07-11 12:17:41 -05:00
Sylvain
a0a047ba18 Added API Artwork Router (#1776)
* Added API Artwork Router

* Simplifying code
2024-07-08 15:15:14 -05:00
Sylvain
687a4f4f10 Alow HEAD requests on /iptv/channels.m3u (#1779) 2024-07-08 13:10:36 -05:00
Sylvain
b91ab5d898 Fixing Artwork OtherVideo rel (#1774)
* Fixing Artwork OtherVideo rel

* show other video artwork in ui

* don't run code quality on PRs

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-07-05 09:16:09 -05:00
Jason Dove
256042947d use macos-12 runners for github actions (#1773) 2024-07-05 06:39:09 -05:00
Sylvain
85029cbbcd Escaping & on xmltv file (#1772) 2024-07-05 05:56:14 -05:00
Jason Dove
b5d679212d cache bust xmltv images (#1771) 2024-07-03 12:14:40 -05:00
Sylvain
36e86587ef Allow Other Videos Library Type on Plex to be sync (#1766)
* Allow Other Videos Library Type on Plex to be sync

* Migrating database: Creating PlexOtherVideo table

* Using Plex Media path to create tags for OtherVideos

* missed these in the merge

* Getting PlexLibrary for Tag set on OtherVideo

* fix migrations

* set tag metadata on plex other videos

* update changelog

---------

Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com>
2024-07-02 15:41:09 -05:00
Jason Dove
f41fa669be update media server scanning and paging (#1770)
* update media server scanning and paging

* remove unused types
2024-07-02 13:12:09 -05:00
Jason Dove
f62e841af4 update changelog for release v0.8.7-beta [no ci] 2024-06-26 22:11:36 -05:00
Jason Dove
109960c457 fix some more search queries (#1763) 2024-06-26 22:08:59 -05:00
Jason Dove
6858103be5 fix transcoding 10-bit source content using qsv on windows (#1762) 2024-06-26 16:21:42 -05:00
Jason Dove
f7f1a0493b update macos submodule again 2024-06-25 10:43:31 -05:00
Jason Dove
6c18648fd7 update macos submodule (#1760) 2024-06-25 10:31:37 -05:00
Jason Dove
ab3afcfad0 use ffmpeg 6.1 everywhere instead of ffmpeg 7 (#1754) 2024-06-21 12:30:07 -05:00
Jason Dove
5734356d29 fix template and deco template editors with mysql (#1750) 2024-06-17 15:36:14 -05:00
Jason Dove
beee038be3 fix video_bit_depth search field for plex media (#1745) 2024-06-10 15:46:23 -05:00
Jason Dove
a5c8e2b401 fix duration scheduling bug (#1744) 2024-06-10 12:30:56 -05:00
Jason Dove
c8113bdf25 fix vaapi decoder capability check (#1740)
* fix decoder video profile check

* clarify changelog
2024-06-04 06:10:02 -05:00
Jason Dove
9466cf7626 update vaapi docker bundled ffmpeg (#1729) 2024-05-25 12:13:05 -05:00
Jason Dove
43fcf9e63a upgrade to ffmpeg 7 on windows (#1727) 2024-05-24 11:31:28 -05:00
Jason Dove
242a7ae382 add b-frames option to ffmpeg profile (#1726)
* add b-frames option to ffmpeg profile

* update dependencies

* cleanup
2024-05-24 08:50:11 -05:00
Jason Dove
6dc526a817 fix dotnet in vaapi docker (#1724) 2024-05-23 07:38:01 -05:00
Jason Dove
182c02b865 use ffmpeg 7 with vaapi again (#1723) 2024-05-22 20:09:38 -05:00
Jason Dove
7c1f0d6dbd treat mid-roll as post-roll when chapter markers are missing (#1722) 2024-05-21 15:51:35 -05:00
Jason Dove
2c97c49763 custom config folders env vars (#1714)
* use custom config folder location

* allow customizing config and transcode folder locations
2024-05-10 09:28:58 -05:00
Jason Dove
f5038c2b66 play all items in playlist before starting again (#1711) 2024-05-07 17:05:12 -05:00
Jason Dove
85eb7623da fix playlist playouts (#1709)
* fix playlist playouts

* remove some logging
2024-05-06 20:40:10 -05:00
Jason Dove
a87ec2d75d cleanup (#1708)
* fix blazor naming

* code cleanup

* update dependencies
2024-05-06 17:00:52 -05:00
Jason Dove
ef6c8b0816 allow clearing (omitting) video profile setting (#1706) 2024-05-05 19:23:26 -05:00
Jason Dove
dc3578660f fix flood playout item watermark (#1705) 2024-05-05 09:08:08 -05:00
Jason Dove
ce958bb7bb enable video profile editing for qsv (#1704) 2024-05-05 08:53:34 -05:00
Jason Dove
33a8b29a27 fix block playout random seeds (#1703) 2024-05-04 09:09:04 -05:00
Jason Dove
48a504735c fix xmltv generation when no block items are selected for guide (#1702) 2024-05-03 09:18:06 -05:00
Jason Dove
0b64e97df6 fix media card (#1701) 2024-05-02 16:38:20 -05:00
Jason Dove
5d89f5d0a4 fix template editors (#1700) 2024-05-02 12:59:13 -05:00
Jason Dove
50a6ed4cea fix docker again 2024-05-02 10:31:01 -05:00
Jason Dove
66b8c8aa0e fix docker builds (#1699) 2024-05-02 10:23:07 -05:00
Jason Dove
398a3c041a detect /dev/dri/card* devices (#1698)
* remove unused nupkg

* cleanup obsolete properties

* detect /dev/dri/card devices
2024-05-02 10:17:38 -05:00
Jason Dove
6ce7265427 add qodana workflow (#1697) 2024-05-02 08:34:17 -05:00
Jason Dove
b4fe38d4ae segmenter v2 fixes (#1696) 2024-05-01 12:53:56 -05:00
Jason Dove
e19b639527 add playlist item play all option (#1694) 2024-04-28 13:00:22 -05:00
Jason Dove
a6d5df3ca6 more add to playlist buttons; fix playlist preview (#1693) 2024-04-28 08:59:14 -05:00
Jason Dove
202ae33e37 add individual media items to playlists (#1692)
* add movies to playlists

* add search results to playlist

* update changelog
2024-04-28 06:33:57 -05:00
Jason Dove
c46b3787d8 set guide mode filler as appropriate on playlist items (#1691) 2024-04-27 20:31:03 -05:00
Jason Dove
706a2d14a2 add playlists (#1690)
* update dependencies

* add playlists

* add playlist support to schedules

* playout builder (flood) supports playlists

* update changelog
2024-04-27 19:58:40 -05:00
Jason Dove
11f99216a3 fix plex library scans with mysql/mariadb (#1689) 2024-04-24 13:16:53 -05:00
Jason Dove
b9a7ad2f5a add video presets for nvenc, qsv, software h264 and hevc encoders (#1688)
* add video preset to ffmpeg profile

* add some hevc video presets
2024-04-23 14:26:30 -05:00
Jason Dove
07e7e5fe66 fix block playout playback when no deco is configured (#1687) 2024-04-23 13:10:11 -05:00
Jason Dove
4a19d046e4 fix mysql db migrations 2024-04-22 22:40:53 -05:00
Jason Dove
c1d6ddcc57 add h264 profile option to ffmpeg profile (#1686)
* add video profile for nvenc/software h264 encoders

* add h264 profile for all other encoders

* update changelog
2024-04-22 22:03:18 -05:00
Jason Dove
35eb200aee more segmenter v2 improvements (#1685)
* more segmenter v2 improvements

* changelog updates
2024-04-21 19:17:06 -05:00
Jason Dove
19af303d76 improve segmenter v2 hevc compliance with fmp4 (#1684) 2024-04-21 10:01:11 -05:00
Jason Dove
da20393a39 allow m3u8 playlist head requests (#1683) 2024-04-21 08:18:05 -05:00
Jason Dove
d78110f6f1 fix macos config folder regression (#1682)
* migrate macos config folder on startup, if needed

* add macos config folder health check

* update macos fix; update changelog
2024-04-17 09:18:09 -05:00
Jason Dove
c1bedb661c rever vaapi docker to ffmpeg 6.1 (#1681) 2024-04-16 22:27:26 -05:00
Jason Dove
d31d6f20cf upgrade to ffmpeg 7 in all docker images (#1680)
* upgrade nvidia docker to ffmpeg 7

* upgrade all docker images to ffmpeg 7

* update changelog
2024-04-16 18:24:40 -05:00
Jason Dove
7469559bb3 fix external subtitle detection (#1679) 2024-04-15 12:31:06 -05:00
Jason Dove
af5dc0968b add xmltv days to build setting (#1678) 2024-04-12 09:44:08 -05:00
Jason Dove
6e7880386b update plex show title (#1677)
* update plex show title when changed in plex

* fix test build
2024-04-12 09:21:11 -05:00
Jason Dove
8945a87f9b fix segmenter semaphore (#1676)
* disable dead air watermarks by default

* fix session worker semaphore

* update changelog
2024-04-11 19:38:57 -05:00
Jason Dove
e305222141 add dead air fallback to deco system (#1668)
* add dead air fallback to deco editor

* fix deco watermark logic

* use dead air fallback from decos
2024-04-05 23:11:14 -05:00
Jason Dove
2e2523c380 another wildcard search fix (#1667) 2024-04-05 09:12:46 -05:00
Jason Dove
b461631be9 properly update plex part keys (#1666) 2024-04-04 22:40:21 -05:00
Jason Dove
19fc068a73 add deco system (#1665)
* add deco groups and decos; set default deco for block playout

* use block playout default deco for watermark

* add deco templates, groups and deco template editor

* associate deco template with playout template

* use deco template item watermark for playback

* update changelog for decos
2024-04-04 14:38:56 -05:00
Jason Dove
e5f15df196 rework active date range; add tests (#1664) 2024-04-03 13:43:20 -05:00
Jason Dove
6ac2384cbc add active date range to block playout templates editor (#1663)
* update dependencies

* add active date range to block playout templates editor
2024-04-03 10:50:43 -05:00
Jason Dove
cd6673da82 update changelog for v0.8.6-beta [no ci] 2024-04-03 05:04:35 -05:00
Jason Dove
8113827802 allow block durations with 5-minute increments (#1662) 2024-04-02 10:27:27 -05:00
Jason Dove
4e56117e0e use jwt for mpeg-ts streaming mode (#1661) 2024-03-29 21:36:49 -05:00
Jason Dove
7702999b9a properly restore all local library items from trash during scans (#1660) 2024-03-29 20:03:19 -05:00
Jason Dove
14a707a4e2 improve plex item change detection (#1659) 2024-03-29 15:15:42 -05:00
Jason Dove
340ab61a26 add more logging to other video library scans (#1658) 2024-03-29 14:02:12 -05:00
Jason Dove
d91d991251 always interpret asterisk as wildcard search (#1657) 2024-03-29 13:27:18 -05:00
Jason Dove
3ce341eee5 fix build error for single file artifacts (#1656) 2024-03-29 12:21:58 -05:00
Jason Dove
476fe991b6 other video library scanner improvements (#1655)
* improve scanner detection when developing on macos

* support ogv files in local libraries

* improve health check wording

* try to properly restore other videos that are no longer missing
2024-03-29 12:00:46 -05:00
Jason Dove
39df3504fc content rating shouldn't ever be a phrase (#1653) 2024-03-22 09:43:51 -05:00
Jason Dove
60bb369d0c add search query parser (#1652)
* add search query parser

* add some search tests; use consistent analyzers for indexing and searching
2024-03-22 09:41:08 -05:00
Jason Dove
aae704f3a5 fix searching id fields (#1651) 2024-03-21 19:33:50 -05:00
Jason Dove
a45583d77a fix type and tag searches (#1649) 2024-03-20 22:52:16 -05:00
Jason Dove
923b36604c fix many search queries (#1648) 2024-03-20 22:29:51 -05:00
Jason Dove
b21d16b0f1 add show_content_rating to search index (#1647) 2024-03-20 19:46:19 -05:00
Jason Dove
a5aaceeee5 fix path replacement logic with inconsistent casing (#1646) 2024-03-19 08:50:00 -05:00
Jason Dove
e52d45fcf8 fix mysql migration (#1645)
* update dependencies; resync mysql db

* manually fix mysql migration
2024-03-18 11:09:27 -05:00
Jason Dove
21d39bc26f fix multi collection editor id collision (#1644) 2024-03-15 09:48:01 -05:00
Jason Dove
233a1c228a fix block creation (#1643)
* fix block creation

* update dependencies
2024-03-13 08:32:58 -05:00
Jason Dove
56988be57b properly isolate library folders between library paths (#1642) 2024-03-10 20:06:37 -05:00
Jason Dove
aded03d962 fix deleting local libraries and local library paths (#1640) 2024-03-09 12:12:22 -06:00
Jason Dove
2119c88c97 always downsample after loudnorm (#1639) 2024-03-07 09:15:02 -06:00
Jason Dove
a5d83a970a use mkv container for 8-bit segmenter v2 content (#1637) 2024-03-06 12:42:48 -06:00
Jason Dove
986785d863 always use nv12 for 8 bit vaapi encoding (#1636) 2024-03-06 12:03:14 -06:00
Jason Dove
087901d177 adjust block unique constraint (#1634)
* upgrade dependencies

* allow blocks with same name in different groups

* code cleanup
2024-03-05 10:39:06 -06:00
Jason Dove
70c4036dc9 fix ten bit source and destination with segmenter v2 (#1633)
* fix software and vaapi 10 bit content

* fix nvidia 10 bit content

* some qsv improvements
2024-03-04 15:29:45 -06:00
Jason Dove
955add1efd fix av1 software decoder priority (#1632) 2024-03-03 22:29:25 -06:00
Jason Dove
99cd01f73b update iptv routing (#1631) 2024-03-02 22:16:46 -06:00
Jason Dove
ef29e8c5a1 fix vaapi transcoding with a53 cc data (#1625) 2024-02-23 11:27:51 -06:00
Jason Dove
3b4c993530 add xmltv time zone option (#1624) 2024-02-21 12:46:09 -06:00
Jason Dove
bcc58bd668 allow segmenter v2 playback on intel vaapi/qsv (#1623) 2024-02-20 15:45:59 -06:00
Jason Dove
6957a76156 fix concat segmenter for nvidia and qsv (#1622) 2024-02-19 15:29:19 -06:00
Jason Dove
4bafc316cc fix playlist for segmenter v2 (#1621) 2024-02-19 14:11:06 -06:00
Jason Dove
35817f09ac add hls segmenter v2 streaming mode (#1620)
* concat segmenter process kind of works

* segmenter v2 improvements

* rework to allow hw accel in concat segmenter

* remove shortest; use different audio alignment filter

* hls v2 improvements

* fix tests

* update changelog
2024-02-19 13:59:33 -06:00
Jason Dove
f4520a5520 allow decimal image duration (#1619)
* add missing mysql migration

* allow decimal image duration
2024-02-16 20:01:40 -06:00
Jason Dove
3a906816fc allow playback of items with duration of 1 second (#1618) 2024-02-16 13:41:52 -06:00
Jason Dove
707292c50f add configurable image duration (#1617) 2024-02-16 13:05:28 -06:00
Jason Dove
71e9ea867a store local library folder hierarchy in db (#1616) 2024-02-16 06:16:19 -06:00
Jason Dove
c490832f66 fix episode artwork in channel guide (#1613) 2024-02-14 20:21:46 -06:00
Jason Dove
e00568cc23 fix build error (#1612) 2024-02-13 10:51:17 -06:00
Jason Dove
356e0f101a fix edge case where channel would fail to start (#1611) 2024-02-13 10:01:02 -06:00
Jason Dove
1f6e843a26 fix segmenter timestamp continuity (#1610) 2024-02-12 08:50:18 -06:00
Jason Dove
9587692486 optimize image playback (#1609) 2024-02-11 12:54:18 -06:00
Jason Dove
f8c4f44216 add basic image library support (#1608)
* add basic image library support

* add image page

* update changelog
2024-02-11 11:24:19 -06:00
Jason Dove
d55ba235bf evenly divide epg time for schedule blocks (#1607)
* add checkbox to include block items in program guide

* evenly divide epg time for schedule blocks
2024-02-10 20:59:29 -06:00
Jason Dove
60b479e330 scanning fixes (#1606)
* fix music video fallback metadata

* properly re-scan song metadata
2024-02-10 06:44:27 -06:00
Jason Dove
b866d07911 fix song libraries using string collections (#1605) 2024-02-09 13:38:54 -06:00
Jason Dove
93db79f8c4 load song comment (#1604) 2024-02-09 11:37:55 -06:00
Jason Dove
a15854d0ad more guide templates; new song metadata library (#1603)
* refactor template processing

* use template for song xmltv entries

* use template for other video xmltv entries

* update changelog
2024-02-09 11:27:44 -06:00
Jason Dove
c743d07425 include external subtitles in search index (#1602) 2024-02-07 13:28:43 -06:00
Jason Dove
8c3b8e81ca separate request logging into its own category (#1601) 2024-02-07 08:40:31 -06:00
Jason Dove
49050a57d2 load music video artists for channel guide template (#1600) 2024-02-06 10:46:34 -06:00
Jason Dove
49c53c5c96 add stale issue workflow (#1599) 2024-02-05 22:00:57 -06:00
Jason Dove
1510c56e69 generate music video xmltv fragment from template (#1598)
* generate music video xmltv fragment from template

* load all music video data
2024-02-05 19:56:19 -06:00
Jason Dove
3ec610d65f properly encode xmltv fragments (#1597) 2024-02-05 18:15:38 -06:00
Jason Dove
69f9b6f137 add channel guide templates (#1596)
* generate channels xmltv fragment from template

* generate movie xmltv fragment from template

* generate episode xmltv fragment from template

* add channel guide template changelog
2024-02-05 14:55:08 -06:00
Jason Dove
08837bda80 properly categorize some existing streaming debug logs (#1595)
* properly categorize some existing streaming debug logs

* cleanup
2024-02-05 06:37:48 -06:00
Jason Dove
9089e2ee04 add iptv request logging (#1594) 2024-02-04 21:34:41 -06:00
Jason Dove
abed22b560 prevent unnecessary search index updates (#1592)
* add sub_language and sub_language_tag fields to search index

* prevent unnecessary search index updates

* update changelog

* update dependencies
2024-02-02 06:33:06 -06:00
Jason Dove
e0f9ab4b88 batch search index updates (#1591) 2024-02-01 21:30:33 -06:00
Jason Dove
231a214223 add new subtitle settings (#1590) 2024-02-01 11:45:22 -06:00
Jason Dove
82bfa8019e tweak subtitle display in media info (#1589) 2024-01-31 19:17:04 -06:00
Jason Dove
d9bbe4df1b auto generate jwt token for channel preview (#1588) 2024-01-31 15:08:23 -06:00
Jason Dove
e0aa44d41b fix updating jellyfin movies (#1587) 2024-01-31 13:38:35 -06:00
Jason Dove
3d99c2593d allow previewing jwt channels (#1586) 2024-01-31 12:03:04 -06:00
Jason Dove
d6dfc1edaa persist data protection keys in etv config folder (#1585) 2024-01-30 19:41:39 -06:00
Jason Dove
7d5cd229d4 add show_studio search field (#1584) 2024-01-30 16:50:57 -06:00
Jason Dove
cd0219c5c3 update changelog for release v0.8.5-beta [no ci] 2024-01-30 15:34:28 -06:00
Jason Dove
4cf8b83de4 ignore subtitles when they are unavailable (#1583) 2024-01-30 14:29:13 -06:00
Jason Dove
6923b25177 add more log level switches (#1582)
* label block and json playouts as experimental

* add more log level switches
2024-01-30 13:10:19 -06:00
Jason Dove
5dce905b8e clear block playout items without clearing history (#1581) 2024-01-30 09:10:49 -06:00
Jason Dove
46c26b5ea7 include all block playout items in xmltv (#1580)
* include all block playout items in xmltv

* double check whether channel preview will work
2024-01-30 06:44:19 -06:00
Jason Dove
7fffc8cf63 channel preview player (#1579)
* add channel preview

* add button to stop transcoding session
2024-01-29 20:52:52 -06:00
Jason Dove
18deff0b83 add session api endpoints (#1578) 2024-01-29 11:31:16 -06:00
Jason Dove
16007a888e fix actions and changelog (#1576) 2024-01-27 10:14:53 -06:00
Jason Dove
7eb1227ba4 fix action version (#1575) 2024-01-26 06:17:55 -06:00
Jason Dove
1d1d5bf9bc use software overlay for intermittent watermark on nvidia (#1574)
* use software overlay for intermittent watermark on nvidia

* update some github action versions

* update changelog
2024-01-26 06:16:00 -06:00
Jason Dove
45c04366c9 remove dynaudnorm filter (#1573) 2024-01-25 19:56:14 -06:00
Jason Dove
60b3bc92f4 use super shuffle in block playouts (#1572) 2024-01-24 15:29:19 -06:00
Jason Dove
12234c3e21 allow shuffling block items (#1571)
* allow shuffling block items

* fix drop down search results
2024-01-23 22:42:28 -06:00
Jason Dove
d37ce2d38a update xmltv channel list on channel edit (#1570) 2024-01-23 13:10:52 -06:00
Jason Dove
6f49233864 fix image upload corruption (#1569) 2024-01-23 10:49:19 -06:00
Will
a67a6047c1 Update README.md (#1567)
Remove the link for Hardware-accelerated transcoding which was just linking back to itself
2024-01-22 14:33:16 -06:00
Jason Dove
33f67b88f0 show chapter markers in media info (#1568) 2024-01-22 14:19:35 -06:00
Jason Dove
b88deaafe5 add tests, replace playout items when collections are updated (#1566) 2024-01-22 10:10:22 -06:00
Jason Dove
83fc3081d8 add some logging around playlist trimming (#1565) 2024-01-22 05:47:00 -06:00
Jason Dove
15d4b0f82b remove v2 ui and node (#1564) 2024-01-16 13:28:46 -06:00
Jason Dove
88fac0de04 add option to stop scheduling before or after block duration end (#1563) 2024-01-16 12:53:56 -06:00
Jason Dove
4805d0d40f add button to copy block item (#1562) 2024-01-16 10:30:50 -06:00
Jason Dove
ef3b941a39 fix mysql migration (#1561)
* clean up block playout preview logic

* fix some bugs with playout templates editor

* fix mysql migration
2024-01-16 05:40:29 -06:00
Jason Dove
a59f71039c preview block playout in block editor (#1558)
* block editor cleanup

* preview block playout

* cleanup
2024-01-15 19:39:18 -06:00
Jason Dove
1ad42fffb1 fix mac builds (#1557) 2024-01-15 10:29:07 -06:00
Jason Dove
2ce8db9e01 basic block duration enforcement (#1556) 2024-01-15 06:26:14 -06:00
Jason Dove
c409fd8b47 fix playout build hang for block playouts (#1555) 2024-01-14 19:40:29 -06:00
Jason Dove
907b8074f1 allow more collection types and playback orders in blocks (#1554) 2024-01-14 12:51:45 -06:00
Jason Dove
adbd0bcec0 block schedule refactor (#1553)
* erase block playout history and items from playouts page

* remove block from template

* refactor block scheduling; improve history behavior
2024-01-14 10:22:04 -06:00
Jason Dove
2c4379886a limit blocks to television shows and seasons (#1551) 2024-01-14 06:46:38 -06:00
Jason Dove
caef4a139e block scheduling skip unchanged blocks (#1550)
* schedule blocks in order

* block minutes must be multiple of 15

* improve block minutes entry, validation and display

* confirm deleting blocks and block groups

* confirm deleting templates and template groups

* skip unchanged blocks in playout
2024-01-14 06:16:53 -06:00
Jason Dove
dcbe4837bf first pass at block scheduling (#1548)
* add blocks, block groups

* basic block and block item editing

* add template groups and basic template editing (name)

* add blocks to template calendar

* edit playout templates

* add calendar preview to playout templates

* add basic block playout building

* add mysql migration

* update changelog
2024-01-13 22:01:21 -06:00
Jason Dove
5e530b9301 fix scale behavior crop with qsv (#1546) 2024-01-12 13:21:49 -06:00
Jason Dove
2a28bf68bf fix crop mode with nvidia accel (#1545) 2024-01-11 11:42:26 -06:00
Jason Dove
f39eac97c0 fix fill with group when show is also included individually (#1544) 2024-01-11 10:44:50 -06:00
Jason Dove
9fd6589831 disambiguate seasons (#1543) 2024-01-11 09:08:52 -06:00
Jason Dove
e2a516f5e8 fix external json playouts with mysql (#1542) 2024-01-09 05:26:57 -06:00
Jason Dove
64502315a3 generate xmltv for external json playouts (#1541) 2024-01-08 20:54:40 -06:00
Jason Dove
56bc58fce9 reorganize to fix build (#1540) 2024-01-08 20:00:35 -06:00
Jason Dove
0330b9326d add external json playout type for dizquetv interop (#1539)
* add external json playout

* basic local playback works

* fallback to streaming from plex

* update external json file

* update changelog
2024-01-08 19:45:43 -06:00
Jason Dove
6708d6b4d7 support filling with groups of song artists (#1537) 2024-01-05 10:32:04 -06:00
Jason Dove
c18be5559b fix delete old segments (#1536)
* code cleanup

* ignore errors deleting old hls segments
2024-01-04 10:42:04 -06:00
Jason Dove
18ed20e203 fix multiple zero when using fill with group (#1535) 2024-01-02 15:29:50 -06:00
Jason Dove
965c7d0eac update changelog [no ci] 2024-01-02 10:34:57 -06:00
Jason Dove
545bf1b775 fill with group (#1534)
* use browser's accept-language header

* add fill with group mode to schedule items

* update dependencies

* fixes

* fix tests
2024-01-02 10:18:49 -06:00
Jason Dove
bb299d4ee7 maybe these don't need npm (#1533) 2023-12-30 20:13:11 -06:00
Jason Dove
0e6c7d2bc3 fix npm in docker builds (#1532) 2023-12-30 20:03:29 -06:00
Jason Dove
576f0cd7e7 more dotnet 8 fixes (#1530) 2023-12-30 13:47:45 -06:00
Jason Dove
9471cb55dd upgrade from dotnet 7 to dotnet 8 (#1529)
* upgrade sdk

* fix warnings in ersatztv.ffmpeg

* fix warnings in ersatztv.core

* fix warnings in ersatztv.infrastructure

* fix warnings in ersatztv.application

* disable analysis for migrations projects

* fix warnings in ersatztv.scanner

* fix warnings in ersatztv

* upgrade project framework

* update github actions and dockerfiles
2023-12-30 13:29:57 -06:00
Jason Dove
3a84af1626 update dependencies (#1527) 2023-12-27 04:44:30 -06:00
Jason Dove
3d3bb64844 fix path replacements page with mysql (#1521) 2023-12-11 17:54:15 -06:00
Jason Dove
8fc1f36638 use explorer to open logs folder on windows (#1520) 2023-12-05 18:28:10 -06:00
Jason Dove
1823a5bae5 update changelog for release v0.8.4-beta [no ci] 2023-12-02 16:45:33 -06:00
Jason Dove
fc871e6f74 fix detection of amf hw accel on windows (#1519) 2023-12-02 09:05:02 -06:00
Jason Dove
24780cbe84 fix disappearing collection tags (#1517) 2023-11-30 20:31:37 -06:00
Jason Dove
c6ed258021 validate filler mode pad settings (#1516) 2023-11-26 12:54:06 -06:00
Jason Dove
7586647b73 fix ffmpeg version health check on windows (#1515) 2023-11-23 06:05:49 -06:00
Jason Dove
d91e945124 update changelog for release v0.8.3-beta [no ci] 2023-11-22 11:36:31 -06:00
Jason Dove
9dabffbac1 support more formats for show fallback metadata (#1514) 2023-11-21 15:52:25 -06:00
Jason Dove
d310b5c09d fix nvidia hardware decoding on windows (#1513) 2023-11-21 06:36:05 -06:00
Jason Dove
ba48b3a676 update dependencies (#1512) 2023-11-20 21:57:43 -06:00
Jason Dove
d8a51b5d6d fix season display bug (#1511) 2023-11-20 21:17:11 -06:00
Jason Dove
97674cff89 fix bug scheduling duration filler (#1510) 2023-11-20 21:02:26 -06:00
Jason Dove
4820615308 proper fix to the sdk mismatch (#1509) 2023-11-16 13:37:20 -06:00
Jason Dove
1ddf27ce88 pin dotnet sdk version used in github actions (#1508) 2023-11-16 13:21:51 -06:00
Jason Dove
cd98a89acd enable docker arm builds again (#1507) 2023-11-16 13:07:49 -06:00
Jason Dove
a2a6afc3e3 temp disable arm docker builds (#1506) 2023-11-16 09:58:46 -06:00
Jason Dove
dfaba8c7b0 use release version of ffmpeg 6.1 (#1505) 2023-11-16 09:57:13 -06:00
Jason Dove
5d11a6b46f use separate model for plex collection scanning since the api types are inconsistent (#1504) 2023-11-16 06:43:48 -06:00
Jason Dove
b95a89b11f plex collection rework (#1503)
* start to rework plex collection scanning

* sync plex collections to db

* sync plex collection items

* update changelog
2023-11-14 10:41:21 -06:00
Jason Dove
948b3735bd fix file not found music videos (#1502)
* fix indexing music videos in file not found state

* update dependencies
2023-11-14 05:50:51 -06:00
Jason Dove
5ecf271773 fix jellyfin library scan (#1501)
* update dependencies

* fix jellyfin library scan
2023-11-10 06:26:23 -06:00
Jason Dove
b287c0d6ec add jellyfin season number fallback (#1497) 2023-11-06 09:37:12 -06:00
Jason Dove
b667659c05 use notarytool directly instead of gon (#1493) 2023-11-05 07:46:15 -06:00
Jason Dove
22d3025e8e include noto cjk fonts in docker (#1492) 2023-11-05 06:15:57 -06:00
Jason Dove
8f5b181372 mysql media server library scan fixes (#1491)
* fix some mysql movie library updates

* fix some mysql show library updates

* update dependencies
2023-10-30 06:45:00 -05:00
Jason Dove
f5060522aa windows nvidia h264 workaround (#1487)
* work around bad h264_cuvid behavior on windows with ffmpeg snapshot

* use latest ffmpeg build on windows

* nvdec => cuda
2023-10-16 11:40:12 -05:00
Jason Dove
14a88bd225 optimize ffmpeg capability cache (#1486)
* minimize cached ffmpeg capabilities

* use set intersect

* try disabling work ahead on nvidia/windows
2023-10-16 08:42:26 -05:00
Jason Dove
0550c60a78 allow older ffmpeg for testing (#1485)
* allow older ffmpeg for testing

* use proper option name
2023-10-14 21:13:18 -05:00
Jason Dove
d3bdcf9bc4 allow plex personal media show libraries (#1483) 2023-10-13 13:33:10 -05:00
Jason Dove
714f68a887 add language_tag and seconds fields to search index (#1479)
* add `language_tag` and `seconds` fields to search index

* simplify
2023-10-10 20:36:50 -05:00
Jason Dove
17bed524f2 fix ui display of multiple languages (#1474) 2023-10-08 18:21:48 -05:00
Jason Dove
c3fe263978 validate hardware accel, use hw accel for error messages (#1471)
* only display supported hw accels in ffmpeg profile editor

* qsv capability improvements

* qsv fixes

* update changelog
2023-10-08 11:21:04 -05:00
Jason Dove
5291832e6c fix clipboard and logs (#1466)
* fix copy to clipboard in some cases

* improve subtitle language selection logging

* log playout item details
2023-10-06 19:36:42 -05:00
Jason Dove
b39dd693f0 update dependencies and windows ffmpeg (#1462)
* update dependencies

* update windows ffmpeg version
2023-10-05 19:14:06 -05:00
Jason Dove
46bf9ef990 fix intel vaapi pgs subtitle pixel format (#1455) 2023-09-30 13:10:23 -05:00
Jason Dove
bc845b1327 schedule filler using ticks instead of milliseconds (#1454)
* add script to set db provider

* don't extract embedded subtitles with DEBUG_NO_SYNC

* fix playout filler precision bug
2023-09-30 06:41:15 -05:00
Jason Dove
3ab8e5bc3a optimize jellyfin collection scanning (#1453) 2023-09-29 09:47:57 -05:00
Jason Dove
e8bc051f73 transcoding improvements (#1452)
* use noautoscale with vaapi encoder

* only use one input file for vaapi with radeonsi driver

* fix vaapi 8-bit to 10-bit

* fix nvidia subtitle scaling

* optimize nvidia subtitle scaling

* fix test pgs subtitle
2023-09-29 06:29:59 -05:00
Jason Dove
b008fcfd85 fix scheduling precision error (#1451)
* fix scheduling precision error

* update dependencies
2023-09-27 06:07:48 -05:00
Jason Dove
547db5fb51 add kodiprop to channels.m3u (#1448) 2023-09-26 15:47:55 -05:00
Jason Dove
58fae1b0cc add crop scaling behavior (#1443)
* add scaling behavior - crop

* fix ffmpeg version check on windows (snapshot)

* update dependencies
2023-09-22 08:23:49 -05:00
Jason Dove
694b6bbd91 scaling behavior and normalize loudness (#1439)
* update changelog [no ci]

* add ffmpeg profile scaling behavior

* update dependencies

* add normalize loudness mode

* update changelog
2023-09-21 02:46:43 -05:00
Jason Dove
e0f8b7d7ae use ffmpeg 6.1 snapshot for windows (#1435) 2023-09-14 19:33:40 -05:00
Jason Dove
b16215fcd6 improve hls throttle (#1434)
* throttle using ffmpeg option

* update ffmpeg version
2023-09-14 19:28:15 -05:00
1846 changed files with 1080294 additions and 42315 deletions

View File

@@ -3,10 +3,11 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2023.2.0",
"version": "2025.1.4",
"commands": [
"jb"
]
],
"rollForward": false
}
}
}

View File

@@ -2,11 +2,27 @@
[*]
charset=utf-8
end_of_line=lf
trim_trailing_whitespace=false
trim_trailing_whitespace=true
insert_final_newline=false
indent_style=space
indent_size=4
[*.json]
ij_json_array_wrapping = normal
ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false
ij_json_keep_line_breaks = true
ij_json_keep_trailing_comma = false
ij_json_object_wrapping = normal
ij_json_property_alignment = do_not_align
ij_json_space_after_colon = true
ij_json_space_after_comma = true
ij_json_space_before_colon = false
ij_json_space_before_comma = false
ij_json_spaces_within_braces = true
ij_json_spaces_within_brackets = true
ij_json_wrap_long_lines = false
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
@@ -15,7 +31,7 @@ csharp_style_expression_bodied_constructors=true:none
csharp_style_expression_bodied_methods=true:none
csharp_style_expression_bodied_properties=true:suggestion
csharp_style_var_elsewhere=false:suggestion
csharp_style_var_for_built_in_types=false:suggestion
csharp_style_var_for_built_in_types=false:none
csharp_style_var_when_type_is_apparent=true:suggestion
dotnet_naming_rule.local_constants_rule.severity=warning
dotnet_naming_rule.local_constants_rule.style=all_upper_style
@@ -66,7 +82,7 @@ resharper_built_in_type_reference_style_highlighting=hint
resharper_redundant_base_qualifier_highlighting=warning
resharper_suggest_var_or_type_built_in_types_highlighting=hint
resharper_suggest_var_or_type_elsewhere_highlighting=hint
resharper_suggest_var_or_type_simple_types_highlighting=hint
resharper_suggest_var_or_type_simple_types_highlighting=none
resharper_web_config_module_not_resolved_highlighting=warning
resharper_web_config_type_not_resolved_highlighting=warning
resharper_web_config_wrong_module_highlighting=warning

8
.gitattributes vendored Normal file
View File

@@ -0,0 +1,8 @@
# Auto detect text files and perform LF normalization
* text=auto
*.cs text diff=csharp
*.cshtml text diff=html
*.csx text diff=csharp
*.sln text eol=crlf
*.csproj text eol=crlf

View File

@@ -29,14 +29,13 @@ 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-11
- os: macos-14
kind: macOS
target: osx-x64
- os: macos-11
- os: macos-14
kind: macOS
target: osx-arm64
steps:
@@ -46,20 +45,10 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '14'
- name: Cache NPM dependencies
uses: bahmutov/npm-install@v1.8.28
with:
working-directory: ErsatzTV/client-app
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@@ -68,7 +57,7 @@ jobs:
run: dotnet restore -r "${{ matrix.target}}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
uses: Apple-Actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
p12-password: ${{ secrets.apple_developer_certificate_password }}
@@ -83,8 +72,8 @@ jobs:
shell: bash
run: |
sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o publish -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=false -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
- name: Bundle
shell: bash
@@ -97,7 +86,7 @@ jobs:
- name: Sign
shell: bash
run: scripts/macOS/sign.sh
- name: Create DMG
shell: bash
run: |
@@ -112,18 +101,15 @@ jobs:
--hide-extension "ErsatzTV.app" \
--app-drop-link 600 185 \
--skip-jenkins \
--no-internet-enable \
"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 }}
xcrun notarytool submit ErsatzTV.dmg --apple-id "${{ secrets.ac_username }}" --password "${{ secrets.ac_password }}" --team-id 32MB98Q32R --wait
xcrun stapler staple ErsatzTV.dmg
- name: Cleanup
shell: bash
@@ -139,22 +125,20 @@ jobs:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
assets: "*${{ matrix.target }}.dmg"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
prerelease: false
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
files: "${{ env.RELEASE_NAME }}.dmg"
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
build_and_upload_linux:
name: Build & Upload Linux
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
@@ -163,39 +147,23 @@ jobs:
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
target: linux-musl-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: ubuntu-24.04-arm
kind: linux
target: linux-arm64
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '14'
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
if: ${{ matrix.kind == 'windows' }}
- name: Cache NPM dependencies
uses: bahmutov/npm-install@v1.8.28
with:
working-directory: ErsatzTV/client-app
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@@ -203,14 +171,6 @@ jobs:
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target }}"
- uses: suisei-cn/actions-download-file@v1.3.0
if: ${{ matrix.kind == 'windows' }}
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-full_build.7z"
target: ffmpeg/
- name: Build
shell: bash
run: |
@@ -220,34 +180,15 @@ jobs:
# Build everything
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net7.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
# Build Windows launcher
if [ "${{ matrix.kind }}" == "windows" ]; then
cargo build --manifest-path=ErsatzTV-Windows/Cargo.toml --release --all-features
ls -l ErsatzTV-Windows/target/release
mv ErsatzTV-Windows/target/release/ersatztv_windows.exe "$release_name/ErsatzTV-Windows.exe"
fi
# Download ffmpeg
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
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "${{ matrix.target }}" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
mkdir "$release_name"
mv scanner/* "$release_name/"
mv main/* "$release_name/"
tar czvf "${release_name}.tar.gz" "$release_name"
# 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
@@ -256,16 +197,128 @@ jobs:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
assets: "*${{ matrix.target }}.tar.gz"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
prerelease: false
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
files: "${{ env.RELEASE_NAME }}.tar.gz"
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_dotnet_windows:
name: Build dotnet for Windows
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "win-x64"
- name: Build dotnet projects
shell: bash
run: |
sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
dotnet publish ErsatzTV.Scanner/ErsatzTV.Scanner.csproj --framework net9.0 --runtime "win-x64" -c Release -o "scanner" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-win-x64" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net9.0 --runtime "win-x64" -c Release -o "main" -p:RestoreEnablePackagePruning=true -p:InformationalVersion="${{ inputs.release_version }}-win-x64" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
- name: Upload .NET Artifact
uses: actions/upload-artifact@v4
with:
name: dotnet-windows-build
path: |
scanner/
main/
retention-days: 1
build_rust_windows:
name: Build rust for Windows
runs-on: windows-latest
steps:
- name: Get the sources
uses: actions/checkout@v4
- name: Build Windows Launcher
shell: bash
run: cargo build --manifest-path=ErsatzTV-Windows/Cargo.toml --release --all-features
- name: Upload Rust Artifact
uses: actions/upload-artifact@v4
with:
name: rust-windows-build
path: ErsatzTV-Windows/target/release/ersatztv_windows.exe
retention-days: 1
package_and_upload_windows:
name: Package & Upload Windows
runs-on: ubuntu-latest
needs: [build_dotnet_windows, build_rust_windows]
steps:
- name: Download dotnet artifacts
uses: actions/download-artifact@v4
with:
name: dotnet-windows-build
path: dotnet-build
- name: Download rust artifacts
uses: actions/download-artifact@v4
with:
name: rust-windows-build
path: rust-build
- name: Download ffmpeg
uses: suisei-cn/actions-download-file@v1.3.0
id: downloadffmpeg
with:
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/7.1.1/ffmpeg-n7.1.1-56-gc2184b65d2-win64-gpl-7.1.zip"
target: ffmpeg/
- name: Package artifacts
shell: bash
run: |
release_name="ErsatzTV-${{ inputs.release_version }}-win-x64"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
mkdir "$release_name"
mv dotnet-build/scanner/* "$release_name/"
mv dotnet-build/main/* "$release_name/"
# dotnet shouldn't copy the resources here, but it does
rm -rf "$release_name/Resources"
mv rust-build/ersatztv_windows.exe "$release_name/ErsatzTV-Windows.exe"
7z e "ffmpeg/${{ steps.downloadffmpeg.outputs.filename }}" -o"$release_name" '*.exe' -r
rm -f "$release_name/ffplay.exe"
7z a -tzip "${release_name}.zip" "./${release_name}/*"
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
if: ${{ inputs.release_tag == 'develop' }}
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: "*win-x64.zip"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: false
tag_name: ${{ inputs.release_tag }}
files: "${{ env.RELEASE_NAME }}.zip"
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}

View File

@@ -19,14 +19,14 @@ jobs:
tag=$(git describe --tags --abbrev=0)
tag2="${tag:1}"
short=$(git rev-parse --short HEAD)
final="${tag2/beta/$short}"
final="${tag2}-${short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
final="${tag/beta/$short}"
final="${tag}-${short}"
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
@@ -46,7 +46,7 @@ jobs:
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
build_images:
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:

View File

@@ -1,4 +1,4 @@
name: Build & Publish to Docker Hub
name: Build & Publish to Docker Hub
on:
workflow_call:
inputs:
@@ -20,33 +20,28 @@ on:
docker_hub_access_token:
required: true
jobs:
build_and_push:
name: Build & Publish
runs-on: ubuntu-latest
build_images:
name: Build ${{ matrix.name }} image
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- name: base
- name: amd64
os: ubuntu-latest
path: ''
suffix: ''
qemu: false
- name: nvidia
path: 'nvidia/'
suffix: '-nvidia'
qemu: false
- name: vaapi
path: 'vaapi/'
suffix: '-vaapi'
qemu: false
suffix: '-amd64'
platform: 'linux/amd64'
- name: arm32v7
os: ubuntu-latest
path: 'arm32v7/'
suffix: '-arm'
qemu: true
platform: 'linux/arm/v7'
- name: arm64
os: ubuntu-24.04-arm
path: 'arm64/'
suffix: '-arm64'
qemu: true
platform: 'linux/arm64'
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -54,59 +49,93 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
if: ${{ matrix.qemu == true }}
if: ${{ matrix.name == 'arm32v7' }}
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
id: docker-buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Build and push
uses: docker/build-push-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/${{ matrix.path }}Dockerfile
push: true
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
if: ${{ matrix.name != 'arm64' && matrix.name != 'arm32v7' }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
- name: Build and push by digest
id: build
uses: docker/build-push-action@v5
with:
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/${{ matrix.path }}Dockerfile
push: true
platforms: 'linux/arm64'
provenance: false
platforms: ${{ matrix.platform }}
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
if: ${{ matrix.name == 'arm64' }}
outputs: |
type=image,name=jasongdove/ersatztv,name-canonical=true,push-by-digest=true
type=image,name=ghcr.io/ersatztv/ersatztv,name-canonical=true,push-by-digest=true
- name: Build and push
uses: docker/build-push-action@v3
- name: Save digest to artifact
run: echo ${{ steps.build.outputs.digest }} > digest.txt
- name: Upload digest artifact
uses: actions/upload-artifact@v4
with:
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/${{ matrix.path }}Dockerfile
push: true
platforms: 'linux/arm/v7'
build-args: |
INFO_VERSION=${{ inputs.info_version }}-docker${{ matrix.suffix }}
tags: |
jasongdove/ersatztv:${{ inputs.base_version }}${{ matrix.suffix }}
jasongdove/ersatztv:${{ inputs.tag_version }}${{ matrix.suffix }}
if: ${{ matrix.name == 'arm32v7' }}
name: digest-${{ matrix.name }}
path: digest.txt
merge_manifests:
name: Merge Manifests
runs-on: ubuntu-latest
needs: build_images
steps:
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download all digest artifacts
uses: actions/download-artifact@v4
with:
path: digests/
- name: Read digests from artifacts
id: digests
run: |
AMD64_HASH=$(cat digests/digest-amd64/digest.txt)
ARM32V7_HASH=$(cat digests/digest-arm32v7/digest.txt)
ARM64_HASH=$(cat digests/digest-arm64/digest.txt)
DOCKER_HUB_DIGESTS="jasongdove/ersatztv@${AMD64_HASH} jasongdove/ersatztv@${ARM64_HASH} jasongdove/ersatztv@${ARM32V7_HASH}"
GHCR_DIGESTS="ghcr.io/ersatztv/ersatztv@${AMD64_HASH} ghcr.io/ersatztv/ersatztv@${ARM64_HASH} ghcr.io/ersatztv/ersatztv@${ARM32V7_HASH}"
echo "docker_hub_digests=${DOCKER_HUB_DIGESTS}" >> $GITHUB_OUTPUT
echo "ghcr_digests=${GHCR_DIGESTS}" >> $GITHUB_OUTPUT
- name: Create and push manifests
run: |
docker manifest create jasongdove/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.docker_hub_digests }}
docker manifest push jasongdove/ersatztv:${{ inputs.base_version }}
docker manifest create jasongdove/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.docker_hub_digests }}
docker manifest push jasongdove/ersatztv:${{ inputs.tag_version }}
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }} ${{ steps.digests.outputs.ghcr_digests }}
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.base_version }}
docker manifest create ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }} ${{ steps.digests.outputs.ghcr_digests }}
docker manifest push ghcr.io/ersatztv/ersatztv:${{ inputs.tag_version }}

27
.github/workflows/issue-stale.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: 'Close stale issues'
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
operations-per-run: 500
exempt-issue-labels: 'regression,security,roadmap,future,feature,enhancement,confirmed'
stale-issue-label: 'stale'
stale-issue-message: |-
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you have any questions you can use one of several ways to [contact us](https://ersatztv.org).
close-issue-message: |-
This issue was closed due to inactivity.

View File

@@ -8,15 +8,10 @@ jobs:
- name: Get the sources
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@@ -31,39 +26,50 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
- name: Build Windows
run: |
cd ErsatzTV-Windows
cargo build --release --all-features
build_and_test_linux:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-latest
target: linux-x64
- os: ubuntu-latest
target: linux-musl-x64
- os: ubuntu-latest
target: linux-arm
- os: ubuntu-24.04-arm
target: linux-arm64
steps:
- name: Get the sources
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
run: dotnet restore -p:RestoreEnablePackagePruning=true -r "${{ matrix.target }}"
- name: Prep project file
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
- name: Build
run: dotnet build --configuration Release --no-restore
run: dotnet build ErsatzTV/ErsatzTV.csproj --runtime "${{ matrix.target }}" --configuration Release --no-restore && dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal
build_and_test_mac:
runs-on: macos-11
runs-on: macos-14
steps:
- name: Get the sources
uses: actions/checkout@v4
@@ -71,10 +77,10 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: 7.0.x
dotnet-version: 9.0.203
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
@@ -89,4 +95,4 @@ jobs:
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
run: dotnet test --blame-hang-timeout "2m" --no-restore --verbosity normal

View File

@@ -16,7 +16,7 @@ jobs:
run: |
tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |
@@ -41,7 +41,7 @@ jobs:
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
build_images:
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:

View File

@@ -1,22 +0,0 @@
name: Lint VueJS Files on PR Request
on:
pull_request:
jobs:
vue-lint:
runs-on: ubuntu-latest
steps:
# Checkout the current repo
- name: Checkout current repository
uses: actions/checkout@v4
# Setup NodeJS version 16
- name: Setup NodeJS V16.x.x
uses: actions/setup-node@v3
with:
node-version: '16'
# CD into the current client directory and lint and build the client
- name: Lint and Build the client
run: |
cd ./ErsatzTV/client-app/
npm ci --no-optional
npm run lint
npm run build --if-present

1
.gitignore vendored
View File

@@ -40,6 +40,7 @@ msbuild.wrn
core
scripts/generate-api-sdk/swagger.json
scripts/download-test-content.sh
docker-compose.override.yml

View File

@@ -1,10 +1,774 @@
# Changelog
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
## [25.4.0] - 2025-08-05
### Added
- Add `Troubleshoot Playback` to overflow menu on all media cards
- This should eliminate the need to lookup media ids for content
- Add subtitle selection to playback troubleshooting. This is limited to:
- Sidecar text subtitles (e.g. `srt` files)
- Embedded image subtitles
- Embedded text subtitles that have already been extracted by ETV
- Add light mode and light/dark mode toggle to app bar
- YAML playout: add `pre_roll` instruction to enable and disable a pre-roll sequence
- With value of `true` and `sequence` property, will enable automatic pre-roll for all content in the playout to the sequence with the provided key
- With value of `false`, will disable automatic pre-roll in the playout
- YAML playout: add `post_roll` instruction to enable and disable a post-roll sequence
- With value of `true` and `sequence` property, will enable automatic post-roll for all content in the playout to the sequence with the provided key
- With value of `false`, will disable automatic post-roll in the playout
- YAML playout: add `mid_roll` instruction to enable and disable a mid-roll sequence
- With value of `true` and `sequence` property, will enable automatic mid-roll for (`count` and `all`) content in the playout to the sequence with the provided key
- With value of `false`, will disable automatic post-roll in the playout
- `expression` can be used to influence which chapters are selected for mid roll (same as in filler preset)
- YAML playout: add `rewind` instruction to set start of playout relative to the current time
- Value should be formatted as `HH:MM:SS` e.g. `00:05:30` for 5 minutes 30 seconds (before now)
- This is instruction is mostly useful for debugging transitions, and can only be used as a reset instruction
- YAML playout: add `import` section to allow importing partial YAML definitions that include `content` and `sequence` entries
- Add YAML playout validation (using JSON Schema)
- Invalid YAML playout definitions will fail to build and will log validation failures as warnings
- `content` is fully validated
- `sequence` is fully validated
- `reset` is fully validated
- `playout` is fully validated
- Add `Playlist` collection type to filler presets
- This will force filler mode `Count`
- Whenever the filler is used, it will schedule `Count` times full time through the playlist
- If the playlist has 3 items and none set to play all, it will schedule 3 items when `Count = 1`
- If the playlist has 3 items and none set to play all, it will schedule 6 items when `Count = 2`
- Using the same playlist in the same schedule for anything other than filler may cause undesired behavior
- Detect supported VideoToolbox hardware decoders and encoders
- Software decoders/encoders will automatically be used when hardware versions are unavailable
- Add VideoToolbox Capabilities to Troubleshooting page
- Add `Use Chapters As Media Items` option to filler preset
- This option allows scheduling individual chapters as filler
- The chapters are shuffled or otherwise sorted together just like normal filler would be
- Add smart collection edit page to allow renaming smart collections
- Previous edit link behavior (performing search using smart collection query) now uses magnifying glass icon
- Add channel `Transcode Mode` setting
- This setting is currently disabled and only has the value `On Demand`
- Add channel `Idle Behavior` setting to control the transcoding behavior after all clients have disconnected
- `Stop On Disconnect` - stops the transcoder after all clients have disconnected + the global idle timeout
- `Keep Running` - transcoder will run until manually stopped
- Add support for music video thumbnails that end in `-thumb`
- For example `Music Video.mkv` could have a corresponding thumbnail `Music Video-thumb.jpg`
- Reorganize troubleshooting page
- Add `YAML Validation` tool in `Troubleshooting` > `Tools`
### Fixed
- Fix app startup with MySql/MariaDB
- YAML playout: fix `pad_to_next` always running over time
- Fix playback with text subtitles when seeking into content, i.e. when first joining a channel
- Fix playback with `.ass` and `.ssa` text subtitles
- Fix green padding with 10-bit source content and i965 VAAPI driver
- Fix building playouts with empty schedules
- Fix schedule start time calculation when daily playout build goes beyond midnight and into a different alternate schedule
- Fix compatibility with older NVIDIA devices (compute capability 3.0+) in unified docker image
- Fix transitions when using NVIDIA, QSV and VAAPI acceleration
- Fix playback of remote streams on channels where framerate normalization is enabled
### Changed
- Always tell ffmpeg to stop encoding with a specific duration
- This was removed to try to improve transitions with ffmpeg 7.x, but has been causing issues with other content
- Move search debug logging to its own log category; add `Searching Minimum Log Level` to `Settings` > `Logging`
- Classic schedules: always schedule the full `Duration` amount instead of stopping mid-duration
- This allows duration items to be scheduled beyond midnight
- e.g. fixed start time 22:00 with 4 hour duration will schedule until 02:00 instead of stopping at midnight
- Rename channel setting `Progress Mode` to `Playout Mode`
- This controls the progression of the channel's playout, and has nothing to do with transcoding
- `Always` is now called `Continuous` (playout progresses with wall clock)
- `On Demand` is unchanged (playout only progresses while a client is watching the channel)
- Replace channel `Active Mode` setting with new `Is Enabled` and `Show In EPG` settings
- `Active` channels will be converted to `Is Enabled` = true and `Show In EPG` = true
- `Hidden` channels will be converted to `Is Enabled` = true and `Show In EPG` = false
- `Inactive` channels will be converted to `Is Enabled` = false and `Show In EPG` = false
## [25.3.1] - 2025-07-24
### Fixed
- Fix fallback filler playback
## [25.3.0] - 2025-07-24
### Added
- Add new channel stream (audio and subtitle) selector system
- Channel editor has a new field `Stream Selector Mode`
- `Default` maintains existing behavior
- `Custom` uses a YAML config file
- The YAML config contains a prioritized list of stream selector "items" (audio and subtitle criteria pairs)
- The items are tested against the media from top to bottom, and when (at least) a matching audio track is found, stream selection occurs
- As an example, the custom stream selector config can specify (in priority order):
- english audio (and disable subtitles)
- any other audio (and english subtitles, if they exist)
- Criteria can include
- Stream language
- Stream title (allowed title and/or blocked title)
- Stream condition, which is an expression that can use
- `id` (index)
- `title`
- `lang`
- `default`
- `forced`
- `sdh` (subtitle only)
- `external` (subtitle only)
- `codec`
- `channels` (audio only)
- An example subtitle condition: `lang like 'en%' and external`
- An example audio condition: `title like '%movie%' and channels > 2`
- Add new channel setting `Active Mode`
- `Active` - default value, channel streams as normal and has normal visibility
- `Hidden` - channel streams as normal and is hidden from M3U/XMLTV/HDHR
- `Inactive` - channel cannot stream (will 404) and is hidden from M3U/XMLTV/HDHR
- Synchronize Plex "network" metadata for Plex show libraries
- Shows will have new `network` search field
- Episodes will have new `show_network` search field
- YAML playout: add `stop_before_end` setting to `pad_until` and `duration` instructions
- When `stop_before_end: false`, content can run over the desired time before executing the next instruction
- YAML playout: add `offline_tail` setting to `pad_until` instruction
- This can be used to stop primary content before the desired time (`stop_before_end: true` and `offline_tail: false`)
- You can then have a second `pad_until` with the same target time and different content
- YAML playout: make `tomorrow` an expression on `pad_until` instruction
- `true` and `false` still work as normal
- The current time (as a decimal) can also be used in the expression, e.g. `now > 23`
- `now = hours + minutes / 60.0 + seconds / 3600.0`
- So `10:30 AM` would be `10.5`, `10:45 PM` would be `22.75`, etc
- YAML playout: make `skip_items` an expression
- The following parameters can be used:
- `count`: the total number of items in the content
- `random`: a random number between zero and (count - 1)
- For example:
- `count / 2` will start in the middle of the content
- `random` will start at a random point in the content
- `2` (similar to before this change) will skip the first two items in the content
- YAML playout: make `count` an expression
- The following parameters can be used:
- `count`: the total number of items in the content
- `random`: a random number between zero and (count - 1)
- For example:
- `count / 2` will play half of the items in the content
- `random % 4 + 1` will play between 1 and 4 items
- `2` (similar to before this change) will play exactly two items
- YAML playout: add `disable_watermarks` property to all content instructions
- This property defaults to `false` (meaning watermarks are allowed by default)
- Setting to `true` will prevent watermarks from ever appearing over the content
- YAML playout: add `watermark` instruction
- With value of `true` and `name` property, will override the watermark in the playout to the watermark with the provided name
- With value of `false`, will restore default watermark value (channel watermark, global watermark)
- Show health check warning and error badges in nav menu
- Add `Expression` for mid-roll filler to allow custom logic for using or skipping chapter markers
- The following parameters can be used:
- `total_points`: total number of potential mid-roll points
- `matched_points`: number of mid-roll points that have already matched the expression
- `total_duration`: total duration of the content, in seconds
- `total_progress`: normalized position from 0 to 1
- `last_mid_filler`: seconds since last mid-roll filler
- `remaining_duration`: duration of the content after this mid-roll point, in seconds
- `point`: the position of the mid-roll point, in seconds
- `num`: the mid-roll point number, starting with 1
- Add `Disable Watermarks` checkbox to block items
- Block items that have this checked will never display a watermark, even with Deco set to override watermark
- Add `ETV_MAXIMUM_UPLOAD_MB` environment variable to allow uploading large watermarks
- Default value is 10
- Update ffmpeg health check to link to ErsatzTV-FFmpeg release that contains binaries for win64, linux64, linuxarm64
- Add `Playback Troubleshooting` page
- This tool lets you play specific content without needing a test channel or schedule
- You can specify
- The media item id (found in ETV media info, and ETV movie URLs)
- The ffmpeg profile to use
- The watermark to use (if any)
- Clicking `Play` will play up to 30 seconds of the specified content using the desired settings
- Clicking `Download Results` will generate a zip archive containing:
- The FFmpeg report of the playback attempt
- The media info for the content
- The `Troubleshooting` > `General` output
- Support `(Part [english number])` name suffixes for multi-part episode grouping, for example:
- `Awesome Episode (Part One)`
- `Better Episode (Part Two)`
- `Not So Great (Part Three)`
- Add Trakt List option `Auto Refresh` to automatically update list from trakt.tv once each day
- Add Trakt List option `Generate Playlist` to automatically generate ETV Playlist from matched Trakt List items
- Read `country` field from movie NFO files and include in search index as `country`
- Add *experimental* and *incomplete* `Remote Stream` library kind
- Remote Stream libraries have fallback metadata added like Other Video libraries (every folder is a tag)
- Remote Stream library items consist of YAML (`.yml`) files with the following fields
- `url`: the URL of the content that can be played directly by ffmpeg
- `script`: the process name and arguments for a command that will output content to stdout
- `is_live`: *required* property that indicates whether the remote stream contains live content
- When this is set to `true`, ETV cannot work ahead on transcoding this item, which is a necessary tradeoff for supporting live content
- When this is set to `false`, ETV will treat the stream as VOD and attempt to work ahead on transcoding like any other local item
- This *will* cause errors when the content is actually live, so it's important to configure this correctly
- `duration`: when the content is live and does not have duration metadata, this must be provided to allow scheduling
- The remote stream definition (YAML file) may provide either a `url` or a `script`
- If both are provided, `url` will be used
- Include number of chapters in search index as `chapters`
### Changed
- Allow `Other Video` libraries and `Image` libraries to use the same folders
- Try to mitigate inotify limit error by disabling automatic reloading of `appsettings.json` config files
- Support `movie`, `musicvideo` and `episodedetails` top-level tags in other video NFO files
- Note that no change has been made to the metadata tags that are actually parsed, but this should help with various types of content
- Remove some limits on multithreading that are no longer needed with latest ffmpeg
- Mixed transcoding (software decode, hardware filters/encode) can now use multiple decode threads
- Split main `Settings` page into multiple pages
- Update UI layout on all pages to be less cramped and to work better on mobile
- Add CPU and Video Controller info to `Troubleshooting` > `General` output
- Enable write-ahead logging (WAL) mode on SQLite databases
- Add `Multiple Mode` option to schedule items editor and remove support for count values of zero
- `Count`: same behavior as before, requires a number of media items to play and will always schedule the same number
- `Collection Size`: similar to count of zero before, will play all media items from the collection before continuing to the next schedule item
- `Playlist Item Size`: will play all media items from the current playlist item before continuing to the next schedule item
- `Multi-Episode Group Size`: will play all media items from the current multi-part episode group, or one ungrouped media item
- Change watermark width and margins to allow decimals
- Move `Add To Collection` button to overflow menu on all media cards, and add `Show Media Info` to overflow menu
- This allows showing media info for all media kinds
- Unify on a multi-platform base docker tag (`latest` and `develop`)
- `amd64`, `arm64`, `arm/v7` platforms are now all supported in the base docker tag
- Other docker platform tags are deprecated and will receive no new updates after the next release
- A health check has been added to notify users (on `-arm` or `-arm64` tags) of this change
### Fixed
- Fix QSV acceleration in docker with older Intel devices
- Fix HDR transcoding with NVIDIA accel for:
- All NVIDIA docker users
- Windows NVIDIA users who have set the `ETV_DISABLE_VULKAN` env var
- Fix audio sync issue with QSV acceleration
- YAML playout: fix history for marathon and playlist content
- This allows playouts to be extended correctly, instead of always resetting to the earliest item in each group
- Fix using channel External Logo URL as watermark
- Fix display of SVG channel logo and watermark in admin UI
- Existing SVG logos and watermarks will have to be re-uploaded to display properly in the admin UI
- This does not affect streaming at all; existing artwork still works fine for streaming
- Classify HDHR endpoints as streaming endpoints
- This allows these endpoints to be accessed through port `ETV_STREAMING_PORT` (default `8409`)
- This only matters if you configured `ETV_UI_PORT` to be a different value, which makes UI endpoints inaccessible on the streaming port
- Update Plex movie/other video plot ("summary") during library deep scan
- Fix compatibility with ffmpeg 7.2+ when using NVIDIA accel and 10-bit source content
- Fix some NVIDIA edge cases when media servers don't provide video bit depth information
- Fix VAAPI tonemap failure
- Fix green bars after VAAPI tonemap
- Fix bug where playout mode `Multiple` would ignore fixed start time
- Fix block playout EPG generation to use `XMLTV Time Zone` setting
- Fix adding "official" Trakt lists
- Fix searching for `collection` names with spaces or other special characters, e.g. `collection:"Movies - Action"`
- Fix QSV transcoding errors when scaling
- Fix QSV frame freezing in browser
- Fix some stream continuity issues, and some cases where audio sync is lost at transition
- Fix HDR transcoding with AMD VAAPI accel
- Allow paths longer than 255 characters in MySql databases
## [25.2.0] - 2025-06-24
### Added
- Add `linux-musl-x64` artifact for users running Alpine x64
- Add API endpoint to empty trash (POST to `/api/maintenance/empty_trash`)
- e.g. `curl -XPOST -d '' http://localhost:8409/api/maintenance/empty_trash`
- Add remote IP and user agent to HTTP request logging
- Add environment variables to allow ETV to run UI and streaming on separate ports
- `ETV_STREAMING_PORT`: port used for streaming requests, defaults to 8409
- `ETV_UI_PORT`: port used for admin UI, defaults to 8409
- Publish docker images to ghcr.io (`ghcr.io/ersatztv/ersatztv`)
- Add new option `Fixed Start Time Behavior` to Schedules and Schedule Items
- Schedules can set a default behavior for all items
- Schedule items can override this default behavior
- Possible values are:
- `Strict`: Always wait for the exact start time, even if that means waiting (adding unscheduled time) until the next day
- `Flexible`: Start scheduling immediately (do not wait) if waiting (adding unscheduled time) would go into the next day
- As an example, if the current scheduling time is 6:02 AM and the next schedule item has a fixed start time of 6:00 AM
- `Strict` will add nearly 24h (23:58) of unscheduled time so that it can start exactly at 6:00 AM the next day
- `Flexible` will NOT add unscheduled time, and will schedule its item at 6:02 AM (which may also affect the scheduling of later items)
- Add basic HDR transcoding support
- VAAPI may use hardware-accelerated tone mapping (when opencl accel is also available)
- NVIDIA may use hardware-accelerated tone mapping (when vulkan accel and libplacebo filter are also available)
- QSV may use hardware-accelerated tone mapping (when hardware decoding is used)
- In all other cases, HDR content will use a software pipeline
- The tonemap algorithm can be configured in the ffmpeg profile
- Use hardware-accelerated padding with VAAPI
- Add environment variable `ETV_DISABLE_VULKAN`
- Any non-empty value will disable use of Vulkan acceleration and force software tonemapping
- This may be needed with misbehaving NVIDIA drivers on Windows
- Add health check error when invalid VAAPI device and VAAPI driver combination is used in an active ffmpeg profile
- This makes it obvious when hardware acceleration will not work as configured
- Add button in schedule editor to clone schedule item
- Allow YAML playout sequence definitions to reference other sequences
- Cycles will be detected and logged, and sequences with cycles will prevent the playout from building
- Add `repeat` property to YAML sequence instruction
- This tells the playout builder how many times this sequence should repeat
- Omitting this value is the same as setting it to `1`
- Add `collection` (name) to search index for manual collections created within ETV
- Collections synchronized from media servers are still indexed as `tag`
- Allow searching by `smart_collection` (name)
- Quotes are *always* required around each collection name when using this feature
- e.g. `smart_collection:"one" OR smart_collection:"two"`
- Cycles will be detected and logged, and searches with cycles will not work as expected
- Add all `ETV_*` environment variables to Troubleshooting > General info
- Add `External Logo URL` field to channel editor
- Using external (public) logos should fix channel logo display for clients that don't proxy artwork (such as Plex)
- Users who have customized the XMLTV channel template `channel.sbntxt` will need to update their templates again
- This is because the templates require different logic for external URLs vs ETV-hosted URLs
### Changed
- Start to make UI minimally responsive (functional on smaller screens)
- Change how ETV determines which address to use for Plex connections
- The active Plex connection (address) will only be cached for 30 seconds
- When the connection is no longer cached, a ping will be sent to the last used address for Plex (the last address that had a successful ping)
- If the ping is successful, the address will be cached for another 30 seconds
- If the ping is not successful, all addresses will be checked again, and the first address to return a successful ping will be cached for 30 seconds
- Remove requirement to have Jellyfin admin user; user id is no longer required on requests to latest Jellyfin server
- Upgrade bundled ffmpeg on Windows from 6.1 to 7.1.1
- Upgrade VAAPI docker image Ubuntu base from 22 to 24; bundled ffmpeg from 6.1 to 7.1.1
- Upgrade NVIDIA docker image Ubuntu base from 20 to 24; bundled ffmpeg from 6.1 to 7.1.1
- Upgrade base, arm, arm64 docker images bundled ffmpeg from 6.1 to 7.1.1
- Unify all hardware acceleration methods in base docker images (`latest` and `develop`)
- VAAPI, QSV and NVIDIA are now all supported in the base docker image
- Other docker image tags are deprecated and will receive no new updates after the next release
- A health check has been added to notify users (on `-vaapi` or `-nvidia` tags) of this change
- Schedule items editor: show currently selected row using background color instead of font weight
### Fixed
- Fix error message about synchronizing Plex collections from a Plex server that has zero collections
- Fix navigation after form submission when using `ETV_BASE_URL` environment variable
- Fix UI crashes when channel numbers contain a period `.` in locales that have a different decimal separator (e.g. `,`)
- Fix playout detail table to only reload once when resetting a playout
- Fix date formatting in playout detail table on reload (will now respect browser's `Accept-Language` header)
- Use cache busting to avoid UI errors after upgrading the MudBlazor library
- Fix multi-variant playlist to report more accurate `BANDWIDTH` value based on ffmpeg profile
- Fix detecting NVIDIA capabilities on Blackwell GPUs
- Fix decoder selection in NVIDIA pipeline
- Prevent playback order `Shuffle In Order` from being used with `Fill With Group Mode` as they are incompatible
- Fix XMLTV items not grouping properly (guide mode: `Filler`) due to post-roll filler
## [25.1.0] - 2025-01-10
### Added
- Add `Reset All Playouts` button to top of playouts page
- Add `rewind_on_reset` option to `wait_until` YAML playout instruction
- This option allows YAML playouts to start in the past
- Add `advance` option to `epg_group` YAML playout instruction
- When set to `false`, this option will lock the guide group without starting a new guide group
- This can be helpful for "post roll" items that should be part of the previous item's guide group
- Add `Song Video Mode` to channel settings
- `Default` - existing behavior
- `With Progress` - show animated progress bar at bottom of generated video
- Thanks to @JeckDev for the idea and the artwork
- Add fallback album art image for songs that have no album art
- Add `Vaapi Display` option to FFmpeg Profile
- Possible values will be install-specific and sourced from `vainfo`
- `drm` was the previous default value, and should be used in most cases
- Test all `Vaapi Display` values in `Troubleshooting` > `VAAPI Capabilities`
- Add `tag_full` field to search index
- This field contains the same values as the existing `tag` field, but it is not analyzed or tokenized
### Changed
- **BREAKING CHANGE**: Change channel identifiers used in XMLTV to work around bad behavior in Plex
### Fixed
- Fix startup error with MySql backend caused by database cleaner
- Fix emptying trash with ElasticSearch backend
- Fix double loading of trash UI elements, and fix reloading of all UI elements after emptying trash
- Fix destroying channel preview player when preview dialog is closed
- This bug made it difficult to "stop" a channel after previewing it
- Fix bug where deco default filler would never use hardware acceleration
- Fix deleting local libraries with MySql backend
- Fix `Scaling Behavior` `Crop` when content is smaller than FFmpeg Profile resolution
- Now, content will properly scale beyond the desired resolution before cropping
- Fix displaying playout item durations that are greater than 24 hours
- Fix building playouts when playlist has been changed to have fewer items
- Fix selecting audio stream with preferred title
- Fix synchronizing Plex collections
- If this breaks collection sync for you, you will need to update your Plex server
- Fix guide group generation for `duration` YAML instructions
- Fix default song background when targeting 4:3 resolutions
- Previously the background was always 16:9 and was padded, now it will fill 4:3
## [0.8.8-beta] - 2024-09-19
### Added
- Add support for Plex Other Video libraries
- These libraries will now appear as ETV Other Video libraries
- Items in these libraries will have tag metadata added from folders just like local Other Video libraries
- Thanks @raknam for adding this feature!
- Add *experimental* support for `On Demand` channel progress
- With `On Demand` channel progress, the playout will only advance when the channel is being streamed
- When the channel is idle, the playout is unmodified and will be shifted forward as needed so no content is missed
- Setting a channel to `On Demand` progress will disable alternate schedules
- The `On Demand` setting will only be used for `Flood` playouts (NOT `Block` or `External JSON`)
- It is NOT recommended to use fixed start times with `On Demand` progress
- This will probably be disabled with a future update
- Add `Default Filler` to `Deco` system
- After all blocks are scheduled/added to the playout, a second pass will be made to insert filler
- Default filler will be shuffled and inserted in all unscheduled time between blocks
- Default filler will stop scheduling when the next item would extend into primary content
- Alternatively, default filler can be configured to `Trim To Fit`
- In this case, the last item that would extend into primary content is trimmed to end exactly when the primary content starts
- Add **experimental** playout type `YAML`
- This playout type uses a YAML file to declare content and describe how the playout should be built
- Content currently supports search queries
- Playout instructions currently include `count`, `pad to next`, and `repeat`
- `count`: add the specified number of items (from the referenced content) to the playout
- `duration`: play the referenced content for the specified duration
- `pad to next`: add items from the referenced content until the wall clock is a multiple of the specified minutes value
- `repeat`: continue building the playout from the first instruction in the YAML file
- Add channel logo generation by @raknam
- Channels without custom uploaded logos will automatically generate a logo that includes the channel name
- Add two new API endpoints
- Reset playout for channel
- POST `/api/channels/{channelNumber}/playout/reset`
- Scan library
- POST `/api/libraries/{libraryId}/scan`
- Add Deco setting to `Use Watermark During Filler`
- This setting is turned OFF by default, meaning filler will NOT use the configured watermark unless this is manually turned on
- Add `Random Count` filler mode by @embolon
- This mode will randomly schedule between zero and the provided count number of items
- e.g. random count 3 will schedule between 0 and 3 filler items
- Add `Random Rotation` playback order for block scheduling by @embolon
- This playback order will pick a random item from a randomly selected group (show or artist)
- It is somewhat similar to the `Fill With Group` mode used in flood scheduling
### Fixed
- Add basic cache busting to XMLTV image URLs
- This should help with clients not showing correct channel logos or posters
- Fix artwork in other video libraries by @raknam
- Fix adding items to empty playlists
- Fix filler preset editor and deco dead air fallback editor to only show supported collection types
- Fix infinite loop caused by impossible schedule (all collection items longer than schedule item duration)
- Fix selecting audio and subtitle streams with two-letter language codes
- Fix adding pad filler to content that is less than one minute in duration
- Generate unique identifier for virtual HDHomeRun tuner by @raknam
- This allows a single Plex server to connect to multiple ETV instances
- Include *all* language codes from media library in preferred audio and subtitle language options
- Language codes where an English name cannot be found will be at the bottom of the list
- Fix local libraries to detect external subtitle files with unrecognized language codes
- Fix playback selection of subtitles with unrecognized language codes
- Fix incorrectly removing block items that are hidden from EPG when deco filler is applied
- Fix deco selection when deco is scheduled until midnight
- Previously, this deco item would be ignored so watermark and filler would be missing
- Fix movies with missing medata by generating fallback metadata
- This allows these movies to appear in the Trash where they can be deleted
- Fix synchronizing trakt lists from users with special characters in their username
- Note that these lists MUST be added as URLs; the short-form `user/list` will NOT work with special characters
- Fix local subtitle scanner to detect non-lowercase extensions (e.g. `Movie (2000).EN.SRT`)
- Fix adding a single image to a manual collection from search results
- Fix loading manual collection view when collection contains images
- Fix edge case where block playout history would get stuck and repeat an item
- Fix adjusting watermark opacity when watermark already contains alpha channel (is already transparent)
### Changed
- Remove some unnecessary API calls related to media server scanning and paging
- Improve trakt list URL validation; non-trakt URLs will no longer be requested
- Prevent saving block templates when blocks are overlapping
- This can happen if block durations are changed for blocks that are already on the template
- Redirect variant playlist request to proper URL for starting `HLS Segmenter` session when no session is active
- This can happen when some clients "pause" long enough for the session to stop in ETV
- When the client resumes playback, it requests the temp playlist URL which is now invalid e.g. `/iptv/session/1/hls.m3u8` (not the original URL `/iptv/channel/1.m3u8`)
- To fix, the client will be redirected back to the original URL in this case which will create a new session
## [0.8.7-beta] - 2024-06-26
### Added
- Add `Active Date Range` to block playout template editor to allow limiting templates to a specific date range
- This is year-agnostic, meaning the Month/Day range will apply to every year
- This also supports wrapping the end of the year (e.g., start 12/1 and end 1/15)
- Add new `Deco` system for "decorating" channels with non-primary content
- Decos currently contain
- Watermarks
- Dead Air Fallback (i.e. fallback filler)
- Similar to blocks, decos have deco groups for organization
- Similar to blocks, decos have deco templates for filling a "day" with decos
- In the playout template editor, playout template items can have *both* a block template and a deco template
- This allows watermarks and dead air fallback to change at different times than primary content
- Block playouts can also have a default deco
- This will apply whenever a deco template is missing, or when a deco template item cannot be found for the current time
- Effectively, this sets a default watermark and dead air fallback for the entire playout
- Add `XMLTV Days To Build` setting, which is distinct from the existing `Playout Days To Build` setting
- The value for `XMLTV Days To Build` cannot be larger than `Playout Days To Build`
- This allows, for example, a week of playout data while optimizing XMLTV data to only a day or two
- Add health check to detect config folder issue on MacOS
- ETV versions through v0.8.4-beta (using dotnet 7) stored config data in `$HOME/.local/share/ersatztv`
- ETV versions starting with v0.8.5-beta (using dotnet 8) store config data in `$HOME/Library/Application Support/ersatztv`
- If a dotnet 8 version of ETV has NOT been launched on MacOS, it will automatically migrate the config folder on startup
- If a dotnet 8 version of ETV *has* been launched on MacOS, a failing health check will display with instructions on how to resolve the config issue to restore data
- Add `Video Profile` setting to `FFmpeg Profile` editor when `h264` format is selected
- Add `Video Preset` setting to `FFmpeg Profile` editor for some combinations of acceleration and video format:
- `Nvenc` / `h264`
- `Nvenc` / `hevc`
- `Qsv` / `h264`
- `Qsv` / `hevc`
- `None` / `h264`
- `None` / `hevc`
- Add *experimental* list type `Playlist`
- Playlists contain an ordered list of:
- Collections
- Multi-Collections
- Smart Collections
- TV Shows
- TV Seasons
- Artists
- Movies
- Episodes
- Music Videos
- Other Videos
- Songs
- Images
- Playlists can be added to schedules as a schedule item
- Each time through the playlist, one item will be scheduled from each playlist item (if `Play All` is unchecked)
- NB: This does not mean every collection will always schedule one item; the normal flood playout restrictions like duration and fixed start times still apply here
- If `Play All` is checked, that playlist item will play all of its items each time through the playlist
- This can be helpful if you want to play entire collections in a specific order, e.g.
- Every episode from Show 1 Season 2
- Every episode from Show 2 Season 3
- Every episode from Show 1 Season 3
- Playlist items with fewer media items will be re-shuffled (if applicable) before those with more media items
- Add two new environment variables to customize config and transcode folder locations
- `ETV_CONFIG_FOLDER`
- `ETV_TRANSCODE_FOLDER`
- Add checkbox to allow use of B-frames in FFmpeg Profile (disabled by default)
### Fixed
- Fix some cases of 404s from Plex when files were replaced and scanning the library from ETV didn't help
- Fix more wildcard search phrase queries (when wildcards are used in quotes, like `title:"law & order*"`)
- Fix non-wildcard simple queries when asterisks are used in quotes, like `title:"m*a*s*h"`
- Fix bug where channels would unnecessarily wait on each other
- e.g. in-progress streams would delay responding with a playlist when new streams were starting
- Update Plex show title in ETV when changed in Plex
- Reindex seasons and episodes when show is updated from media server
- This is needed to keep `show_*` tags accurate in the search index (e.g., `show_title`, `show_studio`)
- Fix external subtitle detection to support forced/sdh subtitles with language tag before and after forced/sdh tag:
- `Something.forced.en.srt`
- `Something.sdh.en.srt`
- `Something.en.forced.srt`
- `Something.en.sdh.srt`
- Fix playback from Jellyfin 10.9 by allowing playlist HTTP HEAD requests
- Fix `HLS Segmenter V2` segment duration (previously 10s, now 4s)
- Fix `HLS Segmenter V2` error video generation
- Fix MySql database migrations
- Fix Plex library scans with MySql/MariaDB
- Fix block playout playback when no deco is configured
- Fix `HLS Segmenter V2` to delete old segments (use less disk space while channel is active)
- Fix template and deco template editors to prevent items that go beyond midnight
- Fix block playout random seeds
- Different blocks within a single playout will now correctly use different random seeds (shuffles)
- Erasing block playout history will also generate new random seeds for the playout
- Fix building playouts that use mid-roll filler and have content without chapter markers
- When this happens, mid-roll will be treated as post-roll
- Fix VAAPI decoder capability check
- This caused some streams to incorrectly use software decoding
- Fix scheduling loop/failure caused by some duration schedule items
- Fix `video_bit_depth` search field for Plex media
- Fix template and deco template editors with MariaDB/MySql backend
- Fix transcoding 10-bit source content using QSV acceleration on Windows
### Changed
- Show health checks at top of home page; scroll release notes if needed
- Improve `HLS Segmenter V2` compliance by:
- Serving fmp4 segments when `hevc` video format is selected
- > 1.5. The container format for HEVC video MUST be fMP4.
- Using accurate BANDWIDTH value in multi-variant playlist
- Using proper MIME types for statically-served `.m3u8` and `.ts` files
- Serving playlists with gzip compression
- Use `HLS Segmenter V2` for channel preview when channel is configured for `HLS Segmenter V2`
- Detect and use `/dev/dri/card*` devices in addition to `/dev/dri/render*` devices
- Change default folder locations in docker using new environment variables
- `ETV_CONFIG_FOLDER` - now defaults to `/config`
- `ETV_TRANSCODE_FOLDER` - now defaults to `/transcode`
- If the old locations are still present in docker, these variables will be ignored, so you can migrate at your own pace
- Old config location: `/root/.local/share/ersatztv`
- Old transcode location: `/root/.local/share/etv-transcode`
## [0.8.6-beta] - 2024-04-03
### Added
- Add `show_studio` and `show_content_rating` to search index for seasons and episodes
- Add two new global subtitle settings:
- `Use embedded subtitles`
- Default value: `true`
- When disabled, embedded subtitles will not be considered for extraction (text subtitles), or playback (all embedded subtitles)
- `Extract and use embedded (text) subtitles`
- Default value: `false`
- When enabled, embedded text subtitles will be periodically extracted, and considered for playback
- Add `sub_language` and `sub_language_tag` fields to search index
- Add `/iptv` request logging in its own log category at debug level
- Add channel guide (XMLTV) template system
- Templates should be copied from `_channel.sbntxt`, `_movie.sbntxt`, `_episode.sbntxt`, `_musicVideo.sbntxt`, `_song.sbntxt`, or `_otherVideo.sbntxt` which are located in the config subfolder `templates/channel-guide`
- Copy the file, remove the leading underscore from the name, and only make edits to the copied file
- The default templates will be extracted and overwritten every time ErsatzTV is started
- The templates use [scribian](https://github.com/scriban/scriban/tree/master/doc) template syntax
- The templates contain comments describing which fields are available for use in the templates
- Add *experimental* and *incomplete* `Images` library kind
- Image libraries have fallback metadata added like Other Video libraries (every folder is a tag)
- Image library items currently default to a duration of 15 seconds
- The `Media` > `Images` page can be used to configure image durations at a folder level
- Child folders with unset durations will inherit the closest ancestor's duration
- Add *experimental* new streaming mode `HLS Segmenter V2`
- In my initial testing, this streaming mode produces significantly fewer playback warnings/errors
- If it tests well for others, it *may* replace the current `HLS Segmenter` in a future release
- Add setting to change XMLTV data from `Local` time zone to `UTC`
- This is needed because some clients (incorrectly) ignore time zone specifier and require UTC times
- Support `.ogv` video files in local libraries
### Fixed
- Fix antiforgery error caused by reusing existing browser tabs across docker container restarts
- Data protection keys will now be persisted under ErsatzTV's config folder instead of being recreated at startup
- Fix bug updating/replacing Jellyfin movies
- A deep scan can be used to fix all movies, otherwise any future updates made to JF movies will correctly sync to ETV
- Automatically generate JWT tokens to allow channel previews of protected streams
- Fix bug applying music video fallback metadata
- Fix playback of media items with no audio streams
- Fix timestamp continuity in `HLS Segmenter` sessions
- This should make *some* clients happier
- Fix `Other Video`, `Song` and `Image` fallback metadata tags to always include parent folder (folder added to library)
- Allow playback of items with any positive duration, including less than one second
- Fix VAAPI transcoding of OTA content containing A53 CC data
- Fix AV1 software decoder priority (`libdav1d`, `libaom-av1`, `av1`)
- Fix some stream failures caused by loudnorm filter
- Fix multi-collection editor improperly disabling collections/smart collections that haven't already been added to the multi-collection
- Fix path replacement logic when media server paths use inconsistent casing (e.g. `\\SERVERNAME` AND `\\ServerName`)
- Fix *many* search queries, including actors with the name `Will`
- Fix sqlite `database is locked` error that would crash ETV on startup after search index corruption
- Fix bug where replacing files in Plex would be missed by subsequent ETV library scans
- This fix will require a one-time re-scan of each Plex library in full
- After the initial full scan, incremental scans will behave as normal
- Fix edge case where some local episodes, music videos, other videos, songs, images would not automatically be restored from trash
- Fix `MPEG-TS` playback when JWT tokens are enabled for streaming endpoints
### Changed
- Log search index updates under scanner category at debug level, to indicate a potential cause for the UI being out of date
- Batch search index updates to keep pace with library scans
- Previously, search index updates would slowly process over minutes/hours after library scans completed
- Search index updates should now complete at the same time as library scans
- Do not unnecessarily update the search index during media server library scans
- Use different library for reading song metadata that supports multiple tag entries
- Update `/iptv` routing to make UI completely inaccessible from that path prefix
- Use CUDA 11 instead of CUDA 12 in NVIDIA docker image to significantly lower required driver version
- Allow block durations with 5-minute increments (e.g., 5 min, 10 min, 15 min, etc.)
## [0.8.5-beta] - 2024-01-30
### Added
- Respect browser's `Accept-Language` header for date time display
- Add new schedule item setting `Fill With Group Mode`
- This setting is only available when a `Collection`, `Multi-Collection` or `Smart Collection` is scheduled with `Duration` or `Multiple` playout modes
- Use this setting when you want to schedule a collection containing groups (show or artists), with only videos from a single group (show or artist) being used in each rotation
- The options are `None`, `Ordered Groups` and `Shuffled Groups`:
- `None`: no change to scheduling behavior - all groups (shows and artists) will be shuffled/ordered together
- `Ordered Groups`: each time this item is scheduled, the entire `Duration` or `Multiple` will be filled with a single group, and the groups will rotate in a fixed order
- `Shuffled Groups`: each time this item is scheduled, the entire `Duration` or `Multiple` will be filled with a single group, and the groups will rotate in a shuffled order
- Add new playout type `External Json`
- Use this playout type when you want to manage the channel schedule using DizqueTV
- You must point ErsatzTV to the channel number json file from DizqueTV, e.g. `channels/1.json`
- For playback, ErsatzTV will first check for the appropriate media file file locally
- If found, ErsatzTV will run ffprobe to get statistics immediately before streaming from disk
- When local files are unavailable, ErsatzTV must be logged into the same Plex server as DizqueTV
- ErsatzTV will ask Plex for statistics immediately before streaming from Plex
- Add new *experimental* playout type `Block`
- **This playout type is under active development and updates may reset or delete related playout data**
- Many planned features are missing, incomplete, or result in errors. This is expected.
- Block playouts consist of:
- `Blocks` - ordered list of items to play within the specified duration
- `Templates` - a generic "day" that consists of blocks scheduled at specific times
- `Playout Templates` - templates to schedule using the specified criteria. Only one template will be selected each day
- Much more to come on this feature as development continues
- Show chapter markers in movie and episode media info
- Add two new API endpoints for interacting with transcoding sessions (MPEG-TS and HLS Segmenter):
- GET `/api/sessions`
- Show brief info about all active sessions
- DELETE `/api/session/{channel-number}`
- Stop the session for the given channel number
- Add channel preview (web-based video player)
- Channels MUST use `H264` video format and `AAC` audio format
- Channels MUST use `MPEG-TS` or `HLS Segmenter` streaming modes
- Since `MPEG-TS` uses `HLS Segmenter` under the hood, the preview player will use `HLS Segmenter`, so it's not 100% equivalent, but it should be representative
- Add button to stop transcoding session for each channel that has an active session
- Add more log levels to `Settings` page, allowing more specific debug logging as needed
- Default Minimum Log Level (applies when no other categories/level overrides match)
- Scanning Minimum Log Level
- Scheduling Minimum Log Level
- Streaming Minimum Log Level
### Fixed
- Fix error loading path replacements when using MySql
- Fix tray icon shortcut to open logs folder on Windows
- Unlock playout when playout build fails
- Ignore errors deleting old HLS segments; this should improve stream reliability
- Update show year when changed within Plex
- Fix crop scale behavior with NVIDIA, QSV acceleration
- Fix bug that corrupted uploaded images (watermarks, channel logos)
- Re-uploading images should fix them
- Recreate XMLTV channel list (including logos) when channels are edited in ErsatzTV
- This bug caused the ErsatzTV logo to be used instead of channel logos in some cases
- Update drop down search results in main search bar when items are created/edited/removed
- Fix green line at bottom of video when NVIDIA accel is used with intermittent watermark
- Fix error starting streaming session when subtitles are still being extracted for the current item
### Changed
- Upgrade from .NET 7 to .NET 8
- In schedule items, disambiguate seasons from shows with the same title by including show year
- Old format: `Show Title (Season Number)`
- New format: `Show Title (Show Year) - Season Number`
- Remove FFmpeg Profile `Normalize Loudness` option `dynaudnorm` as it often caused streams to fail to start
- Disable loudness normalization by default in new FFmpeg Profiles
- Use AAC audio format by default in new FFmpeg Profiles
## [0.8.4-beta] - 2023-12-02
### Fixed
- Fix playout builder crash with improperly configured pad filler preset
- Properly validate filler preset mode pad to require `filler pad to nearest minute` value
- Fix bug where previously-synchronized collection tags would disappear
- This bug affected Jellyfin, Emby and Plex collections
- Fix detection of AMF hardware acceleration on Windows
## [0.8.3-beta] - 2023-11-22
### Added
- Add `Scaling Behavior` option to FFmpeg Profile
- `Scale and Pad`: the default behavior and will maintain aspect ratio of all content
- `Stretch`: a new mode that will NOT maintain aspect ratio when normalizing source content to the desired resolution
- `Crop`: a new mode that will scale beyond the desired resolution (maintaining aspect ratio), and crop to desired resolution
- **This mode does NOT detect black and intelligently crop**
- The goal is to fill the canvas by over-scaling and cropping, instead of minimally scaling and padding
- Include `inputstream.ffmpegdirect` properties in channels.m3u when requested by Kodi
- Log playout item title and path when starting a stream
- This will help with media server libraries where the URL passed to ffmpeg doesn't indicate which file is streaming
- Add QSV Capabilities to Troubleshooting page
- Add `language_tag` and `seconds` fields to search index
- Allow synchronizing Plex `TV Show` libraries that use `Personal Media Shows` agent
- Include Noto CJK Fonts in docker images to support those characters in generated subtitles like songs and music video credits
- Support show fallback metadata with folder names like `Show.Name(1992)`
### Fixed
- Fix playout bug that caused some schedule items with fixed start times to be pushed to the next day
- Fix playout bug that prevented padded durations from fitting within a schedule item of the same duration
- For example, filler that padded to 30 minutes would often not fit in a 30 minute duration schedule item
- Fix VAAPI transcoding 8-bit source content to 10-bit
- Fix NVIDIA subtitle scaling when `scale_npp` filter is unavailable
- Remove ffmpeg and ffprobe as required dependencies for scanning media server libraries
- Note that ffmpeg is still *always* required for playback to work
- Fix PGS subtitle pixel format with Intel VAAPI
- Fix some cases where `Copy` button would fail to copy to clipboard
- Fix some cases where ffmpeg process would remain running after properly closing ErsatzTV
- Fix QSV HLS segment duration
- This behavior caused extremely slow QSV stream starts
- Fix displaying multiple languages in UI for movies, artists, shows
- Fix MySQL queries that could fail during media server library scans
- Fix scanning Jellyfin libraries when library options and/or path infos are not returned from Jellyfin API
- Fix error indexing music videos in `File Not Found` state
- Fix bug scheduling duration filler when filler collection contains item with zero duration
- Fix bug displaying television seasons for shows that have no year metadata
### Changed
- Upgrade ffmpeg to 6.1, which is now *required* for all installs
- Use new ffmpeg throttling method to minimize cpu/gpu use without impacting audio normalization
- Change FFmpeg Profile `Normalize Loudness` setting from checkbox to dropdown
- `Off`: do not normalize loudness
- `loudnorm`: use `loudnorm` filter to normalize loudness (generally higher CPU use)
- `dynaudnorm`: use `dynaudnorm` filter to normalize loudness (generally lower CPU use)
- Jellyfin collection scanning will no longer happen after every (automatic or forced) library scan
- Automatic/periodic scans will check collections one time after all libraries have been scanned
- There is a new table in the `Media` > `Libraries` page with a button to manually re-scan Jellyfin collections as needed
- In FFmpeg Profile editor, only display hardware acceleration kinds that are supported by the configured ffmpeg
- Test QSV acceleration if configured, and fallback to software mode if test fails
- Detect QSV capabilities on Linux (supported decoders, encoders)
- Use hardware acceleration for error messages/offline messages
- Try to parse season number from season folder when Jellyfin does not provide season number
- This *may* fix issues where Jellyfin libraries show all season numbers as 0 (specials)
- Rework Plex collection scanning
- Automatic/periodic scans will check collections one time after all libraries have been scanned
- There is a table in the `Media` > `Libraries` page with a button to manually re-scan Plex collections as needed
- Plex smart collections will now be synchronized as tags, similar to other Plex collections
## [0.8.2-beta] - 2023-09-14
### Added
- Automatically rebuild search index after improper shutdown
@@ -46,7 +810,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Only allow a single instance of ErsatzTV to run
- This fixes some cases where the search index would become unusable
- Fix VAAPI rate control mode capability check
- Fix VAAPI rate control mode capability check
### Changed
- Rework startup process to show UI as early as possible
@@ -92,14 +856,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `MKV` output format: stream copy any embedded subtitles
### Fixed
- Fix extracting embedded text subtitles that had been incompletely extracted in the past
- Fix extracting embedded text subtitles that had been incompletely extracted in the past
- Fix fallback filler looping by forcing software mode for this content
- Other content will still use hardware acceleration as configured
- Hardware-accelerated fallback filler may be re-enabled in the future
- Fix playout building when shuffle in order is used with a single media item
- Fix pgs subtitle burn in from media server libraries
- Fix subtitle and watermark overlays with RadeonSI VAAPI driver
- Fix NVIDIA pipeline to use hardware-accelerated decoder with 8-bit h264 content
- Fix NVIDIA pipeline to use hardware-accelerated decoder with 8-bit h264 content
### Changed
- Timeout playout builds after 2 minutes; this should prevent playout bugs from blocking other functionality
@@ -149,7 +913,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Use Poster artwork for XMLTV if available
- If Poster artwork is unavailable, use Thumbnail
- Improve XMLTV response time by caching data as playouts are updated
- Improve XMLTV response time by caching data as playouts are updated
## [0.7.6-beta] - 2023-03-24
### Added
@@ -243,13 +1007,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed
- Merge generated `Other Video` folder tags with tags from sidecar NFO
- Prioritize audio streams that are flagged as "default" when multiple candidate streams are available
- For example, a video with a stereo commentary track and a stereo "default" track will now prefer the "default" track
- For example, a video with a stereo commentary track and a stereo "default" track will now prefer the "default" track
## [0.7.3-beta] - 2023-01-25
### Added
- Attempt to release memory periodically
- Add OpenID Connect (OIDC) support (e.g. Keycloak, Authelia, Auth0)
- This only protects the management UI; all streaming endpoints will continue to allow anonymous access
- This only protects the management UI; all streaming endpoints will continue to allow anonymous access
- This can be configured with the following env vars (note the double underscore separator `__`)
- `OIDC__AUTHORITY`
- `OIDC__CLIENTID`
@@ -267,7 +1031,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Day of week
- Day of month
- Month
- The lowest priority (bottom) item will always match all parameters, and can be considered a "default" or "fallback" schedule
- The lowest priority (bottom) item will always match all parameters, and can be considered a "default" or "fallback" schedule
### Fixed
- Fix schedule editor crashing due to bad music video artist data
@@ -299,7 +1063,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix song playback with VAAPI and NVENC
- Fix edge case where some local movies would not automatically be restored from trash
- Fix synchronizing Jellyfin and Emby collection items
- Fix saving some external subtitle records to database
- Fix saving some external subtitle records to database
### Changed
- Upgrade to dotnet 7
@@ -377,7 +1141,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Add music video credits template system
- Templates are selected in each channel's settings
- Templates are selected in each channel's settings
- Templates should be copied from `_default.ass.sbntxt` which is located in the config subfolder `templates/music-video-credits`
- Copy the file, give it any name ending with `.ass.sbntext`, and only make edits to the copied file
- The default template will be extracted and overwritten every time ErsatzTV is started
@@ -452,7 +1216,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Replace `setsar` filter with `setdar` filter
- `setsar` caused issues scaling between two different aspect ratios
- For example, some 4:3 content would appear stretched when scaled to a 16:9 resolution
- `setdar` is now only used when aspect ratios match
- `setdar` is now only used when aspect ratios match
- Prioritize aspect ratio from container when video stream contains conflicting aspect ratio
- This is usually caused by bad authoring, but the change should improve scaling behavior for edge cases
@@ -555,7 +1319,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Reduce memory use due to library scan operations
- Fix some instances of filler getting "stuck" when a filler item is encountered that's too long for the gap
- Properly ignore Plex `Other Videos` libraries (`movie` libraries where agent is `com.plexapp.agents.none`)
- Fix `Custom Title` for schedule items with `One`, `Multiple` and `Flood` playout modes
- Fix `Custom Title` for schedule items with `One`, `Multiple` and `Flood` playout modes
- Fix scheduling bug where flood items would sometimes fail to continue after midnight
### Added
@@ -585,7 +1349,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix occasional erroneous log messages when HLS channel playback times out because all clients have left
- Fix fallback filler playback
- Fix stream continuity when error messages are displayed
- Fix duplicate scanning within `Other Video` libraries (i.e. folders would be scanned multiple times)
- Fix duplicate scanning within `Other Video` libraries (i.e. folders would be scanned multiple times)
### Added
- Add `show_genre` and `show_tag` to search index for seasons and episodes
@@ -673,7 +1437,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix setting VAAPI driver name
- Fix ffmpeg troubleshooting reports
- Fix bug where filler would behave as if it were configured to pad even though a different mode was selected
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
- Fix bug where mid-roll count filler would skip scheduling the final chapter in an episode
### Added
- Add `Empty Trash` button to `Trash` page
@@ -980,7 +1744,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- When streaming is attempted during an unscheduled gap, the resulting video will be determined using the following priority:
- Channel fallback filler
- Global fallback filler
- Generated `Channel Is Offline` error message video
- Generated `Channel Is Offline` error message video
### Changed
- Allow per-episode folders for local show libraries
@@ -1088,7 +1852,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.1.0-alpha] - 2021-10-08
### Added
- Add *experimental* streaming mode `HLS Segmenter` (most similar to `HLS Hybrid`)
- Add *experimental* streaming mode `HLS Segmenter` (most similar to `HLS Hybrid`)
- This mode is intended to increase client compatibility and reduce issues at program boundaries
- If you want the temporary transcode files to be located on a particular drive, the docker path is `/root/.local/share/etv-transcode`
- Store frame rate with media statistics; this is needed to support HLS Segmenter
@@ -1124,7 +1888,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- See `Logs` for unmatched item details
- Trakt lists can only be scheduled by using Smart Collections
- Add seasons to search index
- This is needed because Trakt lists can contain seasons
- This is needed because Trakt lists can contain seasons
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
### Fixed
@@ -1476,7 +2240,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Limit channels to one playout per channel
- Though more than one playout was previously possible it was unsupported and unlikely to work as expected, if at all
- A future release may make this possible again
## [0.0.32-prealpha] - 2021-04-09
### Added
- `Add All To Collection` button to quickly add all search results to a collection
@@ -1741,7 +2505,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.2-beta...HEAD
[Unreleased]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.4.0...HEAD
[25.4.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.3.1...v25.4.0
[25.3.1]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.3.0...v25.3.1
[25.3.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.2.0...v25.3.0
[25.2.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v25.1.0...v25.2.0
[25.1.0]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.8-beta...v25.1.0
[0.8.8-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.7-beta...v0.8.8-beta
[0.8.7-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.6-beta...v0.8.7-beta
[0.8.6-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.5-beta...v0.8.6-beta
[0.8.5-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.4-beta...v0.8.5-beta
[0.8.4-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.3-beta...v0.8.4-beta
[0.8.3-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.2-beta...v0.8.3-beta
[0.8.2-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.1-beta...v0.8.2-beta
[0.8.1-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.8.0-beta...v0.8.1-beta
[0.8.0-beta]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.7.9-beta...v0.8.0-beta
@@ -1860,4 +2635,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[0.0.5-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.4-prealpha...v0.0.5-prealpha
[0.0.4-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.3-prealpha...v0.0.4-prealpha
[0.0.3-prealpha]: https://github.com/ErsatzTV/ErsatzTV/compare/v0.0.1-prealpha...v0.0.3-prealpha
[0.0.1-prealpha]: https://github.com/ErsatzTV/ErsatzTV/releases/tag/v0.0.1-prealpha
[0.0.1-prealpha]: https://github.com/ErsatzTV/ErsatzTV/releases/tag/v0.0.1-prealpha

View File

@@ -1,5 +1,6 @@
<Project>
<PropertyGroup>
<InformationalVersion>develop</InformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>

View File

@@ -1,6 +1,7 @@
#![windows_subsystem = "windows"]
use special_folder::SpecialFolder;
use std::env;
use std::fs;
use std::os::windows::process::CommandExt;
use std::process::Child;
@@ -21,11 +22,16 @@ fn main() {
let (tx, rx) = mpsc::channel();
tray.add_menu_item("Launch Web UI", || {
let ui_port = env::var("ETV_UI_PORT")
.ok()
.and_then(|val| val.parse::<u16>().ok())
.unwrap_or(8409);
let _ = Command::new("cmd")
.creation_flags(CREATE_NO_WINDOW)
.arg("/C")
.arg("start")
.arg("http://localhost:8409")
.arg(format!("http://localhost:{}", ui_port))
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
@@ -43,10 +49,7 @@ fn main() {
None => {}
Some(folder) => {
fs::create_dir_all(folder).unwrap();
let _ = Command::new("cmd")
.creation_flags(CREATE_NO_WINDOW)
.arg("/C")
.arg("start")
let _ = Command::new("explorer.exe")
.arg(folder)
.stdin(Stdio::null())
.stdout(Stdio::null())

View File

@@ -29,12 +29,12 @@ internal static class Mapper
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Map(lang => allCultures.Filter(ci => string.Equals(
ci.ThreeLetterISOLanguageName,
lang,
StringComparison.OrdinalIgnoreCase)))
.Flatten()
.Distinct()
.ToList();
}
}

View File

@@ -24,7 +24,7 @@ public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<Artist
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
List<string> languageCodes = await _searchRepository.GetAllThreeLetterLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));

View File

@@ -0,0 +1,17 @@
using System.Net;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Artworks;
public record ArtworkContentTypeModel(string Path, string ContentType)
{
public static readonly ArtworkContentTypeModel None = new(string.Empty, string.Empty);
public bool IsExternalUrl => Artwork.IsExternalUrl(Path);
public bool HasContentType => !string.IsNullOrWhiteSpace(ContentType);
public string UrlWithContentType => string.IsNullOrWhiteSpace(ContentType)
? Path
: $"{Path}?contentType={WebUtility.UrlEncode(ContentType)}";
}

View File

@@ -0,0 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Artworks;
public record GetArtwork(int Id) : IRequest<Either<BaseError, Artwork>>;

View File

@@ -0,0 +1,42 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Artworks;
public class GetArtworkHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetArtwork, Either<BaseError, Artwork>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, Artwork>> Handle(
GetArtwork request,
CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Artwork> artwork = await dbContext.Artwork
.AsNoTracking()
.SelectOneAsync(a => a.Id, a => a.Id == request.Id)
.MapT(Project);
return artwork.ToEither(BaseError.New("Artwork not found"));
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
private static Artwork Project(Artwork artwork) =>
new()
{
Id = artwork.Id,
Path = artwork.Path,
ArtworkKind = artwork.ArtworkKind
};
}

View File

@@ -1,4 +1,6 @@
using ErsatzTV.Core.Domain;
using System.Net;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
@@ -9,9 +11,12 @@ public record ChannelViewModel(
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutMode PlayoutMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
@@ -19,4 +24,12 @@ public record ChannelViewModel(
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate);
string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode,
ChannelTranscodeMode TranscodeMode,
ChannelIdleBehavior IdleBehavior,
bool IsEnabled,
bool ShowInEpg)
{
public string WebEncodedName => WebUtility.UrlEncode(Name);
}

View File

@@ -1,22 +1,30 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
public record CreateChannel
(
public record CreateChannel(
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutMode PlayoutMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode,
ChannelTranscodeMode TranscodeMode,
ChannelIdleBehavior IdleBehavior,
bool IsEnabled,
bool ShowInEpg) : IRequest<Either<BaseError, CreateChannelResult>>;

View File

@@ -1,114 +1,116 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels;
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
public class CreateChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ISearchTargets searchTargets)
: IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
private async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
searchTargets.SearchTargetsChanged();
await workerChannel.WriteAsync(new RefreshChannelList());
return new CreateChannelResult(channel.Id);
}
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredAudioLanguage(request),
ValidatePreferredSubtitleLanguage(request),
await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request))
.Apply(
(
name,
number,
ffmpegProfileId,
preferredAudioLanguageCode,
preferredSubtitleLanguageCode,
watermarkId,
fillerPresetId) =>
.Apply((
name,
number,
ffmpegProfileId,
watermarkId,
fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo?.Path))
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
string logo = request.Logo.Path;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
logo = logo.Replace("iptv/logos/", string.Empty);
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredAudioLanguageCode = preferredAudioLanguageCode,
PreferredAudioTitle = request.PreferredAudioTitle,
PreferredSubtitleLanguageCode = preferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate
};
artwork.Add(
new Artwork
{
Path = logo,
ArtworkKind = ArtworkKind.Logo,
OriginalContentType = !string.IsNullOrEmpty(request.Logo.ContentType)
? request.Logo.ContentType
: null,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
Group = request.Group,
Categories = request.Categories,
FFmpegProfileId = ffmpegProfileId,
PlayoutMode = request.PlayoutMode,
StreamingMode = request.StreamingMode,
Artwork = artwork,
StreamSelectorMode = request.StreamSelectorMode,
StreamSelector = request.StreamSelector,
PreferredAudioLanguageCode = request.PreferredAudioLanguageCode,
PreferredAudioTitle = request.PreferredAudioTitle,
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode,
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
SongVideoMode = request.SongVideoMode,
TranscodeMode = request.TranscodeMode,
IdleBehavior = request.IdleBehavior,
IsEnabled = request.IsEnabled,
ShowInEpg = request.IsEnabled && request.ShowInEpg
};
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
return channel;
});
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private static Validation<BaseError, string> ValidatePreferredAudioLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredAudioLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred audio language code is invalid");
private static Validation<BaseError, string> ValidatePreferredSubtitleLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredSubtitleLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred subtitle language code is invalid");
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
CreateChannel createChannel)
@@ -170,8 +172,7 @@ public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseEr
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
.Map(o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}

View File

@@ -1,6 +1,7 @@
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -12,24 +13,26 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ISearchTargets _searchTargets;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeleteChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem)
ILocalFileSystem localFileSystem,
ISearchTargets searchTargets)
{
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, Unit>> Handle(DeleteChannel request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await ChannelMustExist(dbContext, request);
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
return await validation.Apply(c => DoDeletion(dbContext, c, cancellationToken));
}
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
@@ -37,6 +40,8 @@ public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseEr
dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync(cancellationToken);
_searchTargets.SearchTargetsChanged();
// delete channel data from channel guide cache
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
if (_localFileSystem.FileExists(cacheFile))

View File

@@ -1,11 +1,18 @@
using System.Data.Common;
using System.Net;
using System.Xml;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Iptv;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Scriban;
using Scriban.Runtime;
using WebMarkupMin.Core;
namespace ErsatzTV.Application.Channels;
@@ -13,16 +20,19 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelListHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelListHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem)
ILocalFileSystem localFileSystem,
ILogger<RefreshChannelListHandler> logger)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_logger = logger;
}
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
@@ -31,41 +41,67 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "channel.sbntxt");
// fall back to default template
if (!_localFileSystem.FileExists(templateFileName))
{
templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_channel.sbntxt");
}
// fail if file doesn't exist
if (!_localFileSystem.FileExists(templateFileName))
{
_logger.LogError(
"Unable to generate channel list without template file {File}; please restart ErsatzTV",
templateFileName);
return;
}
var minifier = new XmlMinifier(
new XmlMinificationSettings
{
MinifyWhitespace = true,
RemoveXmlComments = true,
CollapseTagsWithoutContent = true
});
string text = await File.ReadAllTextAsync(templateFileName, cancellationToken);
var template = Template.Parse(text, templateFileName);
var templateContext = new XmlTemplateContext();
await using RecyclableMemoryStream ms = _recyclableMemoryStreamManager.GetStream();
await using var xml = XmlWriter.Create(
ms,
new XmlWriterSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment });
await foreach (ChannelResult channel in GetChannels(dbContext).WithCancellation(cancellationToken))
{
await xml.WriteStartElementAsync(null, "channel", null);
await xml.WriteAttributeStringAsync(null, "id", null, $"{channel.Number}.etv");
bool hasLogo = !string.IsNullOrWhiteSpace(channel.ArtworkPath);
bool hasExternalLogo = hasLogo && Artwork.IsExternalUrl(channel.ArtworkPath);
await xml.WriteStartElementAsync(null, "display-name", null);
await xml.WriteStringAsync($"{channel.Number} {channel.Name}");
await xml.WriteEndElementAsync(); // display-name (number and name)
await xml.WriteStartElementAsync(null, "display-name", null);
await xml.WriteStringAsync(channel.Number);
await xml.WriteEndElementAsync(); // display-name (number)
await xml.WriteStartElementAsync(null, "display-name", null);
await xml.WriteStringAsync(channel.Name);
await xml.WriteEndElementAsync(); // display-name (name)
foreach (string category in GetCategories(channel.Categories))
var data = new
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(category);
await xml.WriteEndElementAsync(); // category
}
ChannelId = ChannelIdentifier.FromNumber(channel.Number),
ChannelIdLegacy = ChannelIdentifier.LegacyFromNumber(channel.Number),
ChannelNumber = channel.Number,
ChannelName = channel.Name,
ChannelCategories = GetCategories(channel.Categories),
ChannelHasExternalArtwork = hasExternalLogo,
ChannelHasArtwork = hasLogo,
ChannelArtworkPath = channel.ArtworkPath,
ChannelNameEncoded = WebUtility.UrlEncode(channel.Name)
};
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, GetIconUrl(channel));
await xml.WriteEndElementAsync(); // icon
var scriptObject = new ScriptObject();
scriptObject.Import(data);
templateContext.PushGlobal(scriptObject);
await xml.WriteEndElementAsync(); // channel
string result = await template.RenderAsync(templateContext);
MarkupMinificationResult minified = minifier.Minify(result);
await xml.WriteRawAsync(minified.MinifiedContent);
}
await xml.FlushAsync();
@@ -82,7 +118,7 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
const string QUERY = @"select C.Number, C.Name, C.Categories, A.Path as ArtworkPath
from Channel C
left outer join Artwork A on C.Id = A.ChannelId and A.ArtworkKind = 2
where C.Id in (select ChannelId from Playout)
where C.Id in (select ChannelId from Playout) and C.IsEnabled = 1 and C.ShowInEPG = 1
order by CAST(C.Number as double)";
// TODO: this needs to be fixed for sqlite/mariadb
@@ -106,11 +142,6 @@ public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
.Distinct()
.ToList();
private static string GetIconUrl(ChannelResult channel) =>
string.IsNullOrWhiteSpace(channel.ArtworkPath)
? "{RequestBase}/iptv/images/ersatztv-500.png{AccessTokenUri}"
: $"{{RequestBase}}/iptv/logos/{channel.ArtworkPath}.jpg{{AccessTokenUri}}";
// ReSharper disable once ClassNeverInstantiated.Local
private sealed record ChannelResult(string Number, string Name, string Categories, string ArtworkPath);
}

View File

@@ -1,23 +1,31 @@
using ErsatzTV.Core;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
public record UpdateChannel
(
public record UpdateChannel(
int ChannelId,
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
ArtworkContentTypeModel Logo,
ChannelStreamSelectorMode StreamSelectorMode,
string StreamSelector,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
ChannelPlayoutMode PlayoutMode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode,
ChannelTranscodeMode TranscodeMode,
ChannelIdleBehavior IdleBehavior,
bool IsEnabled,
bool ShowInEpg) : IRequest<Either<BaseError, ChannelViewModel>>;

View File

@@ -1,9 +1,9 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using System.Threading.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -12,26 +12,19 @@ using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels;
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
public class UpdateChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ISearchTargets searchTargets)
: IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdateChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory)
{
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, c => ApplyUpdateRequest(dbContext, c, request));
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
@@ -41,43 +34,75 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
c.Group = update.Group;
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId;
c.StreamSelectorMode = update.StreamSelectorMode;
c.StreamSelector = update.StreamSelector;
c.PreferredAudioLanguageCode = update.PreferredAudioLanguageCode;
c.PreferredAudioTitle = update.PreferredAudioTitle;
c.PreferredSubtitleLanguageCode = update.PreferredSubtitleLanguageCode;
c.SubtitleMode = update.SubtitleMode;
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
c.Artwork ??= new List<Artwork>();
c.SongVideoMode = update.SongVideoMode;
c.TranscodeMode = update.TranscodeMode;
c.IdleBehavior = update.IdleBehavior;
c.IsEnabled = update.IsEnabled;
c.ShowInEpg = update.IsEnabled && update.ShowInEpg;
c.Artwork ??= [];
if (!string.IsNullOrWhiteSpace(update.Logo))
if (!string.IsNullOrWhiteSpace(update.Logo?.Path))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
string logo = update.Logo.Path;
if (logo.StartsWith("iptv/logos/", StringComparison.Ordinal))
{
logo = logo.Replace("iptv/logos/", string.Empty);
}
maybeLogo.Match(
artwork =>
Option<Artwork> maybeLogo = c.Artwork.Where(a => a.ArtworkKind == ArtworkKind.Logo).HeadOrNone();
foreach (Artwork artwork in maybeLogo)
{
artwork.Path = logo;
artwork.OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
? update.Logo.ContentType
: null;
artwork.DateUpdated = DateTime.UtcNow;
}
if (maybeLogo.IsNone)
{
var artwork = new Artwork
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
Path = logo,
OriginalContentType = !string.IsNullOrEmpty(update.Logo.ContentType)
? update.Logo.ContentType
: null,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
}
}
else
{
await dbContext.Entry(c)
.Collection(channel => channel.Artwork)
.LoadAsync();
foreach (Artwork artwork in c.Artwork.Where(x => x.ArtworkKind is ArtworkKind.Logo).ToList())
{
c.Artwork.Remove(artwork);
dbContext.Artwork.Remove(artwork);
}
}
c.PlayoutMode = update.PlayoutMode;
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
searchTargets.SearchTargetsChanged();
if (c.SubtitleMode != ChannelSubtitleMode.None)
{
Option<Playout> maybePlayout = await dbContext.Playouts
@@ -85,18 +110,19 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
foreach (Playout playout in maybePlayout)
{
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
await workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
}
}
await workerChannel.WriteAsync(new RefreshChannelList());
return ProjectToViewModel(c);
}
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredAudioLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
await ValidateNumber(dbContext, request))
.Apply((channelToUpdate, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
@@ -131,11 +157,4 @@ public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseEr
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredAudioLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredAudioLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred audio language code is invalid");
}

View File

@@ -1,4 +1,5 @@
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
@@ -14,8 +15,11 @@ internal static class Mapper
channel.Categories,
channel.FFmpegProfileId,
GetLogo(channel),
channel.StreamSelectorMode,
channel.StreamSelector,
channel.PreferredAudioLanguageCode,
channel.PreferredAudioTitle,
channel.PlayoutMode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
@@ -23,7 +27,12 @@ internal static class Mapper
channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode,
channel.MusicVideoCreditsMode,
channel.MusicVideoCreditsTemplate);
channel.MusicVideoCreditsTemplate,
channel.SongVideoMode,
channel.TranscodeMode,
channel.IdleBehavior,
channel.IsEnabled,
channel.ShowInEpg);
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
new(
@@ -34,9 +43,27 @@ internal static class Mapper
channel.PreferredAudioLanguageCode,
GetStreamingMode(channel));
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
new(resolution.Height, resolution.Width);
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) =>
new(resolution.Height, resolution.Width, bitrate);
private static ArtworkContentTypeModel GetLogo(Channel channel)
{
Option<Artwork> maybeArtwork = channel.Artwork
.Where(a => a.ArtworkKind == ArtworkKind.Logo)
.HeadOrNone();
foreach (Artwork artwork in maybeArtwork)
{
return artwork.IsExternalUrl()
? new ArtworkContentTypeModel(artwork.Path, string.Empty)
: new ArtworkContentTypeModel($"iptv/logos/{artwork.Path}", artwork.OriginalContentType);
}
return ArtworkContentTypeModel.None;
}
private static string GetStreamingMode(Channel channel) =>
channel.StreamingMode switch
@@ -45,6 +72,7 @@ internal static class Mapper
StreamingMode.TransportStreamHybrid => "MPEG-TS",
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
StreamingMode.HttpLiveStreamingSegmenterV2 => "HLS Segmenter V2",
_ => throw new ArgumentOutOfRangeException(nameof(channel))
};
}

View File

@@ -3,13 +3,10 @@ using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
public class GetChannelByIdHandler(IChannelRepository channelRepository)
: IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.GetChannel(request.Id)
channelRepository.GetChannel(request.Id)
.MapT(ProjectToViewModel);
}

View File

@@ -21,82 +21,100 @@ public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, O
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
FFmpegProfile ffmpegProfile = await dbContext.Channels
.Filter(c => c.Number == request.ChannelNumber)
.Include(c => c.FFmpegProfile)
.Map(c => c.FFmpegProfile)
.SingleAsync(cancellationToken);
if (!ffmpegProfile.NormalizeFramerate)
try
{
return Option<int>.None;
}
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
FFmpegProfile ffmpegProfile = await dbContext.Channels
.AsNoTracking()
.Filter(c => c.Number == request.ChannelNumber)
.Include(c => c.FFmpegProfile)
.Map(c => c.FFmpegProfile)
.SingleAsync(cancellationToken);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
if (result < 24)
if (!ffmpegProfile.NormalizeFramerate)
{
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
24,
result);
return 24;
return Option<int>.None;
}
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
if (distinct.Any())
{
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
List<Playout> playouts = await dbContext.Playouts
.AsNoTracking()
.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)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Image).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as RemoteStream).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
if (result < 24)
{
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
24,
result);
return 24;
}
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
if (distinct.Count != 0)
{
_logger.LogInformation(
"All content on channel {ChannelNumber} has the same frame rate of {FrameRate}; will not normalize",
request.ChannelNumber,
distinct[0]);
}
else
{
_logger.LogInformation(
"No content on channel {ChannelNumber} has frame rate information; will not normalize",
request.ChannelNumber);
}
}
else
catch (Exception ex)
{
_logger.LogInformation(
"No content on channel {ChannelNumber} has frame rate information; will not normalize",
_logger.LogWarning(
ex,
"Unexpected error checking frame rates on channel {ChannelNumber}",
request.ChannelNumber);
}

View File

@@ -3,5 +3,5 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels;
public record GetChannelGuide
(string Scheme, string Host, string BaseUrl, string AccessToken) : IRequest<Either<BaseError, ChannelGuide>>;
public record GetChannelGuide(string Scheme, string Host, string BaseUrl, string AccessToken)
: IRequest<Either<BaseError, ChannelGuide>>;

View File

@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Text;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -29,6 +30,12 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var hiddenChannelNumbers = dbContext.Channels
.Where(c => c.ShowInEpg == false)
.Select(c => c.Number)
.AsEnumerable()
.Select(n => $"{n}.xml")
.ToImmutableHashSet();
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
if (!_localFileSystem.FileExists(channelsFile))
@@ -36,10 +43,12 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
return BaseError.New($"Required file {channelsFile} is missing");
}
string accessTokenUri = string.Empty;
long mtime = File.GetLastWriteTime(channelsFile).Ticks;
var accessTokenUri = $"?v={mtime}";
if (!string.IsNullOrWhiteSpace(request.AccessToken))
{
accessTokenUri = $"?access_token={request.AccessToken}";
accessTokenUri += $"&amp;access_token={request.AccessToken}";
}
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
@@ -58,6 +67,11 @@ public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<Ba
continue;
}
if (hiddenChannelNumbers.Contains(Path.GetFileName(fileName)))
{
continue;
}
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
channelDataFragment = channelDataFragment

View File

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

View File

@@ -2,5 +2,10 @@ using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist
(string Scheme, string Host, string BaseUrl, string Mode, string AccessToken) : IRequest<ChannelPlaylist>;
public record GetChannelPlaylist(
string Scheme,
string Host,
string BaseUrl,
string Mode,
string UserAgent,
string AccessToken) : IRequest<ChannelPlaylist>;

View File

@@ -14,25 +14,34 @@ public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, Cha
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(
channels => new ChannelPlaylist(
request.Scheme,
request.Host,
request.BaseUrl,
channels,
request.AccessToken));
.Map(channels => new ChannelPlaylist(
request.Scheme,
request.Host,
request.BaseUrl,
channels,
request.UserAgent,
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{
var result = new List<Channel>();
foreach (Channel channel in channels)
{
if (channel.IsEnabled == false)
{
continue;
}
switch (mode.ToLowerInvariant())
{
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "segmenter-v2":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenterV2;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelResolutionAndBitrate(string ChannelNumber) : IRequest<Option<ResolutionAndBitrateViewModel>>;

View File

@@ -0,0 +1,27 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetChannelResolutionAndBitrateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetChannelResolutionAndBitrate, Option<ResolutionAndBitrateViewModel>>
{
public async Task<Option<ResolutionAndBitrateViewModel>> Handle(
GetChannelResolutionAndBitrate request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Channel> maybeChannel = await dbContext.Channels
.AsNoTracking()
.Include(c => c.FFmpegProfile)
.ThenInclude(ff => ff.Resolution)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber);
return maybeChannel.Map(c => Mapper.ProjectToViewModel(
c.FFmpegProfile.Resolution,
(int)((c.FFmpegProfile.VideoBitrate * 1000 + c.FFmpegProfile.AudioBitrate * 1000) * 1.2)));
}
}

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelStreamSelectors : IRequest<List<string>>;

View File

@@ -0,0 +1,14 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
namespace ErsatzTV.Application.Channels;
public class GetChannelStreamSelectorsHandler(ILocalFileSystem localFileSystem)
: IRequestHandler<GetChannelStreamSelectors, List<string>>
{
public Task<List<string>> Handle(GetChannelStreamSelectors request, CancellationToken cancellationToken) =>
localFileSystem.ListFiles(FileSystemLayout.ChannelStreamSelectorsFolder)
.Map(Path.GetFileName)
.ToList()
.AsTask();
}

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record ResolutionAndBitrateViewModel(int Height, int Width, int Bitrate);

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record ResolutionViewModel(int Height, int Width);

View File

@@ -0,0 +1,14 @@
using System.Net;
using Scriban;
using Scriban.Parsing;
namespace ErsatzTV.Application.Channels;
public class XmlTemplateContext : TemplateContext
{
public override TemplateContext Write(SourceSpan span, object textAsObject)
=> base.Write(span, textAsObject is string text ? WebUtility.HtmlEncode(text) : textAsObject);
public override ValueTask<TemplateContext> WriteAsync(SourceSpan span, object textAsObject)
=> base.WriteAsync(span, textAsObject is string text ? WebUtility.HtmlEncode(text) : textAsObject);
}

View File

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

View File

@@ -1,32 +0,0 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Serilog.Core;
namespace ErsatzTV.Application.Configuration;
public class UpdateGeneralSettingsHandler : IRequestHandler<UpdateGeneralSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly LoggingLevelSwitch _loggingLevelSwitch;
public UpdateGeneralSettingsHandler(
LoggingLevelSwitch loggingLevelSwitch,
IConfigElementRepository configElementRepository)
{
_loggingLevelSwitch = loggingLevelSwitch;
_configElementRepository = configElementRepository;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateGeneralSettings request,
CancellationToken cancellationToken) => await ApplyUpdate(request.GeneralSettings);
private async Task<Unit> ApplyUpdate(GeneralSettingsViewModel generalSettings)
{
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, generalSettings.MinimumLogLevel);
_loggingLevelSwitch.MinimumLevel = generalSettings.MinimumLogLevel;
return Unit.Default;
}
}

View File

@@ -16,10 +16,9 @@ public class UpdateLibraryRefreshIntervalHandler :
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(
_ => _configElementRepository.Upsert(
ConfigElementKey.LibraryRefreshInterval,
request.LibraryRefreshInterval))
.MapT(_ => _configElementRepository.Upsert(
ConfigElementKey.LibraryRefreshInterval,
request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>

View File

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

View File

@@ -0,0 +1,56 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class UpdateLoggingSettingsHandler : IRequestHandler<UpdateLoggingSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly LoggingLevelSwitches _loggingLevelSwitches;
public UpdateLoggingSettingsHandler(
LoggingLevelSwitches loggingLevelSwitches,
IConfigElementRepository configElementRepository)
{
_loggingLevelSwitches = loggingLevelSwitches;
_configElementRepository = configElementRepository;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateLoggingSettings request,
CancellationToken cancellationToken) => await ApplyUpdate(request.LoggingSettings);
private async Task<Unit> ApplyUpdate(LoggingSettingsViewModel loggingSettings)
{
await _configElementRepository.Upsert(ConfigElementKey.MinimumLogLevel, loggingSettings.DefaultMinimumLogLevel);
_loggingLevelSwitches.DefaultLevelSwitch.MinimumLevel = loggingSettings.DefaultMinimumLogLevel;
await _configElementRepository.Upsert(
ConfigElementKey.MinimumLogLevelScanning,
loggingSettings.ScanningMinimumLogLevel);
_loggingLevelSwitches.ScanningLevelSwitch.MinimumLevel = loggingSettings.ScanningMinimumLogLevel;
await _configElementRepository.Upsert(
ConfigElementKey.MinimumLogLevelScheduling,
loggingSettings.SchedulingMinimumLogLevel);
_loggingLevelSwitches.SchedulingLevelSwitch.MinimumLevel = loggingSettings.SchedulingMinimumLogLevel;
await _configElementRepository.Upsert(
ConfigElementKey.MinimumLogLevelSearching,
loggingSettings.SearchingMinimumLogLevel);
_loggingLevelSwitches.SearchingLevelSwitch.MinimumLevel = loggingSettings.SearchingMinimumLogLevel;
await _configElementRepository.Upsert(
ConfigElementKey.MinimumLogLevelStreaming,
loggingSettings.StreamingMinimumLogLevel);
_loggingLevelSwitches.StreamingLevelSwitch.MinimumLevel = loggingSettings.StreamingMinimumLogLevel;
await _configElementRepository.Upsert(
ConfigElementKey.MinimumLogLevelHttp,
loggingSettings.HttpMinimumLogLevel);
_loggingLevelSwitches.HttpLevelSwitch.MinimumLevel = loggingSettings.HttpMinimumLogLevel;
return Unit.Default;
}
}

View File

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

View File

@@ -0,0 +1,49 @@
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Configuration;
public class UpdateXmltvSettingsHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
: IRequestHandler<UpdateXmltvSettings, Either<BaseError, Unit>>
{
public async Task<Either<BaseError, Unit>> Handle(
UpdateXmltvSettings request,
CancellationToken cancellationToken)
{
int playoutDaysToBuild =
await configElementRepository
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.IfNoneAsync(2);
if (playoutDaysToBuild < request.XmltvSettings.DaysToBuild)
{
return BaseError.New(
$"XMLTV days to build ({request.XmltvSettings.DaysToBuild}) cannot be greater than Playout days to build ({playoutDaysToBuild})");
}
return await ApplyUpdate(request.XmltvSettings);
}
private async Task<Unit> ApplyUpdate(XmltvSettingsViewModel xmltvSettings)
{
await configElementRepository.Upsert(ConfigElementKey.XmltvTimeZone, xmltvSettings.TimeZone);
await configElementRepository.Upsert(ConfigElementKey.XmltvDaysToBuild, xmltvSettings.DaysToBuild);
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
foreach (string channelNumber in await dbContext.Channels.Map(c => c.Number).ToListAsync())
{
await workerChannel.WriteAsync(new RefreshChannelData(channelNumber));
}
return Unit.Default;
}
}

View File

@@ -1,8 +0,0 @@
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class GeneralSettingsViewModel
{
public LogEventLevel MinimumLogLevel { get; set; }
}

View File

@@ -0,0 +1,13 @@
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class LoggingSettingsViewModel
{
public LogEventLevel DefaultMinimumLogLevel { get; set; }
public LogEventLevel ScanningMinimumLogLevel { get; set; }
public LogEventLevel SchedulingMinimumLogLevel { get; set; }
public LogEventLevel SearchingMinimumLogLevel { get; set; }
public LogEventLevel StreamingMinimumLogLevel { get; set; }
public LogEventLevel HttpMinimumLogLevel { get; set; }
}

View File

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

View File

@@ -1,24 +0,0 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class GetGeneralSettingsHandler : IRequestHandler<GetGeneralSettings, GeneralSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetGeneralSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<GeneralSettingsViewModel> Handle(GetGeneralSettings request, CancellationToken cancellationToken)
{
Option<LogEventLevel> maybeLogLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
return new GeneralSettingsViewModel
{
MinimumLogLevel = await maybeLogLevel.IfNoneAsync(LogEventLevel.Information)
};
}
}

View File

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

View File

@@ -0,0 +1,44 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class GetLoggingSettingsHandler : IRequestHandler<GetLoggingSettings, LoggingSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetLoggingSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<LoggingSettingsViewModel> Handle(GetLoggingSettings request, CancellationToken cancellationToken)
{
Option<LogEventLevel> maybeDefaultLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevel);
Option<LogEventLevel> maybeScanningLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelScanning);
Option<LogEventLevel> maybeSchedulingLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelScheduling);
Option<LogEventLevel> maybeSearchingLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelSearching);
Option<LogEventLevel> maybeStreamingLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelStreaming);
Option<LogEventLevel> maybeHttpLevel =
await _configElementRepository.GetValue<LogEventLevel>(ConfigElementKey.MinimumLogLevelHttp);
return new LoggingSettingsViewModel
{
DefaultMinimumLogLevel = await maybeDefaultLevel.IfNoneAsync(LogEventLevel.Information),
ScanningMinimumLogLevel = await maybeScanningLevel.IfNoneAsync(LogEventLevel.Information),
SchedulingMinimumLogLevel = await maybeSchedulingLevel.IfNoneAsync(LogEventLevel.Information),
SearchingMinimumLogLevel = await maybeSearchingLevel.IfNoneAsync(LogEventLevel.Information),
StreamingMinimumLogLevel = await maybeStreamingLevel.IfNoneAsync(LogEventLevel.Information),
HttpMinimumLogLevel = await maybeHttpLevel.IfNoneAsync(LogEventLevel.Information)
};
}
}

View File

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

View File

@@ -0,0 +1,22 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class GetXmltvSettingsHandler(IConfigElementRepository configElementRepository)
: IRequestHandler<GetXmltvSettings, XmltvSettingsViewModel>
{
public async Task<XmltvSettingsViewModel> Handle(GetXmltvSettings request, CancellationToken cancellationToken)
{
Option<int> daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.XmltvDaysToBuild);
Option<XmltvTimeZone> maybeTimeZone =
await configElementRepository.GetValue<XmltvTimeZone>(ConfigElementKey.XmltvTimeZone);
return new XmltvSettingsViewModel
{
DaysToBuild = await daysToBuild.IfNoneAsync(2),
TimeZone = await maybeTimeZone.IfNoneAsync(XmltvTimeZone.Local)
};
}
}

View File

@@ -0,0 +1,7 @@
namespace ErsatzTV.Application.Configuration;
public class XmltvSettingsViewModel
{
public int DaysToBuild { get; set; }
public XmltvTimeZone TimeZone { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace ErsatzTV.Application.Configuration;
public enum XmltvTimeZone
{
Local = 0,
Utc = 1
}

View File

@@ -92,7 +92,7 @@ public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLi
toAdd,
toRemove,
toUpdate);
if (ids.Any())
if (ids.Count != 0)
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();

View File

@@ -26,7 +26,6 @@ public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmb
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}

View File

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

View File

@@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Emby;
public class
UpdateEmbyLibraryPreferencesHandler : IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
Either<BaseError, Unit>>
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;

View File

@@ -43,7 +43,6 @@ public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathR
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."));
.Map(v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
}

View File

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

View File

@@ -54,9 +54,8 @@ public class GetEmbyConnectionParametersHandler : IRequestHandler<GetEmbyConnect
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist() =>
_mediaSourceRepository.GetAllEmby().Map(list => list.HeadOrNone())
.Map(
v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
.Map(v => v.ToValidation<BaseError>(
"Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)

View File

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

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<NoWarn>VSTHRD200</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-Recommended</AnalysisLevel>
@@ -9,23 +9,25 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Bugsnag" Version="3.1.0" />
<PackageReference Include="CliWrap" Version="3.6.4" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.7.30">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="2.0.0" />
<PackageReference Include="Winista.MimeDetect" Version="1.0.1" />
<PackageReference Include="Bugsnag" Version="4.1.0" />
<PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.19.0" />
<PackageReference Include="Winista.MimeDetect" Version="1.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
<ProjectReference Include="..\ErsatzTV.Infrastructure\ErsatzTV.Infrastructure.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artists_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=artworks_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=channels_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=configuration_005Ccommands/@EntryIndexedValue">True</s:Boolean>
@@ -35,6 +36,8 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>
@@ -42,5 +45,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=subtitles_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=television_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=templates_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=troubleshooting_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=validators/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=watermarks_005Cqueries/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

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

View File

@@ -1,5 +1,7 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using static ErsatzTV.Application.FFmpegProfiles.Mapper;
namespace ErsatzTV.Application.FFmpegProfiles;
@@ -8,9 +10,13 @@ public class
CopyFFmpegProfileHandler : IRequestHandler<CopyFFmpegProfile, Either<BaseError, FFmpegProfileViewModel>>
{
private readonly IFFmpegProfileRepository _ffmpegProfileRepository;
private readonly ISearchTargets _searchTargets;
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository) =>
public CopyFFmpegProfileHandler(IFFmpegProfileRepository ffmpegProfileRepository, ISearchTargets searchTargets)
{
_ffmpegProfileRepository = ffmpegProfileRepository;
_searchTargets = searchTargets;
}
public Task<Either<BaseError, FFmpegProfileViewModel>> Handle(
CopyFFmpegProfile request,
@@ -19,9 +25,12 @@ public class
.MapT(PerformCopy)
.Bind(v => v.ToEitherAsync());
private Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request) =>
_ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name)
.Map(ProjectToViewModel);
private async Task<FFmpegProfileViewModel> PerformCopy(CopyFFmpegProfile request)
{
FFmpegProfile copy = await _ffmpegProfileRepository.Copy(request.FFmpegProfileId, request.Name);
_searchTargets.SearchTargetsChanged();
return ProjectToViewModel(copy);
}
private static Task<Validation<BaseError, CopyFFmpegProfile>> Validate(CopyFFmpegProfile request) =>
ValidateName(request).AsTask().MapT(_ => request);

View File

@@ -8,18 +8,24 @@ public record CreateFFmpegProfile(
string Name,
int ThreadCount,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -10,9 +11,13 @@ public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
@@ -23,12 +28,13 @@ public class CreateFFmpegProfileHandler :
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
}
private static async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
private async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
_searchTargets.SearchTargetsChanged();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
@@ -36,29 +42,38 @@ public class CreateFFmpegProfileHandler :
TvContext dbContext,
CreateFFmpegProfile request) =>
(ValidateName(request), ValidateThreadCount(request), await ResolutionMustExist(dbContext, request))
.Apply(
(name, threadCount, resolutionId) => new FFmpegProfile
{
Name = name,
ThreadCount = threadCount,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
VideoFormat = request.VideoFormat,
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudness = request.NormalizeLoudness,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
DeinterlaceVideo = request.DeinterlaceVideo
});
.Apply((name, threadCount, resolutionId) => new FFmpegProfile
{
Name = name,
ThreadCount = threadCount,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
VideoFormat = request.VideoFormat,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,
AllowBFrames = request.AllowBFrames,
// mpeg2video only supports 8-bit content
BitDepth = request.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
TonemapAlgorithm = request.TonemapAlgorithm,
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
DeinterlaceVideo = request.DeinterlaceVideo
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -9,23 +10,28 @@ namespace ErsatzTV.Application.FFmpegProfiles;
public class DeleteFFmpegProfileHandler : IRequestHandler<DeleteFFmpegProfile, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
public DeleteFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, Unit>> Handle(
DeleteFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, FFmpegProfile> validation = await FFmpegProfileMustExist(dbContext, request);
return await validation.Apply(p => DoDeletion(dbContext, p));
}
private static async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
private async Task<Unit> DoDeletion(TvContext dbContext, FFmpegProfile ffmpegProfile)
{
dbContext.FFmpegProfiles.Remove(ffmpegProfile);
await dbContext.SaveChangesAsync();
_searchTargets.SearchTargetsChanged();
return Unit.Default;
}

View File

@@ -9,18 +9,24 @@ public record UpdateFFmpegProfile(
string Name,
int ThreadCount,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,

View File

@@ -1,5 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@@ -10,9 +11,13 @@ public class
UpdateFFmpegProfileHandler : IRequestHandler<UpdateFFmpegProfile, Either<BaseError, UpdateFFmpegProfileResult>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory) =>
public UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, UpdateFFmpegProfileResult>> Handle(
UpdateFFmpegProfile request,
@@ -23,7 +28,7 @@ public class
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
private static async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
private async Task<UpdateFFmpegProfileResult> ApplyUpdateRequest(
TvContext dbContext,
FFmpegProfile p,
UpdateFFmpegProfile update)
@@ -31,28 +36,37 @@ public class
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.HardwareAcceleration = update.HardwareAcceleration;
p.VaapiDisplay = update.VaapiDisplay;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.ScalingBehavior = update.ScalingBehavior;
p.VideoFormat = update.VideoFormat;
p.VideoProfile = update.VideoProfile;
p.VideoPreset = update.VideoPreset;
p.AllowBFrames = update.AllowBFrames;
// mpeg2video only supports 8-bit content
p.BitDepth = update.VideoFormat == FFmpegProfileVideoFormat.Mpeg2Video
p.BitDepth = update.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: update.BitDepth;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.TonemapAlgorithm = update.TonemapAlgorithm;
p.AudioFormat = update.AudioFormat;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
p.NormalizeLoudness = update.NormalizeLoudness;
p.NormalizeLoudnessMode = update.NormalizeLoudnessMode;
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeFramerate = update.NormalizeFramerate;
p.DeinterlaceVideo = update.DeinterlaceVideo;
await dbContext.SaveChangesAsync();
_searchTargets.SearchTargetsChanged();
return new UpdateFFmpegProfileResult(p.Id);
}

View File

@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
@@ -11,13 +13,16 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
{
private readonly IConfigElementRepository _configElementRepository;
private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdateFFmpegSettingsHandler(
IConfigElementRepository configElementRepository,
ILocalFileSystem localFileSystem)
ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_localFileSystem = localFileSystem;
_workerChannel = workerChannel;
}
public Task<Either<BaseError, Unit>> Handle(
@@ -87,6 +92,26 @@ public class UpdateFFmpegSettingsHandler : IRequestHandler<UpdateFFmpegSettings,
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredAudioLanguageCode);
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegUseEmbeddedSubtitles,
request.Settings.UseEmbeddedSubtitles);
// do not extract when subtitles are not used
if (request.Settings.UseEmbeddedSubtitles == false)
{
request.Settings.ExtractEmbeddedSubtitles = false;
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegExtractEmbeddedSubtitles,
request.Settings.ExtractEmbeddedSubtitles);
// queue extracting all embedded subtitles
if (request.Settings.ExtractEmbeddedSubtitles)
{
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(Option<int>.None));
}
if (request.Settings.GlobalWatermarkId is not null)
{
await _configElementRepository.Upsert(

View File

@@ -9,18 +9,24 @@ public record FFmpegProfileViewModel(
string Name,
int ThreadCount,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution,
ScalingBehavior ScalingBehavior,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
bool NormalizeLoudness,
NormalizeLoudnessMode NormalizeLoudnessMode,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,

View File

@@ -8,6 +8,8 @@ public class FFmpegSettingsViewModel
public string FFprobePath { get; set; }
public int DefaultFFmpegProfileId { get; set; }
public string PreferredAudioLanguageCode { get; set; }
public bool UseEmbeddedSubtitles { get; set; }
public bool ExtractEmbeddedSubtitles { get; set; }
public bool SaveReports { get; set; }
public int? GlobalWatermarkId { get; set; }
public int? GlobalFallbackFillerId { get; set; }

View File

@@ -11,18 +11,24 @@ internal static class Mapper
profile.Name,
profile.ThreadCount,
profile.HardwareAcceleration,
profile.VaapiDisplay ?? "drm",
profile.VaapiDriver,
profile.VaapiDevice,
profile.QsvExtraHardwareFrames,
Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
profile.ScalingBehavior,
profile.VideoFormat,
profile.VideoProfile,
profile.VideoPreset ?? string.Empty,
profile.AllowBFrames,
profile.BitDepth,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.TonemapAlgorithm,
profile.AudioFormat,
profile.AudioBitrate,
profile.AudioBufferSize,
profile.NormalizeLoudness,
profile.NormalizeLoudnessMode,
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeFramerate,
@@ -42,16 +48,18 @@ internal static class Mapper
ffmpegProfile.Name,
ffmpegProfile.ThreadCount,
(int)ffmpegProfile.HardwareAcceleration,
ffmpegProfile.VaapiDisplay,
(int)ffmpegProfile.VaapiDriver,
ffmpegProfile.VaapiDevice,
ffmpegProfile.ResolutionId,
(int)ffmpegProfile.VideoFormat,
ffmpegProfile.VideoBitrate,
ffmpegProfile.VideoBufferSize,
(int)ffmpegProfile.TonemapAlgorithm,
(int)ffmpegProfile.AudioFormat,
ffmpegProfile.AudioBitrate,
ffmpegProfile.AudioBufferSize,
ffmpegProfile.NormalizeLoudness,
(int)ffmpegProfile.NormalizeLoudnessMode,
ffmpegProfile.AudioChannels,
ffmpegProfile.AudioSampleRate,
ffmpegProfile.NormalizeFramerate,

View File

@@ -8,7 +8,7 @@ namespace ErsatzTV.Application.FFmpegProfiles;
public class
GetFFmpegProfileByIdForApiHandler : IRequestHandler<GetFFmpegFullProfileByIdForApi,
Option<FFmpegFullProfileResponseModel>>
Option<FFmpegFullProfileResponseModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;

View File

@@ -23,6 +23,10 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegSaveReports);
Option<string> preferredAudioLanguageCode =
await _configElementRepository.GetValue<string>(ConfigElementKey.FFmpegPreferredLanguageCode);
Option<bool> useEmbeddedSubtitles =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseEmbeddedSubtitles);
Option<bool> extractEmbeddedSubtitles =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegExtractEmbeddedSubtitles);
Option<int> watermark =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegGlobalWatermarkId);
Option<int> fallbackFiller =
@@ -42,6 +46,8 @@ public class GetFFmpegSettingsHandler : IRequestHandler<GetFFmpegSettings, FFmpe
FFprobePath = await ffprobePath.IfNoneAsync(string.Empty),
DefaultFFmpegProfileId = await defaultFFmpegProfileId.IfNoneAsync(0),
SaveReports = await saveReports.IfNoneAsync(false),
UseEmbeddedSubtitles = await useEmbeddedSubtitles.IfNoneAsync(true),
ExtractEmbeddedSubtitles = await extractEmbeddedSubtitles.IfNoneAsync(false),
PreferredAudioLanguageCode = await preferredAudioLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),

View File

@@ -0,0 +1,5 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.FFmpegProfiles;
public record GetSupportedHardwareAccelerationKinds : IRequest<List<HardwareAccelerationKind>>;

View File

@@ -0,0 +1,79 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles;
public class
GetSupportedHardwareAccelerationKindsHandler : IRequestHandler<GetSupportedHardwareAccelerationKinds,
List<HardwareAccelerationKind>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IHardwareCapabilitiesFactory _hardwareCapabilitiesFactory;
public GetSupportedHardwareAccelerationKindsHandler(
IDbContextFactory<TvContext> dbContextFactory,
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
{
_dbContextFactory = dbContextFactory;
_hardwareCapabilitiesFactory = hardwareCapabilitiesFactory;
}
public async Task<List<HardwareAccelerationKind>> Handle(
GetSupportedHardwareAccelerationKinds request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, string> validation = await Validate(dbContext);
return await validation.Match(
GetHardwareAccelerationKinds,
_ => Task.FromResult(new List<HardwareAccelerationKind> { HardwareAccelerationKind.None }));
}
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(string ffmpegPath)
{
var result = new List<HardwareAccelerationKind> { HardwareAccelerationKind.None };
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Nvenc))
{
result.Add(HardwareAccelerationKind.Nvenc);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Qsv))
{
result.Add(HardwareAccelerationKind.Qsv);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Vaapi))
{
result.Add(HardwareAccelerationKind.Vaapi);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.VideoToolbox))
{
result.Add(HardwareAccelerationKind.VideoToolbox);
}
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Amf))
{
result.Add(HardwareAccelerationKind.Amf);
}
return result;
}
private static async Task<Validation<BaseError, string>> Validate(TvContext dbContext) =>
await FFmpegPathMustExist(dbContext);
private static Task<Validation<BaseError, string>> FFmpegPathMustExist(TvContext dbContext) =>
dbContext.ConfigElements.GetValue<string>(ConfigElementKey.FFmpegPath)
.FilterT(File.Exists)
.Map(maybePath => maybePath.ToValidation<BaseError>("FFmpeg path does not exist on filesystem"));
}

View File

@@ -16,5 +16,8 @@ public record CreateFillerPreset(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
int? SmartCollectionId,
int? PlaylistId,
string Expression,
bool UseChaptersAsMediaItems
) : IRequest<Either<BaseError, Unit>>;

View File

@@ -36,7 +36,10 @@ public class CreateFillerPresetHandler : IRequestHandler<CreateFillerPreset, Eit
CollectionId = request.CollectionId,
MediaItemId = request.MediaItemId,
MultiCollectionId = request.MultiCollectionId,
SmartCollectionId = request.SmartCollectionId
SmartCollectionId = request.SmartCollectionId,
PlaylistId = request.PlaylistId,
Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null,
UseChaptersAsMediaItems = request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems
};
await dbContext.FillerPresets.AddAsync(fillerPreset, cancellationToken);

View File

@@ -17,5 +17,8 @@ public record UpdateFillerPreset(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId
int? SmartCollectionId,
int? PlaylistId,
string Expression,
bool UseChaptersAsMediaItems
) : IRequest<Either<BaseError, Unit>>;

View File

@@ -37,6 +37,9 @@ public class UpdateFillerPresetHandler : IRequestHandler<UpdateFillerPreset, Eit
existing.MediaItemId = request.MediaItemId;
existing.MultiCollectionId = request.MultiCollectionId;
existing.SmartCollectionId = request.SmartCollectionId;
existing.PlaylistId = request.PlaylistId;
existing.Expression = request.FillerKind is FillerKind.MidRoll ? request.Expression : null;
existing.UseChaptersAsMediaItems = request.FillerKind is not FillerKind.Fallback && request.UseChaptersAsMediaItems;
await dbContext.SaveChangesAsync();

View File

@@ -1,4 +1,5 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Application.Filler;
@@ -16,4 +17,7 @@ public record FillerPresetViewModel(
int? CollectionId,
int? MediaItemId,
int? MultiCollectionId,
int? SmartCollectionId);
int? SmartCollectionId,
PlaylistViewModel Playlist,
string Expression,
bool UseChaptersAsMediaItems);

View File

@@ -18,5 +18,10 @@ internal static class Mapper
fillerPreset.CollectionId,
fillerPreset.MediaItemId,
fillerPreset.MultiCollectionId,
fillerPreset.SmartCollectionId);
fillerPreset.SmartCollectionId,
fillerPreset.Playlist is not null
? MediaCollections.Mapper.ProjectToViewModel(fillerPreset.Playlist)
: null,
fillerPreset.Expression,
fillerPreset.UseChaptersAsMediaItems);
}

View File

@@ -5,19 +5,17 @@ using static ErsatzTV.Application.Filler.Mapper;
namespace ErsatzTV.Application.Filler;
public class GetFillerPresetByIdHandler : IRequestHandler<GetFillerPresetById, Option<FillerPresetViewModel>>
public class GetFillerPresetByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetFillerPresetById, Option<FillerPresetViewModel>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetFillerPresetByIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<FillerPresetViewModel>> Handle(
GetFillerPresetById request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.FillerPresets
.AsNoTracking()
.Include(i => i.Playlist)
.SelectOneAsync(c => c.Id, c => c.Id == request.Id)
.MapT(ProjectToViewModel);
}

View File

@@ -16,10 +16,9 @@ public class UpdateHDHRTunerCountHandler : IRequestHandler<UpdateHDHRTunerCount,
UpdateHDHRTunerCount request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(
_ => _configElementRepository.Upsert(
ConfigElementKey.HDHRTunerCount,
request.TunerCount.ToString(CultureInfo.InvariantCulture)))
.MapT(_ => _configElementRepository.Upsert(
ConfigElementKey.HDHRTunerCount,
request.TunerCount.ToString(CultureInfo.InvariantCulture)))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.HDHR;
public record GetHDHRUUID : IRequest<Guid>;

View File

@@ -0,0 +1,23 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.HDHR;
public class GetHDHRUUIDHandler : IRequestHandler<GetHDHRUUID, Guid>
{
private readonly IConfigElementRepository _configElementRepository;
public GetHDHRUUIDHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Guid> Handle(GetHDHRUUID request, CancellationToken cancellationToken)
{
Option<Guid> maybeGuid = await _configElementRepository.GetValue<Guid>(ConfigElementKey.HDHRUUID);
return await maybeGuid.IfNoneAsync(async () =>
{
var guid = Guid.NewGuid();
await _configElementRepository.Upsert(ConfigElementKey.HDHRUUID, guid);
return guid;
});
}
}

View File

@@ -4,4 +4,5 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images;
// ReSharper disable once SuggestBaseTypeForParameter
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind) : IRequest<Either<BaseError, string>>;
public record SaveArtworkToDisk(Stream Stream, ArtworkKind ArtworkKind, string ContentType)
: IRequest<Either<BaseError, string>>;

View File

@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Images;
public record UpdateImageFolderDuration(int LibraryFolderId, double? ImageFolderDuration) : IRequest<double?>;

View File

@@ -0,0 +1,123 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Images;
public class UpdateImageFolderDurationHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<UpdateImageFolderDuration, double?>
{
public async Task<double?> Handle(UpdateImageFolderDuration request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
if (request.ImageFolderDuration.IfNone(1) < 0.01)
{
request = request with { ImageFolderDuration = 0.01 };
}
// delete entry if null
if (request.ImageFolderDuration is null)
{
await dbContext.ImageFolderDurations
.Filter(ifd => ifd.LibraryFolderId == request.LibraryFolderId)
.ExecuteDeleteAsync(cancellationToken);
}
// upsert if non-null
else
{
Option<ImageFolderDuration> maybeExisting = await dbContext.ImageFolderDurations
.SelectOneAsync(ifd => ifd.LibraryFolderId, ifd => ifd.LibraryFolderId == request.LibraryFolderId);
if (maybeExisting.IsNone)
{
var entry = new ImageFolderDuration
{
LibraryFolderId = request.LibraryFolderId
};
maybeExisting = entry;
await dbContext.ImageFolderDurations.AddAsync(entry, cancellationToken);
}
foreach (ImageFolderDuration existing in maybeExisting)
{
existing.DurationSeconds = request.ImageFolderDuration.Value;
await dbContext.SaveChangesAsync(cancellationToken);
}
}
// update all images (bfs) starting at this folder
Option<LibraryFolder> maybeFolder = await dbContext.LibraryFolders
.AsNoTracking()
.Include(lf => lf.ImageFolderDuration)
.SelectOneAsync(lf => lf.Id, lf => lf.Id == request.LibraryFolderId);
var queue = new Queue<FolderWithParentDuration>();
foreach (LibraryFolder libraryFolder in maybeFolder)
{
LibraryFolder currentFolder = libraryFolder;
// walk up to get duration, if needed
double? durationSeconds = currentFolder.ImageFolderDuration?.DurationSeconds;
while (durationSeconds is null && currentFolder?.ParentId is not null)
{
Option<LibraryFolder> maybeParent = await dbContext.LibraryFolders
.AsNoTracking()
.Include(lf => lf.ImageFolderDuration)
.SelectOneAsync(lf => lf.Id, lf => lf.Id == currentFolder.ParentId);
if (maybeParent.IsNone)
{
currentFolder = null;
}
foreach (LibraryFolder parent in maybeParent)
{
currentFolder = parent;
durationSeconds = currentFolder.ImageFolderDuration?.DurationSeconds;
}
}
queue.Enqueue(new FolderWithParentDuration(libraryFolder, durationSeconds));
}
while (queue.Count > 0)
{
(LibraryFolder currentFolder, double? parentDuration) = queue.Dequeue();
double? effectiveDuration = currentFolder.ImageFolderDuration?.DurationSeconds ?? parentDuration;
// Serilog.Log.Logger.Information(
// "Updating folder {Id} with parent duration {ParentDuration}, effective duration {EffectiveDuration}",
// currentFolder.Id,
// parentDuration,
// effectiveDuration);
// update all images in this folder
await dbContext.ImageMetadata
.Filter(im =>
im.Image.MediaVersions.Any(mv => mv.MediaFiles.Any(mf => mf.LibraryFolderId == currentFolder.Id)))
.ExecuteUpdateAsync(
setters => setters.SetProperty(im => im.DurationSeconds, effectiveDuration),
cancellationToken);
List<LibraryFolder> children = await dbContext.LibraryFolders
.AsNoTracking()
.Filter(lf => lf.ParentId == currentFolder.Id)
.Include(lf => lf.ImageFolderDuration)
.ToListAsync(cancellationToken);
// queue all children
foreach (LibraryFolder child in children)
{
queue.Enqueue(new FolderWithParentDuration(child, effectiveDuration));
}
}
return request.ImageFolderDuration;
}
private sealed record FolderWithParentDuration(LibraryFolder LibraryFolder, double? ParentDuration);
}

View File

@@ -0,0 +1,9 @@
namespace ErsatzTV.Application.Images;
public record ImageFolderViewModel(
int LibraryFolderId,
string Name,
string FullPath,
int SubfolderCount,
int ImageCount,
Option<double> DurationSeconds);

View File

@@ -0,0 +1,18 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Images;
public static class Mapper
{
public static ImageFolderViewModel ProjectToViewModel(
LibraryFolder libraryFolder,
int childCount,
int imageCount) =>
new(
libraryFolder.Id,
new DirectoryInfo(libraryFolder.Path).Name,
libraryFolder.Path,
childCount,
imageCount,
libraryFolder.ImageFolderDuration?.DurationSeconds ?? Option<double>.None);
}

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