Compare commits

..
Author SHA1 Message Date
Jason Dove d91e945124 update changelog for release v0.8.3-beta [no ci] 2023-11-22 11:36:31 -06:00
Jason DoveandGitHub 9dabffbac1 support more formats for show fallback metadata (#1514) 2023-11-21 15:52:25 -06:00
Jason DoveandGitHub d310b5c09d fix nvidia hardware decoding on windows (#1513) 2023-11-21 06:36:05 -06:00
Jason DoveandGitHub ba48b3a676 update dependencies (#1512) 2023-11-20 21:57:43 -06:00
Jason DoveandGitHub d8a51b5d6d fix season display bug (#1511) 2023-11-20 21:17:11 -06:00
Jason DoveandGitHub 97674cff89 fix bug scheduling duration filler (#1510) 2023-11-20 21:02:26 -06:00
Jason DoveandGitHub 4820615308 proper fix to the sdk mismatch (#1509) 2023-11-16 13:37:20 -06:00
Jason DoveandGitHub 1ddf27ce88 pin dotnet sdk version used in github actions (#1508) 2023-11-16 13:21:51 -06:00
Jason DoveandGitHub cd98a89acd enable docker arm builds again (#1507) 2023-11-16 13:07:49 -06:00
Jason DoveandGitHub a2a6afc3e3 temp disable arm docker builds (#1506) 2023-11-16 09:58:46 -06:00
Jason DoveandGitHub dfaba8c7b0 use release version of ffmpeg 6.1 (#1505) 2023-11-16 09:57:13 -06:00
Jason DoveandGitHub 5d11a6b46f use separate model for plex collection scanning since the api types are inconsistent (#1504) 2023-11-16 06:43:48 -06:00
Jason DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub 5ecf271773 fix jellyfin library scan (#1501)
* update dependencies

* fix jellyfin library scan
2023-11-10 06:26:23 -06:00
Jason DoveandGitHub b287c0d6ec add jellyfin season number fallback (#1497) 2023-11-06 09:37:12 -06:00
Jason DoveandGitHub b667659c05 use notarytool directly instead of gon (#1493) 2023-11-05 07:46:15 -06:00
Jason DoveandGitHub 22d3025e8e include noto cjk fonts in docker (#1492) 2023-11-05 06:15:57 -06:00
Jason DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub d3bdcf9bc4 allow plex personal media show libraries (#1483) 2023-10-13 13:33:10 -05:00
Jason DoveandGitHub 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 DoveandGitHub 17bed524f2 fix ui display of multiple languages (#1474) 2023-10-08 18:21:48 -05:00
Jason DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub b39dd693f0 update dependencies and windows ffmpeg (#1462)
* update dependencies

* update windows ffmpeg version
2023-10-05 19:14:06 -05:00
Jason DoveandGitHub 46bf9ef990 fix intel vaapi pgs subtitle pixel format (#1455) 2023-09-30 13:10:23 -05:00
Jason DoveandGitHub 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 DoveandGitHub 3ab8e5bc3a optimize jellyfin collection scanning (#1453) 2023-09-29 09:47:57 -05:00
Jason DoveandGitHub 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 DoveandGitHub b008fcfd85 fix scheduling precision error (#1451)
* fix scheduling precision error

* update dependencies
2023-09-27 06:07:48 -05:00
Jason DoveandGitHub 547db5fb51 add kodiprop to channels.m3u (#1448) 2023-09-26 15:47:55 -05:00
Jason DoveandGitHub 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 DoveandGitHub 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 DoveandGitHub e0f8b7d7ae use ffmpeg 6.1 snapshot for windows (#1435) 2023-09-14 19:33:40 -05:00
Jason DoveandGitHub b16215fcd6 improve hls throttle (#1434)
* throttle using ffmpeg option

* update ffmpeg version
2023-09-14 19:28:15 -05:00
Jason Dove 85f2c658aa update changelog for release v0.8.2-beta [no ci] 2023-09-14 09:19:21 -05:00
Jason DoveandGitHub 78356314e6 update dependencies (#1433) 2023-09-14 08:46:59 -05:00
Jason DoveandGitHub b00a25bbee fix parsing show title from nfo (#1426) 2023-09-10 19:46:51 -05:00
4d77576be2 update dependencies (#1425)
Co-authored-by: Jason <jason@mbp-touch.local>
2023-09-10 09:22:35 -05:00
Jason DoveandGitHub a90348740d fix subsequent hls session work ahead (#1419) 2023-09-06 20:01:58 -05:00
Jason DoveandGitHub 8081845ef1 fix adding alternate schedule (#1418) 2023-09-04 14:06:39 -05:00
Jason DoveandGitHub d014eb4274 mysql ui fixes (#1417) 2023-09-04 13:12:00 -05:00
Jason DoveandGitHub 8c9cf7b6f2 fix mid roll pad; fix mysql queries (#1416)
* fix mysql queries

* fix mid roll pad with hls segmenter
2023-09-04 10:01:39 -05:00
Jason DoveandGitHub 5d9c8d4f7d update checkout actions (#1415) 2023-09-04 08:38:10 -05:00
Jason DoveandGitHub 82de3136cd fix hls session worker lifetime (#1414) 2023-09-04 08:30:01 -05:00
Jason DoveandGitHub b1cd324f9c fix docker builds (#1413) 2023-09-03 08:54:48 -05:00
Jason DoveandGitHub 5caf8f7f98 fix builds (#1412) 2023-09-03 08:52:00 -05:00
Jason DoveandGitHub 245c4ec359 code analysis and cleanup (#1411)
* cleanup scanner project

* cleanup infrastructure projects

* cleanup ffmpeg project

* cleanup core project

* cleanup app project

* cleanup main project

* update dependencies

* code cleanup
2023-09-03 06:23:42 -05:00
Jason DoveandGitHub 6414471ace fix emby movie libraries (#1410) 2023-09-02 13:03:48 -05:00
Jason DoveandGitHub 8b0b927a5c use d3d11va for qsv accel on windows (#1408) 2023-09-01 11:50:56 -05:00
Jason DoveandGitHub e5962699a4 schedule item editor updates (#1407) 2023-09-01 06:44:31 -05:00
Jason DoveandGitHub 27504e42bc fix mysql show queries (#1406)
* ignore artwork when sync is disabled

* fix delete playout

* fix some mysql queries
2023-09-01 06:05:36 -05:00
Jason DoveandGitHub deb0ac49b5 show plex server names in libraries page (#1402)
* cleanup plex libraries query

* show plex server names in libraries page
2023-08-29 08:46:36 -05:00
Jason DoveandGitHub 225b95449c rework hls session state (#1401) 2023-08-29 05:59:35 -05:00
Jason DoveandGitHub cb43c28d00 fix hls session when starting with very short content (#1400) 2023-08-29 05:01:13 -05:00
Jason DoveandGitHub 0a75136223 fix transcoding short content (#1399) 2023-08-28 22:01:38 -05:00
Jason Dove efc710749e fix test 2023-08-28 20:44:35 -05:00
Jason DoveandGitHub 8f241f49fc optimize transcoding speed (#1398)
* fix "empty trash" button blinking when loading trash page

* clean channel guide cache on startup

* only work-ahead in hls session for 2 minutes
2023-08-28 20:42:02 -05:00
Jason DoveandGitHub 20d224fcfd more mysql fixes (#1397)
* fix channels and movie page crashes with mysql

* update dependencies
2023-08-26 10:39:33 -05:00
Jason DoveandGitHub b3fda4e88d allow shared plex servers (#1391)
* allow shared plex servers

* update dependencies
2023-08-22 08:49:26 -05:00
Jason DoveandGitHub 560cb826b3 fix local subtitles display (#1388)
* show external subtitles in media info

* fix mysql saved page size
2023-08-19 11:26:52 -05:00
Jason DoveandGitHub b038a58fa2 fix saving smart collection (#1384) 2023-08-15 15:53:56 -05:00
Jason DoveandGitHub c28e201e47 fix adding shows directly to schedule (#1383) 2023-08-15 15:34:53 -05:00
Jason DoveandGitHub b84bb6b437 fix ui crashes (#1382)
* fix ffmpeg editor crash

* fix watermark editor

* fix multi collection editor

* fix filler preset editor crash
2023-08-15 06:39:29 -05:00
Jason DoveandGitHub 641b8bcd10 fix windows dependency (#1380) 2023-08-14 19:44:18 -05:00
Jason DoveandGitHub 4b08ed5381 fix mysql migration (initial data) (#1379) 2023-08-14 15:21:42 -05:00
Jason Dove 5486dcdcab remove docs files and workflow [no ci] 2023-08-13 20:16:49 -05:00
Jason DoveandGitHub 77b32b0f09 fix github repo links (#1378) 2023-08-13 09:59:48 -05:00
Jason Dove 2c6c08becf update submodule [no ci] 2023-08-13 09:33:33 -05:00
Jason DoveandGitHub 99bc19cf26 fix docker builds (#1376) 2023-08-12 22:02:45 -05:00
Jason DoveandGitHub a7661c8498 add mysql database provider (#1375)
* refactor sqlite into separate library

* support mysql

* fixes

* sql fixes

* cleanup

* update changelog
2023-08-12 21:44:14 -05:00
Jason DoveandGitHub d951035183 fix bulk removing items from elasticsearch index (#1374) 2023-08-12 08:51:12 -05:00
Jason DoveandGitHub 097c60169c elasticsearch relative queries (#1373)
* remove unused code

* fix relative queries with elasticsearch

* fix some double page loads

* simplify language model
2023-08-12 06:36:28 -05:00
Jason DoveandGitHub d64d8b0454 don't always rebuild elasticsearch index on startup (#1372) 2023-08-11 15:47:17 -05:00
Jason DoveandGitHub 7486304ed9 fix elasticsearch smart collection playouts (#1371) 2023-08-11 15:28:57 -05:00
Jason DoveandGitHub c62cc98c9f add elasticsearch search index provider (#1370)
* wip

* first pass at elasticsearch; movies kind of work

* use field name constants

* properly sort search results

* fix some crashes

* fix page map/jump letters

* optimize page map using terms aggregation

* index all item types

* optionally use elastic search

* code cleanup

* automatically rebuild lucene index after improper shutdown

* update changelog
2023-08-11 13:57:50 -05:00
Jason Dove 22a13cb1b3 Revert "add debug logs for other video folder scanning (#1369)"
This reverts commit 5e573461f3.
2023-08-10 12:51:47 -05:00
Jason DoveandGitHub 5e573461f3 add debug logs for other video folder scanning (#1369) 2023-08-10 11:26:48 -05:00
Jason DoveandGitHub 76c596a7d8 fix logs page (#1368)
* fix log viewer crash

* update dependencies
2023-08-10 10:45:43 -05:00
Jason DoveandGitHub f945f16d97 fix qsv subtitle scaling (#1367) 2023-08-10 10:16:10 -05:00
Jason DoveandGitHub 797d4005e2 replace moq with nsubstitute (#1365) 2023-08-09 20:14:18 -05:00
Jason Dove 55903430ae update changelog for release v0.8.1-beta [no ci] 2023-08-07 13:32:07 -05:00
Jason DoveandGitHub f929dc92d1 update dependencies; code cleanup (#1357)
* update dependencies

* code cleanup
2023-08-07 09:34:25 -05:00
Jason DoveandGitHub 2ad27c2be0 update dependencies (#1348)
* update dependencies

* silence mudblazor debug logs
2023-07-24 20:40:32 -05:00
Jason DoveandGitHub df2db5caf7 add plex file name logging (#1342) 2023-07-12 19:27:16 -05:00
Jason DoveandGitHub 5978e8ecb1 fix vaapi rate control mode (#1340) 2023-07-08 12:36:07 -05:00
Jason Dove a540efc2e1 add community to readme [no ci] 2023-07-03 13:07:42 -05:00
Jason DoveandGitHub 1938cef6ae add community link to docs (#1339) 2023-07-03 13:01:54 -05:00
Jason DoveandGitHub b23d798aff update dependencies (#1329) 2023-06-26 11:11:40 -05:00
Jason DoveandGitHub ebad7664b0 force hw accel to use one thread (#1327) 2023-06-25 09:56:58 -05:00
Jason DoveandGitHub a9c93ff498 add custom resolution management (#1326)
* update some dependencies

* add custom resolution management
2023-06-25 09:14:19 -05:00
Jason DoveandGitHub 8277894f7b show database and search index initialization in ui (#1325)
* unblock startup, show database initialization message

* wait on search index to be ready (rebuild)

* clean logging and fake delay
2023-06-24 09:12:46 -05:00
Jason DoveandGitHub 0d66f752b6 add global mutex to ensure single instance (#1324) 2023-06-24 06:30:55 -05:00
Jason Dove c128f72a54 update changelog for release v0.8.0 [no ci] 2023-06-23 22:16:26 -05:00
Jason DoveandGitHub 4af2d7aa61 don't trust emby's anamorphic flag (#1321) 2023-06-22 20:07:58 -05:00
Jason DoveandGitHub 20a6727158 fix vaapi hw decoding (#1320) 2023-06-22 15:05:02 -05:00
Jason DoveandGitHub 52e1874426 vaapi cqp rate control mode (#1319) 2023-06-22 11:42:11 -05:00
Jason DoveandGitHub 015f5e9798 fix playout build loop that was recently introduced (#1318) 2023-06-22 09:40:20 -05:00
Jason DoveandGitHub 1fc461e476 update dapper (#1316) 2023-06-21 15:51:35 -05:00
Jason DoveandGitHub 85792f0811 fix nvidia color normalization (#1314) 2023-06-20 09:23:41 -05:00
Jason DoveandGitHub 0f91a43e3f fix scaling subtitles with nvidia accel (#1313) 2023-06-20 06:25:15 -05:00
Jason DoveandGitHub 7a25996ab4 scale subtitles with all accels (#1311)
* properly scale subtitles with qsv and vaapi

* fixes
2023-06-19 15:55:23 -05:00
Jason DoveandGitHub 6985826072 add mpeg-ts output format for hls direct (#1310) 2023-06-19 11:19:19 -05:00
Jason DoveandGitHub 52482ef2fb only discard items with random or shuffle playback order (#1309) 2023-06-19 09:17:10 -05:00
Jason DoveandGitHub c148f2eb11 fix discard to fill calculation (#1308) 2023-06-17 05:11:08 -05:00
Jason DoveandGitHub d490cc6f4b dont give up on scheduling filler while some should fit (#1306) 2023-06-14 16:58:37 -05:00
Jason DoveandGitHub 99bd827bd9 fix multi episode shuffle (#1305) 2023-06-14 16:40:18 -05:00
Jason DoveandGitHub e8cbcc935f rework pad and duration filler (#1304) 2023-06-14 15:54:41 -05:00
Jason DoveandGitHub a2acfe4d80 add finish column to playout detail table (#1302) 2023-06-13 19:09:19 -05:00
Jason DoveandGitHub 5da2bdbab4 add duration discard to fill attempts (#1301) 2023-06-13 17:02:31 -05:00
Jason DoveandGitHub 66607b95bb update dependencies (#1300) 2023-06-13 13:58:44 -05:00
Jason DoveandGitHub 81a6251f65 properly lock playout before build (#1299) 2023-06-13 13:45:00 -05:00
Jason DoveandGitHub c554d83d60 playout management ui improvements (#1298) 2023-06-13 13:26:34 -05:00
Jason Dove 875010bbf4 update changelog for release v0.7.9-beta [no ci] 2023-06-10 10:40:05 -05:00
Jason DoveandGitHub c5692ef5f1 update dependencies (#1296) 2023-06-08 09:20:06 -05:00
Jason DoveandGitHub 147ab6143d hls direct mkv container (#1292)
* use mkv container for hls direct

* add setting for mp4/mkv container with hls direct

* cleanup

* update changelog
2023-06-06 10:21:09 -05:00
Jason DoveandGitHub aca441074e subtitle improvements with hls direct (#1290)
* wip: hls direct subtitles

* convert picture subtitles with hls direct

* use mp4 for hls direct to support more codecs

* disable subtitle conversion in hls direct

* fix tests

* update changelog
2023-06-04 12:29:47 -05:00
Jason DoveandGitHub ef6adf9cbb update dependencies (#1289) 2023-06-02 06:35:31 -05:00
Jason DoveandGitHub ddb7e1887f fix nvidia h264 decoder (#1281) 2023-05-22 21:26:48 -05:00
Jason DoveandGitHub 4997699b4d sync jf and emby episode actors (#1280) 2023-05-22 15:58:08 -05:00
Jason DoveandGitHub c27b906cd5 update docs (#1277)
* tweak mkdocs config; update install

* path replacement doc updates
2023-05-21 10:43:48 -05:00
Jason DoveandGitHub bec3cb864d update dependencies (#1276)
* update dependencies

* fix ide warnings

* tweak ef config
2023-05-21 10:13:44 -05:00
Jason DoveandGitHub 03df2a6c8a overdue code cleanup (#1271) 2023-05-10 13:18:18 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6142dcf153 Bump jetbrains.resharper.globaltools from 2022.1.0 to 2023.1.1 (#1264)
Bumps jetbrains.resharper.globaltools from 2022.1.0 to 2023.1.1.

---
updated-dependencies:
- dependency-name: jetbrains.resharper.globaltools
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-10 08:56:08 -05:00
Jason DoveandGitHub b287f791e6 fix pgs subtitle burn in from media servers (#1270) 2023-05-09 22:43:25 -05:00
Jason DoveandGitHub 2ccba9e476 timeout playout builds after 2 minutes (#1269)
* add cancellation token support to playout builds and collection enumerators

* fix playout bug with shuffle in order

* update changelog
2023-05-08 11:53:02 -05:00
Jason DoveandGitHub e215807e56 add worker service debug logs (#1267)
* add worker service debug logs

* update mudblazor
2023-05-05 08:42:33 -05:00
Jason DoveandGitHub b0333e89cd fix fallback filler (#1265) 2023-05-03 12:08:14 -05:00
Jason DoveandGitHub bc240a40e0 fix extracting text subtitles (#1262) 2023-04-29 21:46:20 -05:00
Jason Dove ed816e4b06 update changelog for release [no ci] 2023-04-29 20:48:24 -05:00
Jason DoveandGitHub 7c0f26ed3e update dependencies (#1261) 2023-04-29 09:42:06 -05:00
Jason Dove a54e37a648 proper fix 2023-04-26 19:35:40 -05:00
Jason DoveandGitHub c0656114b8 ignore dot underscore files (#1259) 2023-04-26 11:46:38 -05:00
Jason DoveandGitHub 7628ec7921 fix vobsub subtitle burn in from media server libraries (#1258) 2023-04-25 15:18:53 -05:00
Jason DoveandGitHub 30850329f3 fix external subtitle playback on windows (#1256) 2023-04-23 09:53:57 -05:00
Jason DoveandGitHub 6bb1c4299f fix colorspace filter for some files with invalid color metadata (#1254) 2023-04-20 11:59:31 -05:00
Jason DoveandGitHub 73c6758537 add show media info button to movie and episode detail pages (#1253) 2023-04-20 10:45:32 -05:00
ef1400d3f8 Corrected issue with channel delete (#1247)
* Channel delete now updates the guide cache.

* update changelog

---------

Co-authored-by: Jason Dove <jason@jasondove.me>
2023-04-14 19:37:46 -05:00
Jason DoveandGitHub 494142f026 fix media server text subtitle extraction (#1246) 2023-04-14 15:09:12 -05:00
Jason DoveandGitHub b89deffda3 cleanup delete channel handler syntax (#1245) 2023-04-14 12:53:33 -05:00
Jason DoveandGitHub ab55978732 add season, episode playback order for shows (#1243) 2023-04-13 11:06:48 -05:00
Jason DoveandGitHub b8dcd26e3a fix hls direct regression (#1242) 2023-04-12 15:47:25 -05:00
Jason DoveandGitHub 24f7544c9f improve schedule editor performance (#1240)
* rename

* async cleanup

* use autocomplete fields in schedule items editor
2023-04-11 14:16:39 -05:00
Jason DoveandGitHub afb2caa95d fix qsv watermark alpha (#1239) 2023-04-11 10:30:19 -05:00
Jason DoveandGitHub a684dcced9 scheduling bug fixes (#1238) 2023-04-11 09:22:51 -05:00
Jason DoveandGitHub cf1552910a limit hls direct streams to realtime speed (#1237) 2023-04-09 20:21:03 -05:00
Jason Dove e53e2b36cf update copyright in docs 2023-04-07 21:42:49 -05:00
Jason Dove 793e85f889 update changelog for release v0.7.7-beta [no ci] 2023-04-07 21:41:50 -05:00
Jason DoveandGitHub 126304bb8a fix subtitles from media server libraries (#1233)
* fix embedded subtitles from media servers

* fix plex external subtitles

* fix artwork bug, delete orphaned subtitles

* jellyfin subtitles work again

* emby subtitles work

* rescan all media server libraries
2023-04-07 13:22:45 -05:00
Jason DoveandGitHub 7b3b9b4aad fix search index bug (#1231) 2023-04-05 21:52:31 -05:00
Jason DoveandGitHub cf3b8d90e3 fix bt470bg color normalization using qsv (#1230) 2023-04-05 10:02:46 -05:00
Jason DoveandGitHub 109b244676 use software decoding for mpeg4 part2 with nvidia accel (#1229) 2023-04-05 08:48:58 -05:00
Jason DoveandGitHub 9f42333465 cache data for xmltv (#1228)
* cache channel list for xmltv

* used cached channel data for xmltv

* fixes

* update changelog
2023-04-03 23:09:54 -05:00
Jason DoveandGitHub c9141b0d86 fix colorspace filter when using vaapi (#1226) 2023-04-02 10:09:05 -05:00
Jason DoveandGitHub d2c4a58528 minor bug fixes (#1225) 2023-04-01 22:51:48 -05:00
Jason DoveandGitHub e93d678b97 add more logging (#1224)
* more logging

* update dependencies
2023-04-01 12:24:15 -05:00
Jason DoveandGitHub 307940d732 add duplicate file logging (#1223) 2023-04-01 09:54:19 -05:00
Jason DoveandGitHub 721f0df82a fix library scanning with non-english season folder names (#1222) 2023-03-31 13:37:54 -05:00
Jason DoveandGitHub aa87abc53d prioritize poster artwork for xmltv (#1221) 2023-03-29 08:18:03 -05:00
Jason DoveandGitHub 83d4aa0cb1 use other video plot as xmltv description (#1219) 2023-03-27 19:22:44 -05:00
Jason DoveandGitHub 46034aff54 fix updating trakt lists (#1218) 2023-03-25 05:58:41 -05:00
Jason Dove 3e447ac7e4 fix changelog links [no ci] 2023-03-24 14:41:10 -05:00
Jason Dove bda27faaa3 update changelog for release v0.7.6-beta [no ci] 2023-03-24 14:40:04 -05:00
Jason DoveandGitHub 80d89a2530 fix color normalization from bt470bg to bt709 (#1217) 2023-03-22 08:04:21 -05:00
Jason Dove e849ef5dfa remove bad file 2023-03-21 22:35:09 -05:00
Jason DoveandGitHub a26ecb91b8 ignore sidecar subtitle files from media server libraries (#1216) 2023-03-21 20:44:13 -05:00
Jason Dove 2853e13edc update changelog [no ci] 2023-03-17 10:05:28 -05:00
9ba0b844a1 JWT Query Parameter Auth for IPTV Links (#1215)
* JWT Auth

* Standardized url variable additions

* formatting and minor refactoring

* this isn't needed

* allow channel logos without auth

* update changelog

---------

Co-authored-by: Ministorm3 <4474921+Ministorm3@users.noreply.github.com>
2023-03-17 09:33:15 -05:00
Jason DoveandGitHub fdab54a055 limit console log output on windows (#1212) 2023-03-16 06:21:02 -05:00
Jason DoveandGitHub 7e0801119e maintain collection progress across alternate schedules (#1211) 2023-03-15 20:34:25 -05:00
Jason DoveandGitHub b2f7bcaf1e add more fields to music video credit templates (#1210) 2023-03-15 20:10:24 -05:00
Jason DoveandGitHub 71b8be37da restyle troubleshooting page (#1208) 2023-03-13 13:24:27 -05:00
Jason DoveandGitHub f7d19e3747 duration mode guide fixes (#1207)
* fix playout mode duration xmltv

* fix playout mode duration wrapping midnight
2023-03-13 09:00:59 -05:00
Jason DoveandGitHub 17dcbfc344 add troubleshooting page (#1206) 2023-03-12 13:00:54 -05:00
Jason DoveandGitHub 78745de0ca rework emby collection scanning (#1205)
* optimize emby collection scan frequency

* add button to sync emby collections

* update changelog

* fix scanning; add progress indicator
2023-03-11 22:29:10 -06:00
Jason DoveandGitHub 35445e2b3d proxy external subtitle files (#1203) 2023-03-09 19:31:59 -06:00
Jason DoveandGitHub bd2f0f6236 song normalization (#1202)
* add tests to verify song normalization

* simplify song setup, include watermarks and album art

* fix song path

* update changelog
2023-03-09 10:40:59 -06:00
Jason DoveandGitHub 4c67965b50 fix emby scanning (#1201) 2023-03-09 07:59:26 -06:00
Jason DoveandGitHub 234e93349b rework concurrency (#1199) 2023-03-08 21:23:18 -06:00
Jason DoveandGitHub e7e20de502 include multiple display-name entries in xmltv (#1198) 2023-03-07 09:53:00 -06:00
Jason DoveandGitHub dfc36b4581 case-insensitive file extensions in local scanner (#1197) 2023-03-06 18:21:34 -06:00
Jason DoveandGitHub c56e2526c4 fix media server scanning (#1196)
* fix media server scans

* update dependencies
2023-03-06 08:28:35 -06:00
Jason DoveandGitHub 8ff6bf652c fix jellyfin streaming and sar calculation (#1195) 2023-03-05 21:40:20 -06:00
Jason Dove a386fe9ba1 update changelog for release v0.7.5-beta [no ci] 2023-03-05 10:12:21 -06:00
Jason DoveandGitHub 4d84fc242b plex scanner improvement (#1193)
* fix crash with some plex multi-episode files

* comments cleanup
2023-03-03 06:08:42 -06:00
Jason DoveandGitHub 40e79a3a14 fix plex scanner crash (#1192) 2023-03-02 22:49:21 -06:00
Jason Dove c653bb32a7 plex scanner logging 2023-03-02 20:57:38 -06:00
Jason DoveandGitHub b032e70d7e support more local season folder names (#1191) 2023-03-02 20:13:34 -06:00
Jason DoveandGitHub 074816be50 simplify qsv accel syntax on linux (#1189) 2023-03-02 06:00:36 -06:00
Jason DoveandGitHub 3fafd5192f fix hevc_nvenc encoder on sm < 75 (#1187) 2023-03-01 20:17:58 -06:00
Jason DoveandGitHub 1d63197b56 fix yuv444p10le (#1186) 2023-03-01 19:56:14 -06:00
Jason DoveandGitHub b2c57e7407 upgrade to ffmpeg 6 (#1185) 2023-03-01 19:21:53 -06:00
Jason DoveandGitHub 581aa51792 fix trash display for certain episodes (#1184)
* fix trash display for certain episodes

* fix multi-episode fallback metadata
2023-02-28 09:37:30 -06:00
Jason DoveandGitHub 4d57ece30d check ffmpeg for available decoders, filters, encoders (#1183)
* check ffmpeg for available decoders, filters, encoders

* revert csproj change
2023-02-27 19:28:42 -06:00
Jason DoveandGitHub eddbf07b11 vaapi: decode vp9 and av1 (#1181) 2023-02-27 05:57:13 -06:00
Jason Dove 450ea063b4 update vaapi docker bundled ffmpeg version 2023-02-26 21:39:22 -06:00
Jason DoveandGitHub f320d84874 fix ffmpeg version health check for vaapi docker (#1179) 2023-02-25 08:18:49 -06:00
Jason DoveandGitHub c832c8e860 prioritize default audio tracks (#1178) 2023-02-24 15:45:30 -06:00
Jason DoveandGitHub e5ef8eaf72 fix some cases where vaapi hwdownload would fail; use libvpl in docker (#1177)
* fix some cases where vaapi hwdownload would fail

* update changelog
2023-02-24 09:10:42 -06:00
Jason DoveandGitHub 6db71f525d remove duplicate filter from search index (#1172) 2023-02-21 22:15:57 -06:00
Jason Dove 3ab66ef12a update intel media driver in vaapi docker image 2023-02-21 19:02:54 -06:00
Jason DoveandGitHub 018f759fa4 improve vaapi capability detection (#1171) 2023-02-21 09:53:23 -06:00
Jason DoveandGitHub 1afff11063 software decoder fixes (#1169)
* fix software decoder pipeline bugs

* tweak nvidia scaling logic

* update changelog

* update dependencies
2023-02-20 19:45:25 -06:00
Jason DoveandGitHub 7e3436e68f direct stream content from emby as needed (#1168) 2023-02-19 20:29:09 -06:00
Jason DoveandGitHub b751f1054b direct stream content from jellyfin if needed (#1167)
* redirect to jellyfin stream as needed

* get jellyfin playback info

* sync chapters from jellyfin

* update changelog

* cleanup
2023-02-19 10:37:48 -06:00
Jason DoveandGitHub 900e9e75f3 sync chapter markers from plex (#1166) 2023-02-18 14:51:51 -06:00
Jason DoveandGitHub 62c28d9f51 direct stream content from plex if needed (#1165)
* start to stream directly from plex

* update metadata and statistics with one plex api call

* stream movies from plex

* scanning bug fix; update changelog
2023-02-18 10:40:05 -06:00
Jason DoveandGitHub 132ca99f94 fix default dockerfile (#1156) 2023-02-13 05:38:11 -06:00
Jason Dove c309ab430e update changelog for release v0.7.4-beta [no ci] 2023-02-12 18:21:11 -06:00
Jason DoveandGitHub 13e21bbcce sync episode tags and genres (#1155)
* sync episode tags and genres

* update dependencies

* property update local episode genres and tags

* fix test
2023-02-12 09:53:20 -06:00
Jason DoveandGitHub 0eb36f0ce1 prioritize default audio streams (#1154) 2023-02-10 09:31:55 -06:00
Jason DoveandGitHub 6429f0f064 fix filler padding (#1153)
* fix filler padding

* update dependencies
2023-02-07 19:55:30 -06:00
Jason DoveandGitHub 7412ac6fc9 fix mid and post roll filler ordering (#1152) 2023-02-07 12:25:22 -06:00
Jason DoveandGitHub e58e3c786d fix last scan check (#1150) 2023-02-06 05:38:08 -06:00
Jason DoveandGitHub 93fc1e4eb4 fix fallback filler looping (#1146) 2023-02-04 08:49:52 -06:00
Jason DoveandGitHub cacde26796 merge other video folder tags with nfo tags (#1144) 2023-02-01 05:58:26 -06:00
Jason DoveandGitHub 0a3db92c60 fix schedule copy (#1142) 2023-01-30 10:23:18 -06:00
Jason DoveandGitHub 8bb0cd5ab5 add copy schedule feature (#1141) 2023-01-30 06:38:34 -06:00
Jason DoveandGitHub e497dc4e36 fix nvidia vp9 color normalization (#1140) 2023-01-29 16:02:29 -06:00
Jason DoveandGitHub 2689a67eb8 qsv and vaapi fixes (#1139)
* lots of qsv fixes

* update changelog

* fix qsv mpeg2

* vaapi fixes

* update changelog

* upgrade mudblazor

* fix bug with undefined input colorspace
2023-01-29 10:00:52 -06:00
Jason Dove 3d821043bb update changelog for v0.7.3-beta [no ci] 2023-01-25 12:01:38 -06:00
Jason DoveandGitHub e69c58e615 conditionally disable v2 apis (#1135)
* conditionally disable v2 apis

* update changelog
2023-01-25 11:37:43 -06:00
Jason DoveandGitHub a21b6f9f4e add oidc logout url to support auth0 (#1134) 2023-01-25 09:30:36 -06:00
Jason DoveandGitHub 99b8038852 add oidc support (#1133) 2023-01-25 08:37:59 -06:00
Jason DoveandGitHub ef8ca9f8c6 build mac artifacts on macos 11 (#1132) 2023-01-24 15:04:55 -06:00
Jason DoveandGitHub d9186df157 minor logging and doc updates (#1130) 2023-01-23 05:28:17 -06:00
Jason DoveandGitHub aca6bfb0bb fix multiple gcs after extracting subtitles (#1129) 2023-01-22 13:10:13 -06:00
Jason DoveandGitHub 587fc3a98f release memory after extracting embedded subtitles (#1128) 2023-01-22 12:34:42 -06:00
Jason DoveandGitHub ab1c67e60e memory improvements (#1127)
* regularly release memory

* don't aggressively GC while legacy streaming

* update changelog
2023-01-22 09:16:24 -06:00
Jason DoveandGitHub e271f43066 more scan check fixes (#1126) 2023-01-21 08:22:56 -06:00
Jason DoveandGitHub 6bf8feb26e fix local library scan check with new install (#1125) 2023-01-21 08:10:42 -06:00
Jason DoveandGitHub ffd66f6a21 fix removing media server libraries (#1124) 2023-01-20 09:31:18 -06:00
Jason DoveandGitHub 3b135df4c1 scan with below-normal priority when unforced (#1123) 2023-01-20 06:05:39 -06:00
Jason DoveandGitHub 4369d04940 scanner improvements (#1122)
* optimize periodic scanning

* set scanner process priority

* update dependencies
2023-01-20 05:37:39 -06:00
Jason Dove faaa78fed7 update changelog [no ci] 2023-01-18 15:40:00 -06:00
Jason DoveandGitHub 6bea1660ea disable mac compression; this is needed until dotnet 7.0.3 (#1120) 2023-01-18 15:13:07 -06:00
Jason DoveandGitHub 8d46676c25 try to fix mac scanning (#1119) 2023-01-18 14:43:26 -06:00
Jason DoveandGitHub 4c75e638a2 fix bug with smart collection progress (#1118) 2023-01-18 14:09:54 -06:00
Jason DoveandGitHub dd73a3803a fix schedule editor crash (#1115)
* fix schedule editor crash due to bad music video artist data

* update dependencies
2023-01-15 06:35:51 -06:00
Jason Dove f6c345d7cf fix build 2023-01-10 15:13:42 -06:00
Jason DoveandGitHub 585b56a668 bug fixes (#1107)
* don't search an empty search index

* fix bug with flood filler prediction check

* extract subtitles on primary worker thread
2023-01-10 14:45:04 -06:00
Jason Dove f18f3b4f35 try to fix develop artifacts 2023-01-09 08:46:55 -06:00
Jason DoveandGitHub eb7871a048 fix alternate schedule playout update check (#1106)
* fix alternate schedule playout update check

* Revert "use mknejp/delete-release-assets again"

This reverts commit 07ac833067.
2023-01-09 05:36:15 -06:00
Jason DoveandGitHub 000fc78fd3 add alternate schedule system (#1105)
* start to add program schedule alternates

* edit days of the week

* editor improvements

* save changes

* build playouts using alternate schedules

* reset playout as needed

* add priority message
2023-01-08 23:22:17 -06:00
Jason DoveandGitHub ba676ef956 add jellyfin admin error logging (#1102) 2023-01-07 09:55:02 -06:00
Jason DoveandGitHub 36ea88e2d6 fix error display (#1099)
* fix error display by ignoring hw accel setting

* update changelog

* revert background change
2023-01-05 20:02:48 -06:00
Jason Dove 5237e6fa50 update changelog for release v0.7.2-beta [no ci] 2023-01-05 11:51:57 -06:00
Jason DoveandGitHub 99bde1819c use mknejp/delete-release-assets again (#1098) 2023-01-05 10:26:42 -06:00
Jason Dove f5d7ec2890 update workflow [no ci] 2023-01-05 10:06:32 -06:00
Jason DoveandGitHub 13c65435d3 update dependencies (#1097) 2023-01-05 09:27:12 -06:00
Jason DoveandGitHub 315420f1a5 fix log viewer on windows (#1095)
* fix log viewer on windows

* catch cancellation on trakt page

* update changelog
2023-01-04 22:26:35 -06:00
Jason DoveandGitHub ab7051f075 reimplement log viewer (#1094) 2023-01-04 10:09:11 -06:00
Jason Dove a43e5bbe9d update changelog for release v0.7.1-beta [no ci] 2023-01-03 09:40:59 -06:00
Jason DoveandGitHub b7bd4541b1 hide windows on windows (#1091)
* hide windows on windows

* update dependencies
2023-01-03 09:14:50 -06:00
Jason Dove 648f25e9cc fix terminating server process 2023-01-01 14:20:52 -06:00
Jason Dove ccbe85a46a also hide the main server window 2023-01-01 14:12:41 -06:00
Jason DoveandGitHub d168d79fe0 hide windows wrapper console (#1088) 2023-01-01 13:42:39 -06:00
Jason Dove d37dde2477 try to fix windows build 2023-01-01 13:19:12 -06:00
Jason DoveandGitHub 8e13b07c84 convert windows project from dotnet to rust (#1087)
* convert windows project from dotnet to rust

* update pr jobs

* pr job fixes

* don't bother building mac app in prs for now

* build windows wrapper with rust
2023-01-01 13:01:58 -06:00
Jason DoveandGitHub 927e7724f0 fix search bug (#1086)
* fix removing media items from search index

* update changelog
2023-01-01 08:49:15 -06:00
Jason DoveandGitHub 6558c5bd69 fix subtitle update bug (#1085)
* fix saving some subtitles to database

* fix ffprobe regression
2022-12-31 19:57:14 -06:00
Jason DoveandGitHub 5f7efbb69c properly fall back to software pipeline (#1084) 2022-12-31 14:06:00 -06:00
Jason DoveandGitHub b79795af50 add debug logging to local subtitle provider (#1083) 2022-12-31 11:36:08 -06:00
Jason DoveandGitHub 9479806cb0 scanner refactoring and other cleanup (#1082)
* move subtitles provider into scanner

* move more stuff into scanner

* move nfo into scanner

* add scan subcommand

* fix a bunch of nfo build warnings

* more subcommands

* fix warnings

* cleanup logging

* remove unused code

* cleanup old ffmpeg stuff

* rename complex filter

* refactor wrapped segmenter
2022-12-31 10:57:20 -06:00
Jason DoveandGitHub 6e49ea78ec music video template contrib (#1081)
* add another music video template

* add more music video credit templates
2022-12-30 13:26:07 -06:00
Jason DoveandGitHub 7b1edd9c54 add new scanner process (#1080)
* start moving local scans to separate process

* send progress updates to main process

* move scanners and tests

* simplify dependencies; sync search index

* commit search index more often when scanning

* support forced scan and cancellation

* use scanner process for plex libraries

* update changelog

* update dockerfiles

* fix search index for local folder scanning

* rework plex scanners

* rework scanner handlers

* emby works again

* sync jellyfin

* cleanup

* update build

* update changelog

* remove scanner dependency in pr and artifacts workflows

* fix mac sed syntax

* fix pr build
2022-12-30 12:53:05 -06:00
Jason DoveandGitHub aeaafd2964 add scanner subtitle logging (#1079) 2022-12-29 06:00:00 -06:00
Jason DoveandGitHub 622fa01602 update dependencies and fix some types (#1077) 2022-12-28 14:21:25 -06:00
Jason DoveandGitHub e2b3c1ce8e properly unflag local movies that are now present on disk (#1076) 2022-12-28 13:41:01 -06:00
Jason DoveandGitHub 6c5db650e7 nvidia pixel format and song fixes (#1075)
* fix nvidia pixel format edge case

* fix nvidia song playback
2022-12-24 20:39:22 -06:00
Jason DoveandGitHub 731072425b fix nvidia pipeline that only requires setparams (#1074) 2022-12-24 13:19:27 -06:00
Jason DoveandGitHub 0f817308a8 limit library scan interval (#1073)
* limit library scan interval

* fix condition
2022-12-24 12:58:38 -06:00
Jason DoveandGitHub 0fc1e15cac colorspace fixes; song playback fixes (#1072)
* fix colorspace bug, vaapi song playback

* more colorspace fixes, nvidia fixes

* nvidia colorspace fixes

* fix some qsv output color metadata

* update changelog

* update changelog
2022-12-23 15:11:05 -06:00
Jason Dove acf30384b7 update changelog [no ci] 2022-12-20 20:14:31 -06:00
Jason DoveandGitHub d2040eaac9 pipeline fixes when colorspace filter is used (#1068)
* fix colorspace filter with missing input transfer or input primaries

* properly download before applying colorspace filter

* fix extra hwupload/hwdownload with nvidia pipeline

* colorspace tests

* update dependencies
2022-12-20 20:12:27 -06:00
Jason DoveandGitHub 93673fce03 add more logging to vaapi capabilities detection (#1059) 2022-12-15 19:32:48 -06:00
Jason Dove d7a432068b fix arm docker builds 2022-12-15 14:26:00 -06:00
Jason Dove cb9215980a fix dockerfiles, focal to jammy 2022-12-15 14:14:07 -06:00
Jason DoveandGitHub a4fc1f1c6f upgrade to dotnet 7, ffmpeg 5.1.2 (#1058)
* wip

* update dockerfiles

* more net6 to net7

* update dependencies

* update builds
2022-12-15 14:08:21 -06:00
Jason Dove cbbdb11938 update changelog for release v0.7.0-beta [no ci] 2022-12-11 06:53:05 -06:00
Jason DoveandGitHub a2274bca7b detect vaapi capabilities (#1051)
* remove unused pipeline

* spike vaapi hardware capabilities

* more vaapi capabilities

* use proper vaapi driver

* update readme

* update dependencies
2022-12-10 14:10:19 -06:00
Jason DoveandGitHub f84496b09d extract attached fonts (#1050) 2022-12-09 22:22:15 -06:00
Jason DoveandGitHub 3abf310a3b add amf pipeline (#1048) 2022-12-09 15:23:00 -06:00
Jason DoveandGitHub f12e361c2e fix videotoolbox color normalization (#1047) 2022-12-08 13:11:46 -06:00
Jason DoveandGitHub cd0f1e98cc fix qsv color normalization (#1046) 2022-12-08 08:17:59 -06:00
Jason DoveandGitHub 325ef80951 normalize bit depth via new pipeline (#1045)
* fix subtitles test and nvidia subtitles

* new ffmpeg pipelines; software and nvidia

* partial qsv

* fix qsv

* fix software pipeline

* add vaapi pipeline

* fix qsv 10-bit h264 output

* nvidia fixes

* properly disable 10-bit h264 hardware encoders

* more nvidia fixes

* add video toolbox pipeline
2022-12-07 21:25:55 -06:00
Jason DoveandGitHub 9a30d7c7da error report bug fixes (#1042)
* fix some potential null reference exceptions

* searching isn't actually async

* add search query breadcrumb
2022-12-03 05:47:04 -06:00
Jason DoveandGitHub 25ea75b761 more color fixes (#1040) 2022-11-25 21:25:04 -06:00
Jason DoveandGitHub 32edf77d35 fix bt709 check (#1039) 2022-11-25 10:09:58 -06:00
Jason DoveandGitHub 47fbb2b1b7 properly unlock trakt (#1035) 2022-11-23 18:34:55 -06:00
Jason DoveandGitHub e388f81e1f re-enable bugsnag auto notification (#1034) 2022-11-22 20:09:25 -06:00
Jason DoveandGitHub f0bea295c4 add video stats to search index (#1033) 2022-11-22 09:35:28 -06:00
Jason DoveandGitHub 7439ded43d normalize bit depth (#1031)
* normalize bit depth and color for nvenc

* fix hls direct

* update changelog

* add bit depth option to ffmpeg profile
2022-11-21 20:20:07 -06:00
Jason DoveandGitHub 6a640d3708 fix ogg song metadata (#1030) 2022-11-20 12:44:08 -06:00
Jason DoveandGitHub 776bce9087 use base path in channel playlist and channel guide (#1028) 2022-11-20 08:28:14 -06:00
Jason Dove 3c499f9e97 proper fix 2022-11-19 10:41:16 -06:00
Jason DoveandGitHub 114ff7a3e3 fix develop build cleanup (#1027) 2022-11-19 10:16:12 -06:00
Jason DoveandGitHub 527cdf523c fix work-ahead limit setting (#1023) 2022-11-16 21:17:23 -06:00
Jason Dove 91eb8ab824 try to fix develop release 2022-11-04 11:26:05 -05:00
Jason DoveandGitHub 7a87fb1c2e fix removing emby and jellyfin libraries (#1011)
* fix removing jellyfin and emby libraries

* remove unneeded change
2022-11-04 06:25:51 -05:00
Jason DoveandGitHub d8cc6b4c22 fix audio stream selection (#1010) 2022-11-02 14:55:43 -05:00
Jason DoveandGitHub c9bd94d9f8 use javascript instead of lua for external scripts; add audio stream selector scripts (#1005)
* use js instead of lua

* update dependencies

* add audio stream selector script for episodes

* add audio stream selector script for movies

* update changelog
2022-10-28 17:05:07 -05:00
Jason Dove 93bf818882 fix syntax [no ci] 2022-10-22 11:20:42 -05:00
Jason Dove 723fb3848d try to fix release by skipping unnecessary step 2022-10-22 11:19:39 -05:00
Jason Dove 6a213e2249 update changelog for release v0.6.9-beta [no ci] 2022-10-21 21:15:43 -05:00
Jason Dove a6c5c3a317 bump search index version 2022-10-21 15:21:33 -05:00
Jason DoveandGitHub 9313d2c8eb fix guide mode filler in xmltv (#1000) 2022-10-16 13:23:26 -05:00
Jason DoveandGitHub 485a874ab5 fix automatic playout reset (#999) 2022-10-16 10:30:54 -05:00
Jason DoveandGitHub f2bc884632 fix x-forwarded-proto (#998) 2022-10-13 13:16:51 -05:00
Jason DoveandGitHub 39d6653f8e temporarily enable http logging (#997) 2022-10-13 12:58:15 -05:00
Jason DoveandGitHub 2ce0fcb264 proxy server improvements (#996) 2022-10-13 12:17:13 -05:00
Jason DoveandGitHub 8bf5e18ae5 fix nfo reader (#995)
* fix nfo reader

* fix template fade

* update dependencies
2022-10-13 05:08:41 -05:00
Jason DoveandGitHub 88f4d8074a fix stream_seek type (#988)
* fix stream seek type

* cleanup
2022-10-09 21:20:34 -05:00
Jason DoveandGitHub f5aa2fcac8 add multi-episode shuffle playout order (#987) 2022-10-09 10:49:07 -05:00
Jason DoveandGitHub 6f892bea6b optionally place watermark within source content (#986) 2022-10-09 08:16:59 -05:00
Jason Dove cbf0c9c988 fix transcoding tests 2022-10-08 21:41:49 -05:00
Jason DoveandGitHub 393c67213d add stream_seek to music video credits template (#985) 2022-10-08 19:45:24 -05:00
Jason DoveandGitHub f69de9f071 fix all_artists in music video credits template (#984) 2022-10-08 12:40:46 -05:00
Jason DoveandGitHub 2e400c0d22 simplify music video credits config (#983) 2022-10-08 07:52:57 -05:00
Jason DoveandGitHub 6035c10550 add music video credits template system (#982)
* add music video credits template system

* fix search index bug

* cleanup csproj
2022-10-07 21:02:29 -05:00
Jason DoveandGitHub 555b156154 fix tail and fallback filler scheduling (#981) 2022-10-07 09:21:19 -05:00
Jason Dove a0ea2e8910 update changelog for release v0.6.8-beta [no ci] 2022-10-05 11:09:03 -05:00
Jason DoveandGitHub 734ca39cbd add guids to search index (#980) 2022-10-04 19:52:19 -05:00
Jason DoveandGitHub e0e5cfada5 fix numeric range search queries (#979) 2022-10-04 18:49:35 -05:00
Jason DoveandGitHub 7e0c43bc46 update dependencies (#977) 2022-10-01 07:56:14 -05:00
Jason DoveandGitHub be1125a9ab properly sync updated file paths from plex (#976) 2022-09-30 20:41:32 -05:00
Jason DoveandGitHub d21c985a77 fix emby tag sync for movies and shows (#975) 2022-09-30 19:56:19 -05:00
Jason DoveandGitHub 28f2b9b27e disable anamorphic edge case in scaling calculations (#971) 2022-09-26 15:11:05 -05:00
Jason DoveandGitHub 9b185e19e9 fix other video search results when nfo metadata is used (#970)
* add debug no sync build config

* fix other video search results

* update dependencies
2022-09-25 22:27:34 -05:00
Jason DoveandGitHub 27b923b462 qsv and vaapi scaling fixes (#966)
* add qsv device option to ffmpeg profile

* fix vaapi scaling

* cleanup
2022-09-18 21:02:09 -05:00
Jason DoveandGitHub 357dfee050 nvidia and software mode scaling improvements (#965)
* convert to square pixels before software scaling

* convert to square pixels in nvidia scale filter

* more scaling fixes; position watermark within padded content

* fix image subtitle scaling

* fix qsv scaling

* update dependencies
2022-09-18 11:04:02 -05:00
Jason DoveandGitHub 7f4004c228 fix qsv hevc encoder (#956)
* update dependencies

* fix typo in qsv hevc encoder param

* update changelog
2022-09-10 15:44:59 -05:00
Jason Dove 9b8dc0ed80 update changelog for release v0.6.7-beta [no ci] 2022-09-05 13:15:04 -05:00
Jason DoveandGitHub 3cc1286271 include other videos (ungrouped) in shuffle in order (#953)
* include other videos (ungrouped) in shuffle in order

* fix id conflict
2022-09-05 09:20:11 -05:00
Jason DoveandGitHub df281758b7 properly fix infinite playout build loop (#952) 2022-09-05 08:41:39 -05:00
Jason DoveandGitHub 25273c18c8 stop infinite playout building loop (#951) 2022-09-04 20:28:28 -05:00
Jason DoveandGitHub f1be945423 add qsv extra hardware frames setting (#950)
* wip add qsv extra_hw_frames setting

* fix ffmpeg profile editor

* update changelog
2022-09-04 18:07:03 -05:00
Jason DoveandGitHub 9a4f772f53 fix image subtitle scaling (#949)
* properly scale image-based subtitles for nvidia and software

* fix vaapi image subtitle scaling

* fix qsv image subtitle scaling

* update changelog
2022-09-04 14:25:05 -05:00
Jason DoveandGitHub d669e8114b more scaling fixes (#948)
* remove force_original_aspect_ratio from scale_cuda

* remove force_original_aspect_ratio from scale_cuda

* fix qsv scaling

* fix qsv scaling on linux

* fix vaapi scaling edge cases

* update changelog
2022-09-03 20:28:18 -05:00
Jason DoveandGitHub 3972e3603b add amf acceleration (#947) 2022-09-03 10:39:54 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
acc22fcb62 Bump MudBlazor from 6.0.14 to 6.0.15 (#945)
Bumps [MudBlazor](https://github.com/MudBlazor/MudBlazor) from 6.0.14 to 6.0.15.
- [Release notes](https://github.com/MudBlazor/MudBlazor/releases)
- [Changelog](https://github.com/MudBlazor/MudBlazor/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/MudBlazor/MudBlazor/compare/v6.0.14...v6.0.15)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 19:41:34 -05:00
Jason DoveandGitHub 2df360d7fb fix xmltv filler bug (#944) 2022-08-31 20:15:25 -05:00
Jason DoveandGitHub 46331ed2c6 add preferred audio title feature (#943)
* use consistent edit/delete icons

* add preferred audio title feature

* update dependencies
2022-08-30 17:04:41 -05:00
Jason Dove 3aee3b0515 fix windows build 2022-08-26 09:07:46 -05:00
Jason DoveandGitHub 72c45692b2 update dependencies (#937) 2022-08-26 08:52:59 -05:00
Jason DoveandGitHub 8edf71ca55 downgrade libva, include vainfo in docker (#936) 2022-08-25 16:29:20 -05:00
Jason DoveandGitHub 612b9e6524 fix scanner crash caused by invalid mtime (#934) 2022-08-20 19:56:37 -05:00
Jason DoveandGitHub 7aff65f07b explicitly copy all audio streams with hls direct (#933)
* ensure audio streams are always copied with hls direct

* update changelog
2022-08-18 14:23:51 -05:00
Jason Dove 5d350fcfad update changelog for release v0.6.6-beta [no ci] 2022-08-17 20:40:50 -05:00
Jason DoveandGitHub 5546ad204c upgrade to ffmpeg 5.1 (#931)
* use ffmpeg 5.1 on windows

* remove some debug logs

* use latest ffmpeg on arm

* use ffmpeg 5.1 base images

* update ffmpeg health check for 5.1

* update changelog
2022-08-17 14:57:23 -05:00
Jason DoveandGitHub d66efa0a1d prioritize container aspect ratio over stream aspect ratio (#930)
* prioritize container aspect ratio over stream aspect ratio

* use setdar filter
2022-08-16 19:57:38 -05:00
Jason DoveandGitHub 36d3d38530 remove all use of setsar filter (#928) 2022-08-16 12:25:59 -05:00
Jason DoveandGitHub 8e79141860 use multi-variant playlists for hls segmenter (#926)
* use multi-variant playlists for hls segmenter

* use lowercase mime type
2022-08-13 19:58:08 -05:00
Jason DoveandGitHub 9b3545f7ca add some temporary debug logging (#925) 2022-08-13 19:19:47 -05:00
Jason DoveandGitHub 56db20faa0 limit segmenter delay to 8s (#924)
* always return initial hls playlist after 8 seconds

* update dependencies

* make fluentvalidation happy
2022-08-12 19:48:24 -05:00
Jason DoveandGitHub b0bd4c9fed add ogg file formats to local song library scanner (#914)
* add ogg file formats to local song library scanner

* update dependencies
2022-08-04 16:01:17 -05:00
Jason DoveandGitHub ba079452e2 add dff and dsf to local song library scanner (#911) 2022-08-03 11:01:13 -05:00
Jason Dove f0f2b3da4b update changelog for release v0.6.5-beta [no ci] 2022-08-02 07:36:45 -05:00
Jason DoveandGitHub 866049543c fix db initializer (#907) 2022-07-31 12:30:33 -05:00
Jason Dove 40ed4b8b0e update changelog for release v0.6.4-beta [no ci] 2022-07-28 12:23:33 -05:00
Jason DoveandGitHub b43d08ca67 fix repeating schedules (#901) 2022-07-26 13:04:05 -05:00
Jason DoveandGitHub 5e7e386108 fix search result filtering for episodes and other videos (#900) 2022-07-25 20:02:07 -05:00
Jason DoveandGitHub 4176df9940 fix nvidia capabilities for second-gen maxwell (#899) 2022-07-24 12:23:30 -05:00
Jason DoveandGitHub de2ef959fe add 640x480 resolution (#898)
* update dependencies

* add 640x480 resolution
2022-07-24 08:17:27 -05:00
Jason DoveandGitHub b53cfebac1 fix bug with unsupported aac channel layouts (#893)
* fix bug with unsupported aac channel layouts

* update dependencies
2022-07-14 10:52:25 -05:00
Jason DoveandGitHub 6895b9cc6b fix search repo caching bug (#886)
* add failing test

* fix search repo bug

* update dependencies
2022-07-10 15:32:06 -05:00
Jason Dove c60d6e46f1 fix changelog [no ci] 2022-07-04 15:23:26 -05:00
Jason Dove c66d190174 update changelog for release v0.6.3-beta [no ci] 2022-07-04 15:20:53 -05:00
Jason DoveandGitHub 5e8da591be update dependencies (#883)
* fix database initialization

* update dependencies
2022-07-02 20:42:07 -05:00
Jason DoveandGitHub 9c02a6738b fix missing trashed episodes (#881)
* fix episodes missing from trash

* cleanup
2022-06-29 15:01:49 -05:00
Jason DoveandGitHub 5ed0184bca add minimum log level setting (#877) 2022-06-27 10:29:04 -05:00
Jason DoveandGitHub ae64ca4a93 fix arm images by using ls55 (#876) 2022-06-26 17:41:39 -05:00
Jason DoveandGitHub c47099895e include item state in search index duplicate filter (#875) 2022-06-26 13:34:54 -05:00
Jason Dove a2529febba use brew for gon 2022-06-26 08:30:32 -05:00
Jason Dove 521e0ba8b3 get a new build 2022-06-26 06:46:30 -05:00
Jason Dove ee0efac9be only publish docs when docs are updated 2022-06-26 06:21:27 -05:00
Jason DoveandGitHub bfe7635489 work around github actions issue on mac (#874) 2022-06-25 19:30:21 -05:00
Jason DoveandGitHub aa1735f024 fix song and other video search index (#873) 2022-06-25 18:13:39 -05:00
Jason DoveandGitHub 8deae983c7 add some startup log messages (#872) 2022-06-25 13:03:37 -05:00
Jason DoveandGitHub f349646703 apply plex episode metadata updates (#871)
* update more plex episode metadata

* update dependencies
2022-06-22 19:41:05 -05:00
Jason DoveandGitHub 5003e80500 maintain stream continuity after playout reset (#868)
* maintain stream continuity after playout reset

* maintain continuity after error streams
2022-06-18 21:38:25 -05:00
Jason Dove 940d9cd6b5 update changelog for release v0.6.2-beta [no ci] 2022-06-18 13:46:45 -05:00
Jason DoveandGitHub 197c166789 fix jellyfin admin id selection (#867) 2022-06-17 18:25:22 -05:00
Jason DoveandGitHub d114db091e use proper nvidia accel output format for 10-bit content (#865) 2022-06-17 11:33:10 -05:00
Jason DoveandGitHub 3204da8e43 adjust nvidia capabilities (#864)
* adjust nvidia capabilities logic

* fallback to software encoding for 10-bit h264

* cleanup

* more tweaks
2022-06-17 10:50:36 -05:00
Jason DoveandGitHub 100eb14408 fix epg sorting (#863)
* fix epg sorting

* update dependencies
2022-06-17 08:44:26 -05:00
Jason DoveandGitHub 025017ace5 regularly delete old segments (#856)
* regularly delete old segments

* cleanup
2022-06-15 21:12:07 -05:00
Jason DoveandGitHub 6a690c7c10 add more filler logging (#854) 2022-06-15 10:21:05 -05:00
Jason DoveandGitHub dd7f77751c detect nvidia capabilities (#853)
* fallback to software codecs for old nvidia cards

* update dependencies
2022-06-14 19:44:34 -05:00
Jason DoveandGitHub 0c13b8ef1a force amd64 for arm32v7 sdk build layer (#843) 2022-06-12 13:54:55 -05:00
Jason DoveandGitHub c6ca58ab97 build arm32v7 docker image (#842)
* build arm32v7 docker image

* fix
2022-06-12 13:42:27 -05:00
Jason DoveandGitHub 0846fc1d96 update workflow dependencies (#841) 2022-06-11 13:40:47 -05:00
Jason DoveandGitHub e41dd68ee0 fix automatic playout building (#840) 2022-06-11 13:11:04 -05:00
Jason DoveandGitHub 0a92996da8 fix repeating content (#838)
* fix repeating content

* update dependencies
2022-06-08 10:37:56 -05:00
Jason Dove 082bc6145c update changelog for release v0.6.1-beta [no ci] 2022-06-03 15:05:06 -05:00
Jason DoveandGitHub bf3f16451b music video credits tweaks (#834)
* fix song subtitles

* always use generated subtitles

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

* add new files

* cleanup controller; remove unused classes

* apply formatting

* cleanup core project

* don't use bom

* whitespace

* remove generated css

* remove generated js/map

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

* Changed deleted confirmation to toast.

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

* lint

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

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

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

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

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

* add paging to media server season and episode library calls

* formatting

* add paging to media server collection calls

* add paging to media server collection item calls

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

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

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

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

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

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

* logging improvements

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

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

* nfo metadata reliability fixes

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

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

* use new transcoder logic for errors

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

* fix local movie fallback metadata

* use imagesharp again

* fix search edge case

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

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

* changelog

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

* add artist nfo tests

* add music video nfo tests

* add tv show nfo reader tests

* custom artist nfo reader

* custom music video nfo reader

* custom tv show nfo reader

* local metadata parsing cleanup

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

* refactor episode nfo reader

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

* sync jellyfin libraries before scanning to ensure latest path infos

* code cleanup

* support emby path infos

* fix periodic scanning of emby and jellyfin libraries

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

* remove transcode property; use i18n

* add api

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

* refactor emby television scanner

* refactor jellyfin television scanner

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

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

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

* optimize search-index rebuilding

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

* client-app: add basic support to translation

* client-app: fix i18n and create lang state

* client-app: add language selector

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

* local library scanner cleanup

* emby and jf library scanner cleanup

* rework emby movie library scanner

* refactor emby movie library scanner

* refactor jellyfin movie library scanner

* clear etag until after new media has been processed

* refactor plex movie library scanner

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

* scanner async cleanup

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

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

* plex television scanner improvements

* reset plex etags as needed

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

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

* fix subtitles in docker

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

* improve plex movie scanner

* sync plex collections as tags

* improve plex tv library scanner

* update dependencies

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

* code cleanup

* simplify subtitle updating

* skip external subtitles from media servers

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

* run code cleanup

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

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

* fixes

* qsv fixes

* vaapi fixes

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

* videotoolbox fixes

* fix some vaapi subtitle edge cases
2022-04-16 16:06:49 -05:00
Jason DoveandGitHub 0ddbb898d6 fix subtitle stream selection (#735) 2022-04-15 19:40:08 -05:00
Jason Dove d6bf579436 fix alpha => beta versioning 2022-04-15 09:08:30 -05:00
Jason DoveandGitHub 765df64555 add picture subtitle transcoding tests, and make them all pass with nvenc (#734) 2022-04-14 22:30:26 -05:00
Jason Dove 8764fb93fa update for release v0.5.0-beta [no ci] 2022-04-13 11:01:54 -05:00
Jason Dove 7d5c3e2384 update docs 2022-04-13 11:01:49 -05:00
Jason DoveandGitHub 1ee3446589 add schedule item watermark setting (#733) 2022-04-12 21:01:20 -05:00
Jason DoveandGitHub af39d93442 more scheduling fixes (#732)
* fix skipping days with fixed start times

* fix playouts getting "stuck" on the same items

* rebuild all playouts

* update dependencies
2022-04-12 09:19:13 -05:00
Jason DoveandGitHub 5d6a6d3a76 fix schedule anchors (#726) 2022-04-10 11:29:50 -05:00
Jason DoveandGitHub 1f27aef11d allow two decimals in channel numbers (#724) 2022-04-05 20:44:50 -05:00
Jason DoveandGitHub ddb6d99cf9 remove sqlite startup messages (#723) 2022-04-03 23:37:38 -05:00
Jason DoveandGitHub d54766866e optimize image manipulation (#722)
* update dependencies

* use ffmpeg to resize images

* use ffprobe to check for animated logos and watermarks

* remove last use of imagesharp
2022-04-03 22:43:11 -05:00
Jason DoveandGitHub 25bc500a2b ensure HDHR clients always get an MPEG-TS stream (#721) 2022-04-03 18:03:34 -05:00
Jason DoveandGitHub c2eec2fc2d playout rework to maintain collection progress (#720)
* initial work on maintaining playout state

* debugging wip

* fix refresh playout logic

* fix failing test

* more fixes

* update changelog

* comment out some debug logs

* comment out more logs
2022-04-02 21:19:59 -05:00
Jason DoveandGitHub f9781a4c05 detect and handle nonzero hls segmenter exit code (#719) 2022-04-01 20:27:40 -05:00
Jason DoveandGitHub df45b93819 burn in picture-based subtitles (#718)
* add subtitle mode setting

* start to add subtitle support

* cuda test

* move subtitle settings from ffmpeg profile to channel

* fix image-based subtitles

* experimental wip

* subtitle fixes
2022-03-31 18:15:57 -05:00
Jason DoveandGitHub e697fd36e9 remove legacy transcoder logic option (#717) 2022-03-31 16:00:50 -05:00
Jason Dove 0308106c1b update changelog for release v0.4.5-alpha [no ci] 2022-03-29 20:12:01 -05:00
Jason DoveandGitHub ba93e3eeea always check for plex on the localhost (#716) 2022-03-29 19:39:55 -05:00
Jason DoveandGitHub aa2c914d8a always transcode and normalize, except with HLS Direct (#715)
* remove transcode, normalize video, normalize audio settings

* cleanup

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

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

* fix version request

* accept base url env var

* tweak colors

* fix version service

* add basic channels list

* fix auto page title for ts classes

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

* use axios to get version

* allow typescript

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

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

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

* use svg logos

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

* add channels mock api

* Initial Vue framework with Vuetify UI (#688)

* fix hls direct streaming mode (#682)

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

* first pass

* analyze zero-duration files

* add readme note for WIP

* add vuetify and basic sidebar layout - responsive

* add vue-router and initial home page

* setup composition-api for vue2

* install pinia ie... vuex4

* mixing for automatic page title

* add logo files

* tweak theme colors

* install store

* use store for menu toggle

* replicate old menu

* implement menu and children menus

* rename state to application state

* update vue files and add version to sidebar

* lock logo and make expandable list remove minified menu

* remove todo, will add to PR

* top bar links and attempt at snackbar with state

* fix snackbar

* add search basic component

* fix search bar placement

* remove un-used footer

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

This reverts commit 43016d502b.

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

* Add ESLint and Prettier for VueJS (#691)

* add prettier to config

* run npm run lint over project to clean up files

* replace hr tag with v-dividers

* add vue-lint github action

* fix the dodo in me

* Hu

* Fix path

* convert to multi-run step

* forgot the name

* add vue-lint github action

fix the dodo in me

Hu

Fix path

convert to multi-run step

forgot the name

* Fix new line at end of file

* WIP

* dockerfile consistency

* use npm ci and node v14

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

* disable filename hashing

* don't build tests in docker

* update dependencies

* fix running both uis

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

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

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

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

* optimize usings

* more usings

* more of the same

* more implicit/global usings

* cleanup all usings

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

* fix controller name

* catch and ignore jsruntime exceptions

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

* use cancellation token in shared ui

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

* qsv fixes

* cleanup

* nvidia fixes

* vaapi fixes

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

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

* nvidia fixes

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

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

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

* fix songs again

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

* test concat command output

* move concat pipeline

* start to use ffmpeg state

* add ffmpeg state and audio state

* audio state is required

* attach input options directly to input files

* move filters to input files

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

* it works

* fix tests

* update readme

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

* add (unused) framerate filter

* disable framerate normalization by default

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

* fix qsv acceleration

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

* add vaapi device name

* add FFREPORT env var

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

* log unexpected data

* vaapi fixes

* disable 444 test

* add qsv deinterlace filter; qsv fixes

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

* start to hook ffmpeg lib into main app

* improvements

* more progress

* make pipeline builder configurable

* more options

* move more logic down into ffmpeg lib

* ffmpeg lib desired state refactoring

* add software scaling and padding

* add loudness normalization and software deinterlace

* add metadata output options

* add setsar filter

* use built-in scaling logic

* fixes

* initial nvidia support

* nvidia improvements

* support hls mode

* print old arguments at debug level

* fix package reference

* start to add qsv support

* formatting

* fix tests

* add timeout to transcode tests

* show successful ffmpeg arguments

* add vaapi support

* add more software decoders

* add experimental transcoder option

* call existing ffmpeg process service for unimplemented features

* fix nvidia mpeg2video bug

* update changelog

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

* downgrade imagesharp to fix blurhash generation

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-05 18:20:45 -06:00
Jason DoveandGitHub a47987a9d7 revert windows trimming (#613)
* Revert "disable trimming in docker"

This reverts commit 5937211bb8.

* Revert "try to reduce windows artifact size"

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

* fix tests

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

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

* refactor reusable artifacts workflow

* fix name

* try to fix

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

* fix vars

* fix condition

* typo

* fix quoting

* use recursive signing script

* fix release cleanup

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

* rename step
2022-01-21 15:18:08 -06:00
Jason DoveandGitHub 685f78bef8 fix search results bug (#583) 2022-01-21 14:05:09 -06:00
Jason DoveandGitHub 3ce267863b fix hls segmenter in some cultures (#582) 2022-01-21 10:42:33 -06:00
Jason DoveandGitHub e4231cb57d upgrade from ffmpeg 4.4 to 5.0 (#581) 2022-01-20 20:57:38 -06:00
Jason DoveandGitHub 03946b13ca always use a single ffmpeg thread with realtime (#580) 2022-01-20 14:53:13 -06:00
Jason DoveandGitHub f1a81bf086 clarify library kind/media kind support (#579) [no docker] 2022-01-19 09:11:23 -06:00
Jason Dove 7a88374362 clarify flood scheduling calc [no ci] 2022-01-18 18:22:46 -06:00
Jason DoveandGitHub 663a62431b properly fix startup paths (#576) 2022-01-17 16:31:22 -06:00
Jason Dove 1d4acc284d Update changelog for release v0.3.7-alpha [no ci] 2022-01-17 15:23:39 -06:00
Jason DoveandGitHub 0440f7643b add videotoolbox acceleration (#575) 2022-01-17 15:05:23 -06:00
Jason DoveandGitHub 0f4219f731 properly unlock libraries after failed scans (#574) 2022-01-14 13:03:15 -06:00
Jason DoveandGitHub cbe5d47611 fix trakt list sync when show does not contain a year (#572) 2022-01-12 21:09:26 -06:00
Jason DoveandGitHub afa52ccc89 add trash system for local libraries (#571)
* flag local movies as file not found

* show warning icon on cards

* unflag movie that is found during scan

* skip missing files when building playouts

* add state to search index

* add file not found health check

* link to search from file not found health check

* support flagging other media kinds as file not found

* continue to schedule missing items

* support episode files not found

* wip trash page

* fix trash url

* trash page is functional

* update changelog

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

* properly index minutes field when adding from scan

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

* recognize qsv acceleration on linux

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

* update changelog

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

* use select instead of autocomplete for filler preset editor

* reset selected collection when changing collection type

* use select instead of autocomplete for multi collection editor

* more select

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

* fix saving artwork blurhash

* fix song detail alignment

* rename song background files

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

* use blurhash song background if available

* only write blur hash to disk once

* use multiple blur hashes

* update changelog

* fix song detail outline

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

* use utf8 encoding for console (logging) output

* use proper sink package

* reset song metadata on windows

* fix nfo processing with missing year

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

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

* reset all song genre metadata

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

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

* move song video generation

* move subtitle generation

* build ASS subtitles

* randomize song detail layout

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

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

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

* show song artist in playout detail

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

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

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

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

* pre-generate song image and always use software encoders

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

* use channel watermark when cover art is unavailable

* add drawtext to song filter

* cleanup

* force song cover art as png

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

* index song artist and song album

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

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

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

* start handling optional video

* fix song playback

* fix song transitions

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

* update dependencies
2021-11-21 06:25:56 -06:00
Jason DoveandGitHub 184c21a91b optimize trakt matching (#487) 2021-11-21 06:13:28 -06:00
Jason DoveandGitHub 6ea3191cf8 fix playout building (#486) 2021-11-20 22:36:15 -06:00
Jason DoveandGitHub d487bbca08 include other video title in channel guide (#483) 2021-11-16 08:46:07 -06:00
Jason Dove 05034b47e2 update changelog for release v0.2.4-alpha [no ci] 2021-11-13 12:54:41 -06:00
Jason DoveandGitHub b0c85b6478 use scale_cuda instead of scale_npp (#481) 2021-11-13 09:06:02 -06:00
Jason DoveandGitHub f1356563da fix ef shared table warnings (#480) 2021-11-10 18:03:28 -06:00
Jason DoveandGitHub c0aad028a8 more dotnet 6 updates (#479) 2021-11-09 13:09:57 -06:00
Jason DoveandGitHub dae06ec0ef upgrade to dotnet 6 (#475) 2021-11-09 07:44:34 -06:00
Jason DoveandGitHub 72f452fd36 update dependencies (#474) 2021-11-09 06:16:26 -06:00
Jason Dove aaf832c0b6 update changelog for release v0.2.3-alpha [no ci] 2021-11-03 13:53:06 -05:00
Jason DoveandGitHub 08a18daf23 movie scanner should respect .etvignore files (#468) 2021-11-03 05:47:29 -05:00
Jason DoveandGitHub 90c1c61a09 fix bug with flood playout mode (#467) 2021-11-02 21:47:46 -05:00
Jason DoveandGitHub 053db71d44 fix decimal separator in ffmpeg apad filter syntax (#464) 2021-11-01 22:18:06 -05:00
2426 changed files with 551986 additions and 53127 deletions
+1 -7
View File
@@ -3,16 +3,10 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2021.1.3",
"version": "2023.2.0",
"commands": [
"jb"
]
},
"swashbuckle.aspnetcore.cli": {
"version": "5.6.2",
"commands": [
"swagger"
]
}
}
}
+6 -1
View File
@@ -1,6 +1,6 @@
[*]
charset=utf-8-bom
charset=utf-8
end_of_line=lf
trim_trailing_whitespace=false
insert_final_newline=false
@@ -83,3 +83,8 @@ tab_width=4
[*.yml]
indent_style = space
indent_size = 2
[*.cs]
# disable CA1848: Use the LoggerMessage delegates`
dotnet_diagnostic.ca1848.severity = none
+18
View File
@@ -6,3 +6,21 @@ updates:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/nvidia"
schedule:
interval: daily
assignees:
- jasongdove
- package-ecosystem: docker
directory: "/docker/vaapi"
schedule:
interval: daily
assignees:
- jasongdove
+265
View File
@@ -0,0 +1,265 @@
name: Build Artifacts
on:
workflow_call:
inputs:
release_tag:
description: 'Release tag'
required: true
type: string
release_version:
description: 'Release version number (e.g. v0.3.7-alpha)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
secrets:
apple_developer_certificate_p12_base64:
required: true
apple_developer_certificate_password:
required: true
ac_username:
required: true
ac_password:
required: true
gh_token:
required: true
jobs:
build_and_upload_mac:
name: Mac Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: macos-11
kind: macOS
target: osx-x64
- os: macos-11
kind: macOS
target: osx-arm64
steps:
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
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
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target}}"
- name: Import Code-Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.apple_developer_certificate_p12_base64 }}
p12-password: ${{ secrets.apple_developer_certificate_password }}
- name: Calculate Release Name
shell: bash
run: |
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
- name: Build
shell: bash
run: |
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
- name: Bundle
shell: bash
run: |
brew install coreutils
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
scripts/macOS/bundle.sh
- name: Sign
shell: bash
run: scripts/macOS/sign.sh
- name: Create DMG
shell: bash
run: |
brew install create-dmg
create-dmg \
--volname "ErsatzTV" \
--volicon "artwork/ErsatzTV.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "ErsatzTV.app" 200 190 \
--hide-extension "ErsatzTV.app" \
--app-drop-link 600 185 \
--skip-jenkins \
"ErsatzTV.dmg" \
"ErsatzTV.app/"
- name: Notarize
shell: bash
run: |
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
run: |
mv ErsatzTV.dmg "${{ env.RELEASE_NAME }}.dmg"
rm -r publish
rm -r ErsatzTV.app
- name: Delete old release assets
uses: mknejp/delete-release-assets@v1
if: ${{ inputs.release_tag == 'develop' }}
with:
token: ${{ secrets.gh_token }}
tag: ${{ inputs.release_tag }}
fail-if-no-assets: false
assets: |
*${{ matrix.target }}.dmg
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.dmg
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
build_and_upload:
name: Build & Upload
runs-on: ${{ matrix.os }}
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: ubuntu-latest
kind: linux
target: linux-arm64
- os: windows-latest
kind: windows
target: win-x64
steps:
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
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
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore -r "${{ matrix.target }}"
- uses: suisei-cn/actions-download-file@v1.3.0
if: ${{ matrix.kind == 'windows' }}
id: downloadffmpeg
name: Download ffmpeg
with:
url: "https://github.com/ErsatzTV/ErsatzTV-ffmpeg/releases/download/6.1-working-cuvid/ffmpeg-6.1-working-cuvid.7z"
target: ffmpeg/
- name: Build
shell: bash
run: |
# Define some variables for things we need
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
# Build everything
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
# Delete output directory
rm -r "$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: |
*${{ matrix.target }}.zip
*${{ matrix.target }}.tar.gz
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ inputs.release_tag }}
files: |
${{ env.RELEASE_NAME }}.zip
${{ env.RELEASE_NAME }}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.gh_token }}
+41 -93
View File
@@ -1,110 +1,58 @@
name: Build
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
jobs:
build_and_test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ windows-latest, ubuntu-latest, macos-latest ]
calculate_version:
name: Calculate version information
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
build_and_push:
name: Build & Publish to Docker Hub
needs: build_and_test
runs-on: ubuntu-latest
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract Git Tag
- name: Extract Docker Tag
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
tag2="${tag:1}"
short=$(git rev-parse --short HEAD)
final="${tag2/alpha/$short}"
final="${tag2/beta/$short}"
echo "GIT_TAG=${final}" >> $GITHUB_ENV
- name: Set up Docker Buildx Base
uses: docker/setup-buildx-action@v1
id: builder-base
- name: Set up Docker Buildx NVIDIA
uses: docker/setup-buildx-action@v1
id: builder-nvidia
- name: Set up Docker Buildx VAAPI
uses: docker/setup-buildx-action@v1
id: builder-vaapi
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push base
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-base.outputs.name }}
context: .
file: ./docker/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker
tags: |
jasongdove/ersatztv:develop
jasongdove/ersatztv:${{ github.sha }}
- name: Build and push nvidia
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:develop-nvidia
jasongdove/ersatztv:${{ github.sha }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:develop-vaapi
jasongdove/ersatztv:${{ github.sha }}-vaapi
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
short=$(git rev-parse --short HEAD)
final="${tag/beta/$short}"
echo "ARTIFACTS_VERSION=${final}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
git_tag: ${{ env.GIT_TAG }}
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: develop
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: develop
info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ github.sha }}
secrets:
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+112
View File
@@ -0,0 +1,112 @@
name: Build & Publish to Docker Hub
on:
workflow_call:
inputs:
base_version:
description: 'Base version (latest or develop)'
required: true
type: string
info_version:
description: 'Informational version number (e.g. 0.3.7-alpha)'
required: true
type: string
tag_version:
description: 'Docker tag version (e.g. v0.3.7)'
required: true
type: string
secrets:
docker_hub_username:
required: true
docker_hub_access_token:
required: true
jobs:
build_and_push:
name: Build & Publish
runs-on: ubuntu-latest
if: contains(github.event.head_commit.message, '[no build]') == false
strategy:
matrix:
include:
- name: base
path: ''
suffix: ''
qemu: false
- name: nvidia
path: 'nvidia/'
suffix: '-nvidia'
qemu: false
- name: vaapi
path: 'vaapi/'
suffix: '-vaapi'
qemu: false
- name: arm32v7
path: 'arm32v7/'
suffix: '-arm'
qemu: true
- name: arm64
path: 'arm64/'
suffix: '-arm64'
qemu: true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
if: ${{ matrix.qemu == true }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
id: docker-buildx
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.docker_hub_username }}
password: ${{ secrets.docker_hub_access_token }}
- name: Build and push
uses: docker/build-push-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' }}
- name: Build and push
uses: docker/build-push-action@v3
with:
builder: ${{ steps.docker-buildx.outputs.name }}
context: .
file: ./docker/${{ matrix.path }}Dockerfile
push: true
platforms: 'linux/arm64'
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' }}
- name: Build and push
uses: docker/build-push-action@v3
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' }}
-19
View File
@@ -1,19 +0,0 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- main
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: ersatztv.org
+92
View File
@@ -0,0 +1,92 @@
name: Pull Request
on:
pull_request:
jobs:
build_and_test_windows:
runs-on: windows-latest
steps:
- name: Get the sources
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Prep project file
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
- name: Build Windows
run: |
cd ErsatzTV-Windows
cargo build --release --all-features
build_and_test_linux:
runs-on: ubuntu-latest
steps:
- name: Get the sources
uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Prep project file
run: sed -i '/Scanner/d' ErsatzTV/ErsatzTV.csproj
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
build_and_test_mac:
runs-on: macos-11
steps:
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: true
- name: Setup .NET Core
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Prep project file
run: sed -i '' '/Scanner/d' ErsatzTV/ErsatzTV.csproj
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
+41 -130
View File
@@ -1,142 +1,53 @@
name: Publish
name: Release
on:
release:
types: [ published ]
jobs:
release:
name: Release
strategy:
matrix:
include:
- os: ubuntu-latest
kind: linux
target: linux-x64
- os: ubuntu-latest
kind: linux
target: linux-arm
- os: windows-latest
kind: windows
target: win-x64
- os: macos-latest
kind: maxOS
target: osx-x64
runs-on: ${{ matrix.os }}
steps:
- name: Get the sources
uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
- name: Clean
run: dotnet clean --configuration Release && dotnet nuget locals all --clear
- name: Install dependencies
run: dotnet restore
- name: Build
shell: bash
run: |
# Define some variables for things we need
tag=$(git describe --tags --abbrev=0)
release_name="ErsatzTV-$tag-${{ matrix.target }}"
# Build everything
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net5.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:PublishSingleFile=true --self-contained true
# Pack files
if [ "${{ matrix.target }}" == "win-x64" ]; then
7z a -tzip "${release_name}.zip" "./${release_name}/*"
elif [ "${{ matrix.target }}" == "linux-arm" ]; then
cp lib/linux-arm/* "$release_name/"
tar czvf "${release_name}.tar.gz" "$release_name"
else
tar czvf "${release_name}.tar.gz" "$release_name"
fi
# Delete output directory
rm -r "$release_name"
- name: Publish
uses: softprops/action-gh-release@v1
with:
prerelease: true
files: |
ErsatzTV*.zip
ErsatzTV*.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
name: Build & Publish to Docker Hub
calculate_version:
name: Calculate version information
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Get the sources
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract Git Tag
- name: Extract Docker Tag
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
echo "GIT_TAG=${tag:1}" >> $GITHUB_ENV
echo "DOCKER_TAG=${tag/-alpha/}" >> $GITHUB_ENV
- name: Set up Docker Buildx Base
uses: docker/setup-buildx-action@v1
id: builder-base
- name: Set up Docker Buildx NVIDIA
uses: docker/setup-buildx-action@v1
id: builder-nvidia
- name: Set up Docker Buildx VAAPI
uses: docker/setup-buildx-action@v1
id: builder-vaapi
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push base
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-base.outputs.name }}
context: .
file: ./docker/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker
tags: |
jasongdove/ersatztv:latest
jasongdove/ersatztv:${{ env.DOCKER_TAG }}
- name: Build and push nvidia
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-nvidia.outputs.name }}
context: .
file: ./docker/nvidia/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-nvidia
tags: |
jasongdove/ersatztv:latest-nvidia
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-nvidia
- name: Build and push vaapi
uses: docker/build-push-action@v2
with:
builder: ${{ steps.builder-vaapi.outputs.name }}
context: .
file: ./docker/vaapi/Dockerfile
push: true
build-args: |
INFO_VERSION=${{ env.GIT_TAG }}-docker-vaapi
tags: |
jasongdove/ersatztv:latest-vaapi
jasongdove/ersatztv:${{ env.DOCKER_TAG }}-vaapi
echo "DOCKER_TAG=${tag/-beta/}" >> $GITHUB_ENV
- name: Extract Artifacts Version
shell: bash
run: |
tag=$(git describe --tags --abbrev=0)
echo "ARTIFACTS_VERSION=${tag}" >> $GITHUB_ENV
echo "INFO_VERSION=${tag:1}" >> $GITHUB_ENV
outputs:
git_tag: ${{ env.GIT_TAG }}
docker_tag: ${{ env.DOCKER_TAG }}
artifacts_version: ${{ env.ARTIFACTS_VERSION }}
info_version: ${{ env.INFO_VERSION }}
build_and_upload:
uses: ersatztv/ersatztv/.github/workflows/artifacts.yml@main
needs: calculate_version
with:
release_tag: ${{ needs.calculate_version.outputs.artifacts_version }}
release_version: ${{ needs.calculate_version.outputs.artifacts_version }}
info_version: ${{ needs.calculate_version.outputs.info_version }}
secrets:
apple_developer_certificate_p12_base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
apple_developer_certificate_password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
ac_username: ${{ secrets.AC_USERNAME }}
ac_password: ${{ secrets.AC_PASSWORD }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
build_and_push:
uses: ersatztv/ersatztv/.github/workflows/docker.yml@main
needs: calculate_version
with:
base_version: latest
info_version: ${{ needs.calculate_version.outputs.git_tag }}
tag_version: ${{ needs.calculate_version.outputs.docker_tag }}
secrets:
docker_hub_username: ${{ secrets.DOCKER_HUB_USERNAME }}
docker_hub_access_token: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+22
View File
@@ -0,0 +1,22 @@
name: Lint VueJS Files on PR Request
on:
pull_request:
jobs:
vue-lint:
runs-on: ubuntu-latest
steps:
# Checkout the current repo
- name: Checkout current repository
uses: actions/checkout@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
View File
@@ -43,3 +43,4 @@ scripts/generate-api-sdk/swagger.json
docker-compose.override.yml
ErsatzTV/wwwroot/v2/
+3
View File
@@ -0,0 +1,3 @@
[submodule "ErsatzTV-macOS"]
path = ErsatzTV-macOS
url = git@github.com:ErsatzTV/ErsatzTV-macOS.git
+1144 -69
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
target/
+1035
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "ersatztv_windows"
version = "0.1.0"
edition = "2021"
[dependencies]
tray-item = { git = "https://github.com/olback/tray-item-rs" }
special-folder = { git = "https://github.com/masinc/special-folder-rs" }
process_path = "0.1.4"
[dependencies.windows]
version = "0.43.0"
features = [
"Win32_System_Console",
"Win32_Foundation"
]
[build-dependencies]
windres = "*"
static_vcruntime = "2.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+6
View File
@@ -0,0 +1,6 @@
use windres::Build;
fn main() {
static_vcruntime::metabuild();
Build::new().compile("ersatztv_windows.rc").unwrap();
}
+2
View File
@@ -0,0 +1,2 @@
id ICON "ersatztv.ico"
ersatztv-icon ICON "ersatztv.ico"
+112
View File
@@ -0,0 +1,112 @@
#![windows_subsystem = "windows"]
use special_folder::SpecialFolder;
use std::fs;
use std::os::windows::process::CommandExt;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
use windows::Win32::System::Console;
use {std::sync::mpsc, tray_item::TrayItem};
const CREATE_NO_WINDOW: u32 = 0x08000000;
enum Message {
Exit,
}
fn main() {
let mut tray = TrayItem::new("ErsatzTV", "ersatztv-icon").unwrap();
let (tx, rx) = mpsc::channel();
tray.add_menu_item("Launch Web UI", || {
let _ = Command::new("cmd")
.creation_flags(CREATE_NO_WINDOW)
.arg("/C")
.arg("start")
.arg("http://localhost:8409")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
})
.unwrap();
tray.add_menu_item("Show Logs", || {
let path = SpecialFolder::LocalApplicationData
.get()
.unwrap()
.join("ersatztv")
.join("logs");
match path.to_str() {
None => {}
Some(folder) => {
fs::create_dir_all(folder).unwrap();
let _ = Command::new("cmd")
.creation_flags(CREATE_NO_WINDOW)
.arg("/C")
.arg("start")
.arg(folder)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
})
.unwrap();
tray.inner_mut().add_separator().unwrap();
tray.add_menu_item("Exit", move || {
tx.send(Message::Exit).unwrap();
})
.unwrap();
let path = process_path::get_executable_path();
let mut child: Option<Child> = None;
match path {
None => {}
Some(path) => {
let etv = path.parent().unwrap().join("ErsatzTV.exe");
if etv.exists() {
match etv.to_str() {
None => {}
Some(etv) => {
child = Some(
Command::new(etv)
.creation_flags(CREATE_NO_WINDOW)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap(),
);
}
}
}
}
}
loop {
match rx.recv() {
Ok(Message::Exit) => {
match child {
None => {}
Some(mut child) => {
unsafe {
if Console::AttachConsole(child.id()) == true
{
Console::GenerateConsoleCtrlEvent(Console::CTRL_C_EVENT, 0);
}
}
child.wait().unwrap();
}
}
break;
}
_ => {}
}
}
}
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
+5
View File
@@ -0,0 +1,5 @@
[*.cs]
# disable CA1711: Identifiers should not have incorrect suffix
dotnet_diagnostic.ca1711.severity = none
# disable CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.ca1848.severity = none
+13 -15
View File
@@ -1,16 +1,14 @@
using System.Collections.Generic;
using System.Globalization;
using System.Globalization;
namespace ErsatzTV.Application.Artists
{
public record ArtistViewModel(
string Name,
string Disambiguation,
string Biography,
string Thumbnail,
string FanArt,
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
}
namespace ErsatzTV.Application.Artists;
public record ArtistViewModel(
string Name,
string Disambiguation,
string Biography,
string Thumbnail,
string FanArt,
List<string> Genres,
List<string> Styles,
List<string> Moods,
List<CultureInfo> Languages);
+30 -37
View File
@@ -1,46 +1,39 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using ErsatzTV.Core.Domain;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Artists
namespace ErsatzTV.Application.Artists;
internal static class Mapper
{
internal static class Mapper
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
{
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
return new ArtistViewModel(
metadata.Title,
metadata.Disambiguation,
metadata.Biography,
Artwork(metadata, ArtworkKind.Thumbnail),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(),
metadata.Moods.Map(m => m.Name).ToList(),
LanguagesForArtist(languages));
}
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
return new ArtistViewModel(
metadata.Title,
metadata.Disambiguation,
metadata.Biography,
Artwork(metadata, ArtworkKind.Thumbnail),
Artwork(metadata, ArtworkKind.FanArt),
metadata.Genres.Map(g => g.Name).ToList(),
metadata.Styles.Map(s => s.Name).ToList(),
metadata.Moods.Map(m => m.Name).ToList(),
LanguagesForArtist(languages));
}
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
.Match(a => a.Path, string.Empty);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
{
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
return languages
.Distinct()
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Sequence()
.Flatten()
.ToList();
}
return languages
.Map(
lang => allCultures.Filter(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
.Flatten()
.Distinct()
.ToList();
}
}
@@ -1,8 +1,5 @@
using System.Collections.Generic;
using ErsatzTV.Application.MediaItems;
using MediatR;
using ErsatzTV.Application.MediaItems;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
}
namespace ErsatzTV.Application.Artists;
public record GetAllArtists : IRequest<List<NamedMediaItemViewModel>>;
@@ -1,29 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.MediaItems.Mapper;
namespace ErsatzTV.Application.Artists.Queries
namespace ErsatzTV.Application.Artists;
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
{
public class GetAllArtistsHandler : IRequestHandler<GetAllArtists, List<NamedMediaItemViewModel>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetAllArtistsHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
CancellationToken cancellationToken)
{
private readonly IArtistRepository _artistRepository;
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
public GetAllArtistsHandler(IArtistRepository artistRepository) => _artistRepository = artistRepository;
List<Artist> allArtists = await dbContext.Artists
.AsNoTracking()
.Include(a => a.ArtistMetadata)
.ToListAsync(cancellationToken);
public Task<List<NamedMediaItemViewModel>> Handle(
GetAllArtists request,
CancellationToken cancellationToken) =>
_artistRepository.GetAllArtists()
.Map(
list => list.Filter(
a => !string.IsNullOrWhiteSpace(
a.ArtistMetadata.HeadOrNone().Match(am => am.Title, () => string.Empty))))
.Map(list => list.Map(ProjectToViewModel).ToList());
return allArtists.Bind(a => ProjectArtist(a)).ToList();
}
private static Option<NamedMediaItemViewModel> ProjectArtist(Artist a)
{
foreach (ArtistMetadata metadata in a.ArtistMetadata.HeadOrNone())
{
if (!string.IsNullOrWhiteSpace(metadata.Title))
{
return ProjectToViewModel(a);
}
}
return None;
}
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Artists;
namespace ErsatzTV.Application.Artists.Queries
{
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
}
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
@@ -1,38 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using static ErsatzTV.Application.Artists.Mapper;
namespace ErsatzTV.Application.Artists.Queries
namespace ErsatzTV.Application.Artists;
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
{
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
private readonly IArtistRepository _artistRepository;
private readonly ISearchRepository _searchRepository;
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
{
_artistRepository = artistRepository;
_searchRepository = searchRepository;
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
public async Task<Option<ArtistViewModel>> Handle(
GetArtistById request,
CancellationToken cancellationToken)
{
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
async artist =>
{
List<string> mediaCodes = await _searchRepository.GetLanguagesForArtist(artist);
List<string> languageCodes = await _searchRepository.GetAllLanguageCodes(mediaCodes);
return ProjectToViewModel(artist, languageCodes);
},
() => Task.FromResult(Option<ArtistViewModel>.None));
}
}
@@ -1,15 +1,22 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels
{
public record ChannelViewModel(
int Id,
string Number,
string Name,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId);
}
namespace ErsatzTV.Application.Channels;
public record ChannelViewModel(
int Id,
string Number,
string Name,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
int PlayoutCount,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate);
@@ -1,18 +1,22 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannel
(
string Name,
string Number,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, CreateChannelResult>>;
}
namespace ErsatzTV.Application.Channels;
public record CreateChannel
(
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>;
@@ -1,162 +1,177 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
{
public class CreateChannelHandler : IRequestHandler<CreateChannel, Either<BaseError, CreateChannelResult>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
public CreateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) => _dbContextFactory = dbContextFactory;
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
public async Task<Either<BaseError, CreateChannelResult>> Handle(
CreateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => PersistChannel(dbContext, c));
}
private static async Task<CreateChannelResult> PersistChannel(TvContext dbContext, Channel channel)
{
await dbContext.Channels.AddAsync(channel);
await dbContext.SaveChangesAsync();
return new CreateChannelResult(channel.Id);
}
private static async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, CreateChannel request) =>
(ValidateName(request), await ValidateNumber(dbContext, request),
await FFmpegProfileMustExist(dbContext, request),
ValidatePreferredLanguage(request),
await WatermarkMustExist(dbContext, request),
await FillerPresetMustExist(dbContext, request))
.Apply(
(name, number, ffmpegProfileId, preferredLanguageCode, watermarkId, fillerPresetId) =>
{
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
var channel = new Channel(Guid.NewGuid())
{
Name = name,
Number = number,
FFmpegProfileId = ffmpegProfileId,
StreamingMode = request.StreamingMode,
Artwork = artwork,
PreferredLanguageCode = preferredLanguageCode
};
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private static Validation<BaseError, string> ValidatePreferredLanguage(CreateChannel createChannel) =>
Optional(createChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
private static async Task<Validation<BaseError, string>> ValidateNumber(TvContext dbContext, CreateChannel createChannel)
{
Option<Channel> maybeExistingChannel = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"),
() =>
{
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
});
}
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
TvContext dbContext,
CreateChannel createChannel) =>
dbContext.FFmpegProfiles
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => createChannel.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
private static async Task<Validation<BaseError, Option<int>>> WatermarkMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.WatermarkId is null)
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) =>
{
return Option<int>.None;
}
var artwork = new List<Artwork>();
if (!string.IsNullOrWhiteSpace(request.Logo))
{
artwork.Add(
new Artwork
{
Path = request.Logo,
ArtworkKind = ArtworkKind.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
});
}
return await dbContext.ChannelWatermarks
.CountAsync(w => w.Id == createChannel.WatermarkId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.WatermarkId))
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
}
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
};
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
foreach (int id in watermarkId)
{
channel.WatermarkId = id;
}
foreach (int id in fillerPresetId)
{
channel.FallbackFillerId = id;
}
return channel;
});
private static Validation<BaseError, string> ValidateName(CreateChannel createChannel) =>
createChannel.NotEmpty(c => c.Name)
.Bind(_ => createChannel.NotLongerThan(50)(c => c.Name));
private static Validation<BaseError, string> 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)
{
Option<Channel> maybeExistingChannel = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == createChannel.Number);
return maybeExistingChannel.Match<Validation<BaseError, string>>(
_ => BaseError.New("Channel number must be unique"),
() =>
{
return Option<int>.None;
}
if (Regex.IsMatch(createChannel.Number, Channel.NumberValidator))
{
return createChannel.Number;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
return BaseError.New("Invalid channel number; two decimals are allowed for subchannels");
});
}
private static Task<Validation<BaseError, int>> FFmpegProfileMustExist(
TvContext dbContext,
CreateChannel createChannel) =>
dbContext.FFmpegProfiles
.CountAsync(p => p.Id == createChannel.FFmpegProfileId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => createChannel.FFmpegProfileId)
.Map(o => o.ToValidation<BaseError>($"FFmpegProfile {createChannel.FFmpegProfileId} does not exist."));
private static async Task<Validation<BaseError, Option<int>>> WatermarkMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.WatermarkId is null)
{
return Option<int>.None;
}
return await dbContext.ChannelWatermarks
.CountAsync(w => w.Id == createChannel.WatermarkId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.WatermarkId))
.Map(o => o.ToValidation<BaseError>($"Watermark {createChannel.WatermarkId} does not exist."));
}
private static async Task<Validation<BaseError, Option<int>>> FillerPresetMustExist(
TvContext dbContext,
CreateChannel createChannel)
{
if (createChannel.FallbackFillerId is null)
{
return Option<int>.None;
}
return await dbContext.FillerPresets
.Filter(fp => fp.FillerKind == FillerKind.Fallback)
.CountAsync(w => w.Id == createChannel.FallbackFillerId)
.Map(Optional)
.Filter(c => c > 0)
.MapT(_ => Optional(createChannel.FallbackFillerId))
.Map(
o => o.ToValidation<BaseError>(
$"Fallback filler {createChannel.FallbackFillerId} does not exist."));
}
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Channels.Commands
{
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
}
namespace ErsatzTV.Application.Channels;
public record CreateChannelResult(int ChannelId) : EntityIdResult(ChannelId);
@@ -1,9 +1,5 @@
using System.Threading.Tasks;
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Channels.Commands
{
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Task>>;
}
namespace ErsatzTV.Application.Channels;
public record DeleteChannel(int ChannelId) : IRequest<Either<BaseError, Unit>>;
@@ -1,28 +1,61 @@
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Unit>>
{
public class DeleteChannelHandler : IRequestHandler<DeleteChannel, Either<BaseError, Task>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public DeleteChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem)
{
private readonly IChannelRepository _channelRepository;
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
}
public DeleteChannelHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
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);
public async Task<Either<BaseError, Task>> Handle(DeleteChannel request, CancellationToken cancellationToken) =>
(await ChannelMustExist(request))
.Map(DoDeletion)
.ToEither<Task>();
return await LanguageExtensions.Apply(validation, c => DoDeletion(dbContext, c, cancellationToken));
}
private Task DoDeletion(int channelId) => _channelRepository.Delete(channelId);
private async Task<Unit> DoDeletion(TvContext dbContext, Channel channel, CancellationToken cancellationToken)
{
dbContext.Channels.Remove(channel);
await dbContext.SaveChangesAsync(cancellationToken);
private async Task<Validation<BaseError, int>> ChannelMustExist(DeleteChannel deleteChannel) =>
(await _channelRepository.Get(deleteChannel.ChannelId))
.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.")
.Map(c => c.Id);
// delete channel data from channel guide cache
string cacheFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channel.Number}.xml");
if (_localFileSystem.FileExists(cacheFile))
{
File.Delete(cacheFile);
}
// refresh channel list to remove channel that has no playout
await _workerChannel.WriteAsync(new RefreshChannelList(), cancellationToken);
return Unit.Default;
}
private static async Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
DeleteChannel deleteChannel)
{
Option<Channel> maybeChannel = await dbContext.Channels
.SelectOneAsync(c => c.Id, c => c.Id == deleteChannel.ChannelId);
return maybeChannel.ToValidation<BaseError>($"Channel {deleteChannel.ChannelId} does not exist.");
}
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record RefreshChannelData(string ChannelNumber) : IRequest, IBackgroundServiceRequest;
@@ -0,0 +1,525 @@
using System.Globalization;
using System.Xml;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
namespace ErsatzTV.Application.Channels;
public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<RefreshChannelDataHandler> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelDataHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem,
ILogger<RefreshChannelDataHandler> logger)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
_logger = logger;
}
public async Task Handle(RefreshChannelData request, CancellationToken cancellationToken)
{
_localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
List<PlayoutItem> sorted = await dbContext.Playouts
.AsNoTracking()
.Filter(pi => pi.Channel.Number == request.ChannelNumber)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Episode).EpisodeMetadata)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Episode).Season)
.ThenInclude(s => s.Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(em => em.Genres)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Movie).MovieMetadata)
.ThenInclude(mm => mm.Genres)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mm => mm.Artwork)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as MusicVideo).MusicVideoMetadata)
.ThenInclude(mvm => mvm.Genres)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as MusicVideo).Artist)
.ThenInclude(a => a.ArtistMetadata)
.ThenInclude(am => am.Genres)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as OtherVideo).OtherVideoMetadata)
.ThenInclude(vm => vm.Artwork)
.Include(p => p.Items)
.ThenInclude(i => i.MediaItem)
.ThenInclude(i => (i as Song).SongMetadata)
.ThenInclude(vm => vm.Artwork)
.ToListAsync(cancellationToken)
.Map(list => list.Collect(p => p.Items).OrderBy(pi => pi.Start).ToList());
using MemoryStream ms = _recyclableMemoryStreamManager.GetStream();
await using var xml = XmlWriter.Create(
ms,
new XmlWriterSettings { Async = true, ConformanceLevel = ConformanceLevel.Fragment });
// skip all filler that isn't pre-roll
var i = 0;
while (i < sorted.Count && sorted[i].FillerKind != FillerKind.None &&
sorted[i].FillerKind != FillerKind.PreRoll)
{
i++;
}
while (i < sorted.Count)
{
PlayoutItem startItem = sorted[i];
int j = i;
while (sorted[j].FillerKind != FillerKind.None && j + 1 < sorted.Count)
{
j++;
}
PlayoutItem displayItem = sorted[j];
bool hasCustomTitle = !string.IsNullOrWhiteSpace(startItem.CustomTitle);
int finishIndex = j;
while (finishIndex + 1 < sorted.Count && (sorted[finishIndex + 1].GuideGroup == startItem.GuideGroup
|| sorted[finishIndex + 1].FillerKind is FillerKind.GuideMode
or FillerKind.Tail or FillerKind.Fallback))
{
finishIndex++;
}
int customShowId = -1;
if (displayItem.MediaItem is Episode ep)
{
customShowId = ep.Season.ShowId;
}
bool isSameCustomShow = hasCustomTitle;
for (int x = j; x <= finishIndex; x++)
{
isSameCustomShow = isSameCustomShow && sorted[x].MediaItem is Episode e &&
customShowId == e.Season.ShowId;
}
PlayoutItem finishItem = sorted[finishIndex];
i = finishIndex;
string start = startItem.StartOffset.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty);
string stop = displayItem.GuideFinishOffset.HasValue
? displayItem.GuideFinishOffset.Value.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty)
: finishItem.FinishOffset.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
.Replace(":", string.Empty);
string title = GetTitle(displayItem);
string subtitle = GetSubtitle(displayItem);
string description = GetDescription(displayItem);
Option<ContentRating> contentRating = GetContentRating(displayItem);
await xml.WriteStartElementAsync(null, "programme", null);
await xml.WriteAttributeStringAsync(null, "start", null, start);
await xml.WriteAttributeStringAsync(null, "stop", null, stop);
await xml.WriteAttributeStringAsync(null, "channel", null, $"{request.ChannelNumber}.etv");
await xml.WriteStartElementAsync(null, "title", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(title);
await xml.WriteEndElementAsync(); // title
if (!string.IsNullOrWhiteSpace(subtitle))
{
await xml.WriteStartElementAsync(null, "sub-title", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(subtitle);
await xml.WriteEndElementAsync(); // subtitle
}
if (!isSameCustomShow)
{
if (!string.IsNullOrWhiteSpace(description))
{
await xml.WriteStartElementAsync(null, "desc", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(description);
await xml.WriteEndElementAsync(); // desc
}
}
if (!hasCustomTitle && displayItem.MediaItem is Movie movie)
{
foreach (MovieMetadata metadata in movie.MovieMetadata.HeadOrNone())
{
if (metadata.Year.HasValue)
{
await xml.WriteStartElementAsync(null, "date", null);
await xml.WriteStringAsync(metadata.Year.Value.ToString(CultureInfo.InvariantCulture));
await xml.WriteEndElementAsync(); // date
}
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync("Movie");
await xml.WriteEndElementAsync(); // category
foreach (Genre genre in Optional(metadata.Genres).Flatten().OrderBy(g => g.Name))
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(genre.Name);
await xml.WriteEndElementAsync(); // category
}
string poster = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone()
.Match(a => GetArtworkUrl(a, ArtworkKind.Poster), () => string.Empty);
if (!string.IsNullOrWhiteSpace(poster))
{
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, poster);
await xml.WriteEndElementAsync(); // icon
}
}
}
if (!hasCustomTitle && displayItem.MediaItem is MusicVideo musicVideo)
{
foreach (MusicVideoMetadata metadata in musicVideo.MusicVideoMetadata.HeadOrNone())
{
if (metadata.Year.HasValue)
{
await xml.WriteStartElementAsync(null, "date", null);
await xml.WriteStringAsync(metadata.Year.Value.ToString(CultureInfo.InvariantCulture));
await xml.WriteEndElementAsync(); // date
}
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync("Music");
await xml.WriteEndElementAsync(); // category
// music video genres
foreach (Genre genre in Optional(metadata.Genres).Flatten().OrderBy(g => g.Name))
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(genre.Name);
await xml.WriteEndElementAsync(); // category
}
// artist genres
Option<ArtistMetadata> maybeMetadata =
Optional(musicVideo.Artist?.ArtistMetadata.HeadOrNone()).Flatten();
foreach (ArtistMetadata artistMetadata in maybeMetadata)
{
foreach (Genre genre in Optional(artistMetadata.Genres).Flatten().OrderBy(g => g.Name))
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(genre.Name);
await xml.WriteEndElementAsync(); // category
}
}
string artworkPath = GetPrioritizedArtworkPath(metadata);
if (!string.IsNullOrWhiteSpace(artworkPath))
{
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, artworkPath);
await xml.WriteEndElementAsync(); // icon
}
}
}
if (!hasCustomTitle && displayItem.MediaItem is Song song)
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync("Music");
await xml.WriteEndElementAsync(); // category
foreach (SongMetadata metadata in song.SongMetadata.HeadOrNone())
{
string artworkPath = GetPrioritizedArtworkPath(metadata);
if (!string.IsNullOrWhiteSpace(artworkPath))
{
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, artworkPath);
await xml.WriteEndElementAsync(); // icon
}
}
}
if (displayItem.MediaItem is Episode episode && (!hasCustomTitle || isSameCustomShow))
{
Option<ShowMetadata> maybeMetadata =
Optional(episode.Season?.Show?.ShowMetadata.HeadOrNone()).Flatten();
foreach (ShowMetadata metadata in maybeMetadata)
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync("Series");
await xml.WriteEndElementAsync(); // category
foreach (Genre genre in Optional(metadata.Genres).Flatten().OrderBy(g => g.Name))
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(genre.Name);
await xml.WriteEndElementAsync(); // category
}
string artworkPath = GetPrioritizedArtworkPath(metadata);
if (!string.IsNullOrWhiteSpace(artworkPath))
{
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, artworkPath);
await xml.WriteEndElementAsync(); // icon
}
}
if (!isSameCustomShow)
{
int s = await Optional(episode.Season?.SeasonNumber).IfNoneAsync(-1);
// TODO: multi-episode?
int e = episode.EpisodeMetadata.HeadOrNone().Match(em => em.EpisodeNumber, -1);
if (s >= 0 && e > 0)
{
await xml.WriteStartElementAsync(null, "episode-num", null);
await xml.WriteAttributeStringAsync(null, "system", null, "onscreen");
await xml.WriteStringAsync($"S{s:00}E{e:00}");
await xml.WriteEndElementAsync(); // episode-num
await xml.WriteStartElementAsync(null, "episode-num", null);
await xml.WriteAttributeStringAsync(null, "system", null, "xmltv_ns");
await xml.WriteStringAsync($"{s - 1}.{e - 1}.0/1");
await xml.WriteEndElementAsync(); // episode-num
}
}
}
await xml.WriteStartElementAsync(null, "previously-shown", null);
await xml.WriteEndElementAsync(); // previously-shown
foreach (ContentRating rating in contentRating)
{
await xml.WriteStartElementAsync(null, "rating", null);
foreach (string system in rating.System)
{
await xml.WriteAttributeStringAsync(null, "system", null, system);
}
await xml.WriteStartElementAsync(null, "value", null);
await xml.WriteStringAsync(rating.Value);
await xml.WriteEndElementAsync(); // value
await xml.WriteEndElementAsync(); // rating
}
await xml.WriteEndElementAsync(); // programme
i++;
}
await xml.FlushAsync();
string tempFile = Path.GetTempFileName();
await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken);
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{request.ChannelNumber}.xml");
File.Move(tempFile, targetFile, true);
}
private static string GetArtworkUrl(Artwork artwork, ArtworkKind artworkKind)
{
string artworkPath = artwork.Path;
int height = artworkKind switch
{
ArtworkKind.Thumbnail => 220,
_ => 440
};
if (artworkPath.StartsWith("jellyfin://", StringComparison.OrdinalIgnoreCase))
{
artworkPath = JellyfinUrl.PlaceholderProxyForArtwork(artworkPath, artworkKind, height);
}
else if (artworkPath.StartsWith("emby://", StringComparison.OrdinalIgnoreCase))
{
artworkPath = EmbyUrl.PlaceholderProxyForArtwork(artworkPath, artworkKind, height);
}
else
{
string artworkFolder = artworkKind switch
{
ArtworkKind.Thumbnail => "thumbnails",
_ => "posters"
};
artworkPath = $"{{RequestBase}}/iptv/artwork/{artworkFolder}/{artwork.Path}.jpg{{AccessTokenUri}}";
}
return artworkPath;
}
private static string GetTitle(PlayoutItem playoutItem)
{
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
{
return playoutItem.CustomTitle;
}
return playoutItem.MediaItem switch
{
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Title ?? string.Empty)
.IfNone("[unknown movie]"),
Episode e => e.Season.Show.ShowMetadata.HeadOrNone().Map(em => em.Title ?? string.Empty)
.IfNone("[unknown show]"),
MusicVideo mv => mv.Artist.ArtistMetadata.HeadOrNone().Map(am => am.Title ?? string.Empty)
.IfNone("[unknown artist]"),
OtherVideo ov => ov.OtherVideoMetadata.HeadOrNone().Map(vm => vm.Title ?? string.Empty)
.IfNone("[unknown video]"),
Song s => s.SongMetadata.HeadOrNone().Map(sm => sm.Artist ?? string.Empty)
.IfNone("[unknown artist]"),
_ => "[unknown]"
};
}
private static string GetSubtitle(PlayoutItem playoutItem)
{
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
{
return string.Empty;
}
return playoutItem.MediaItem switch
{
Episode e => e.EpisodeMetadata.HeadOrNone().Match(
em => em.Title ?? string.Empty,
() => string.Empty),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Match(
mvm => mvm.Title ?? string.Empty,
() => string.Empty),
Song s => s.SongMetadata.HeadOrNone().Match(
mvm => mvm.Title ?? string.Empty,
() => string.Empty),
_ => string.Empty
};
}
private static string GetDescription(PlayoutItem playoutItem)
{
if (!string.IsNullOrWhiteSpace(playoutItem.CustomTitle))
{
return string.Empty;
}
return playoutItem.MediaItem switch
{
Movie m => m.MovieMetadata.HeadOrNone().Map(mm => mm.Plot ?? string.Empty).IfNone(string.Empty),
Episode e => e.EpisodeMetadata.HeadOrNone().Map(em => em.Plot ?? string.Empty)
.IfNone(string.Empty),
MusicVideo mv => mv.MusicVideoMetadata.HeadOrNone().Map(mvm => mvm.Plot ?? string.Empty)
.IfNone(string.Empty),
OtherVideo ov => ov.OtherVideoMetadata.HeadOrNone().Map(ovm => ovm.Plot ?? string.Empty)
.IfNone(string.Empty),
_ => string.Empty
};
}
private Option<ContentRating> GetContentRating(PlayoutItem playoutItem)
{
try
{
return playoutItem.MediaItem switch
{
Movie m => m.MovieMetadata
.HeadOrNone()
.Match(mm => ParseContentRating(mm.ContentRating, "MPAA"), () => None),
Episode e => e.Season.Show.ShowMetadata
.HeadOrNone()
.Match(sm => ParseContentRating(sm.ContentRating, "VCHIP"), () => None),
_ => None
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get content rating for playout item {Item}", GetTitle(playoutItem));
return None;
}
}
private static Option<ContentRating> ParseContentRating(string contentRating, string system)
{
Option<string> maybeFirst = (contentRating ?? string.Empty).Split('/').HeadOrNone();
return maybeFirst.Map(
first =>
{
string[] split = first.Split(':');
if (split.Length == 2)
{
return split[0].ToLowerInvariant() == "us"
? new ContentRating(system, split[1].ToUpperInvariant())
: new ContentRating(None, split[1].ToUpperInvariant());
}
return string.IsNullOrWhiteSpace(first)
? Option<ContentRating>.None
: new ContentRating(None, first);
}).Flatten();
}
private static string GetPrioritizedArtworkPath(Metadata metadata)
{
Option<string> maybeArtwork = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Poster)
.HeadOrNone()
.Map(a => GetArtworkUrl(a, ArtworkKind.Poster));
if (maybeArtwork.IsNone)
{
maybeArtwork = Optional(metadata.Artwork).Flatten()
.Filter(a => a.ArtworkKind == ArtworkKind.Thumbnail)
.HeadOrNone()
.Map(a => GetArtworkUrl(a, ArtworkKind.Thumbnail));
}
return maybeArtwork.IfNone(string.Empty);
}
private sealed record ContentRating(Option<string> System, string Value);
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record RefreshChannelList : IRequest, IBackgroundServiceRequest;
@@ -0,0 +1,116 @@
using System.Data.Common;
using System.Xml;
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.IO;
namespace ErsatzTV.Application.Channels;
public class RefreshChannelListHandler : IRequestHandler<RefreshChannelList>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public RefreshChannelListHandler(
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem)
{
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem;
}
public async Task Handle(RefreshChannelList request, CancellationToken cancellationToken)
{
_localFileSystem.EnsureFolderExists(FileSystemLayout.ChannelGuideCacheFolder);
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
using MemoryStream 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");
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))
{
await xml.WriteStartElementAsync(null, "category", null);
await xml.WriteAttributeStringAsync(null, "lang", null, "en");
await xml.WriteStringAsync(category);
await xml.WriteEndElementAsync(); // category
}
await xml.WriteStartElementAsync(null, "icon", null);
await xml.WriteAttributeStringAsync(null, "src", null, GetIconUrl(channel));
await xml.WriteEndElementAsync(); // icon
await xml.WriteEndElementAsync(); // channel
}
await xml.FlushAsync();
string tempFile = Path.GetTempFileName();
await File.WriteAllBytesAsync(tempFile, ms.ToArray(), cancellationToken);
string targetFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
File.Move(tempFile, targetFile, true);
}
private static async IAsyncEnumerable<ChannelResult> GetChannels(TvContext dbContext)
{
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)
order by CAST(C.Number as double)";
// TODO: this needs to be fixed for sqlite/mariadb
await using var reader = (DbDataReader)await dbContext.Connection.ExecuteReaderAsync(QUERY);
Func<DbDataReader, ChannelResult> rowParser = reader.GetRowParser<ChannelResult>();
while (await reader.ReadAsync())
{
yield return rowParser(reader);
}
while (await reader.NextResultAsync())
{
}
}
private static List<string> GetCategories(string categories) =>
(categories ?? string.Empty).Split(',')
.Map(s => s.Trim())
.Filter(s => !string.IsNullOrWhiteSpace(s))
.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);
}
@@ -1,19 +1,23 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Commands
{
public record UpdateChannel
(
int ChannelId,
string Name,
string Number,
int FFmpegProfileId,
string Logo,
string PreferredLanguageCode,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId) : IRequest<Either<BaseError, ChannelViewModel>>;
}
namespace ErsatzTV.Application.Channels;
public record UpdateChannel
(
int ChannelId,
string Name,
string Number,
string Group,
string Categories,
int FFmpegProfileId,
string Logo,
string PreferredAudioLanguageCode,
string PreferredAudioTitle,
StreamingMode StreamingMode,
int? WatermarkId,
int? FallbackFillerId,
string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>;
@@ -1,122 +1,141 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Application.Subtitles;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Channels.Commands
namespace ErsatzTV.Application.Channels;
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
{
public class UpdateChannelHandler : IRequestHandler<UpdateChannel, Either<BaseError, ChannelViewModel>>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdateChannelHandler(
ChannelWriter<IBackgroundServiceRequest> workerChannel,
IDbContextFactory<TvContext> dbContextFactory)
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public UpdateChannelHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Channel> validation = await Validate(dbContext, request);
return await validation.Apply(c => ApplyUpdateRequest(dbContext, c, request));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.FFmpegProfileId = update.FFmpegProfileId;
c.PreferredLanguageCode = update.PreferredLanguageCode;
c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
return ProjectToViewModel(c);
}
private async Task<Validation<BaseError, Channel>> Validate(TvContext dbContext, UpdateChannel request) =>
(await ChannelMustExist(dbContext, request), ValidateName(request),
await ValidateNumber(dbContext, request),
ValidatePreferredLanguage(request))
.Apply((channelToUpdate, _, _, _) => channelToUpdate);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
UpdateChannel updateChannel) =>
dbContext.Channels
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
int matchId = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
.Match(c => c.Id, () => updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; one decimal is allowed for subchannels");
}
return BaseError.New("Channel number must be unique");
}
private static Validation<BaseError, string> ValidatePreferredLanguage(UpdateChannel updateChannel) =>
Optional(updateChannel.PreferredLanguageCode ?? string.Empty)
.Filter(
lc => string.IsNullOrWhiteSpace(lc) || CultureInfo.GetCultures(CultureTypes.NeutralCultures).Any(
ci => string.Equals(ci.ThreeLetterISOLanguageName, lc, StringComparison.OrdinalIgnoreCase)))
.ToValidation<BaseError>("Preferred language code is invalid");
_workerChannel = workerChannel;
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, ChannelViewModel>> Handle(
UpdateChannel request,
CancellationToken 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));
}
private async Task<ChannelViewModel> ApplyUpdateRequest(TvContext dbContext, Channel c, UpdateChannel update)
{
c.Name = update.Name;
c.Number = update.Number;
c.Group = update.Group;
c.Categories = update.Categories;
c.FFmpegProfileId = update.FFmpegProfileId;
c.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>();
if (!string.IsNullOrWhiteSpace(update.Logo))
{
Option<Artwork> maybeLogo =
Optional(c.Artwork).Flatten().FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo);
maybeLogo.Match(
artwork =>
{
artwork.Path = update.Logo;
artwork.DateUpdated = DateTime.UtcNow;
},
() =>
{
var artwork = new Artwork
{
Path = update.Logo,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow,
ArtworkKind = ArtworkKind.Logo
};
c.Artwork.Add(artwork);
});
}
c.StreamingMode = update.StreamingMode;
c.WatermarkId = update.WatermarkId;
c.FallbackFillerId = update.FallbackFillerId;
await dbContext.SaveChangesAsync();
if (c.SubtitleMode != ChannelSubtitleMode.None)
{
Option<Playout> maybePlayout = await dbContext.Playouts
.SelectOneAsync(p => p.ChannelId, p => p.ChannelId == c.Id);
foreach (Playout playout in maybePlayout)
{
await _workerChannel.WriteAsync(new ExtractEmbeddedSubtitles(playout.Id));
}
}
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);
private static Task<Validation<BaseError, Channel>> ChannelMustExist(
TvContext dbContext,
UpdateChannel updateChannel) =>
dbContext.Channels
.Include(c => c.Artwork)
.Include(c => c.Watermark)
.SelectOneAsync(c => c.Id, c => c.Id == updateChannel.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist."));
private static Validation<BaseError, string> ValidateName(UpdateChannel updateChannel) =>
updateChannel.NotEmpty(c => c.Name)
.Bind(_ => updateChannel.NotLongerThan(50)(c => c.Name));
private static async Task<Validation<BaseError, string>> ValidateNumber(
TvContext dbContext,
UpdateChannel updateChannel)
{
int matchId = await dbContext.Channels
.SelectOneAsync(c => c.Number, c => c.Number == updateChannel.Number)
.Match(c => c.Id, () => updateChannel.ChannelId);
if (matchId == updateChannel.ChannelId)
{
if (Regex.IsMatch(updateChannel.Number, Channel.NumberValidator))
{
return updateChannel.Number;
}
return BaseError.New("Invalid channel number; two decimals are allowed for subchannels");
}
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");
}
+44 -23
View File
@@ -1,29 +1,50 @@
using System.Linq;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels
namespace ErsatzTV.Application.Channels;
internal static class Mapper
{
internal static class Mapper
{
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
new(
channel.Id,
channel.Number,
channel.Name,
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredLanguageCode,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId);
internal static ChannelViewModel ProjectToViewModel(Channel channel) =>
new(
channel.Id,
channel.Number,
channel.Name,
channel.Group,
channel.Categories,
channel.FFmpegProfileId,
GetLogo(channel),
channel.PreferredAudioLanguageCode,
channel.PreferredAudioTitle,
channel.StreamingMode,
channel.WatermarkId,
channel.FallbackFillerId,
channel.Playouts?.Count ?? 0,
channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode,
channel.MusicVideoCreditsMode,
channel.MusicVideoCreditsTemplate);
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
new(
channel.Id,
channel.Number,
channel.Name,
channel.FFmpegProfile.Name,
channel.PreferredAudioLanguageCode,
GetStreamingMode(channel));
private static string GetWatermark(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Watermark))
.Match(a => a.Path, string.Empty);
}
private static string GetLogo(Channel channel) =>
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
.Match(a => a.Path, string.Empty);
private static string GetStreamingMode(Channel channel) =>
channel.StreamingMode switch
{
StreamingMode.TransportStream => "MPEG-TS (Legacy)",
StreamingMode.TransportStreamHybrid => "MPEG-TS",
StreamingMode.HttpLiveStreamingDirect => "HLS Direct",
StreamingMode.HttpLiveStreamingSegmenter => "HLS Segmenter",
_ => throw new ArgumentOutOfRangeException(nameof(channel))
};
}
@@ -1,7 +1,3 @@
using System.Collections.Generic;
using MediatR;
namespace ErsatzTV.Application.Channels;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
}
public record GetAllChannels : IRequest<List<ChannelViewModel>>;
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.Channels;
namespace ErsatzTV.Application.Channels;
public record GetAllChannelsForApi : IRequest<List<ChannelResponseModel>>;
@@ -0,0 +1,21 @@
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsForApiHandler : IRequestHandler<GetAllChannelsForApi, List<ChannelResponseModel>>
{
private readonly IChannelRepository _channelRepository;
public GetAllChannelsForApiHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelResponseModel>> Handle(
GetAllChannelsForApi request,
CancellationToken cancellationToken)
{
IEnumerable<Channel> channels = Optional(await _channelRepository.GetAll()).Flatten();
return channels.Map(ProjectToResponseModel).ToList();
}
}
@@ -1,21 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{
public class GetAllChannelsHandler : IRequestHandler<GetAllChannels, List<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetAllChannelsHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
}
public async Task<List<ChannelViewModel>> Handle(GetAllChannels request, CancellationToken cancellationToken) =>
Optional(await _channelRepository.GetAll()).Flatten().Map(ProjectToViewModel).ToList();
}
@@ -1,7 +1,3 @@
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
}
public record GetChannelById(int Id) : IRequest<Option<ChannelViewModel>>;
@@ -1,20 +1,15 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{
public class GetChannelByIdHandler : IRequestHandler<GetChannelById, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelByIdHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.Get(request.Id)
.MapT(ProjectToViewModel);
}
public Task<Option<ChannelViewModel>> Handle(GetChannelById request, CancellationToken cancellationToken) =>
_channelRepository.GetChannel(request.Id)
.MapT(ProjectToViewModel);
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelByNumber(string ChannelNumber) : IRequest<Option<ChannelViewModel>>;
@@ -0,0 +1,14 @@
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Channels.Mapper;
namespace ErsatzTV.Application.Channels;
public class GetChannelByNumberHandler : IRequestHandler<GetChannelByNumber, Option<ChannelViewModel>>
{
private readonly IChannelRepository _channelRepository;
public GetChannelByNumberHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<Option<ChannelViewModel>> Handle(GetChannelByNumber request, CancellationToken cancellationToken) =>
_channelRepository.GetByNumber(request.ChannelNumber).MapT(ProjectToViewModel);
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelFramerate(string ChannelNumber) : IRequest<Option<int>>;
@@ -0,0 +1,123 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Channels;
public class GetChannelFramerateHandler : IRequestHandler<GetChannelFramerate, Option<int>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILogger<GetChannelFramerateHandler> _logger;
public GetChannelFramerateHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<GetChannelFramerateHandler> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task<Option<int>> Handle(GetChannelFramerate request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
FFmpegProfile ffmpegProfile = await dbContext.Channels
.Filter(c => c.Number == request.ChannelNumber)
.Include(c => c.FFmpegProfile)
.Map(c => c.FFmpegProfile)
.SingleAsync(cancellationToken);
if (!ffmpegProfile.NormalizeFramerate)
{
return Option<int>.None;
}
// TODO: expand to check everything in collection rather than what's scheduled?
_logger.LogDebug("Checking frame rates for channel {ChannelNumber}", request.ChannelNumber);
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Movie).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Episode).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as Song).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as MusicVideo).MediaVersions)
.Include(p => p.Items)
.ThenInclude(pi => pi.MediaItem)
.ThenInclude(mi => (mi as OtherVideo).MediaVersions)
.Filter(p => p.Channel.Number == request.ChannelNumber)
.ToListAsync(cancellationToken);
var frameRates = playouts.Map(p => p.Items.Map(i => i.MediaItem.GetHeadVersion()))
.Flatten()
.Map(mv => mv.RFrameRate)
.ToList();
var distinct = frameRates.Distinct().ToList();
if (distinct.Count > 1)
{
// TODO: something more intelligent than minimum framerate?
int result = frameRates.Map(ParseFrameRate).Min();
if (result < 24)
{
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate} instead of min value {MinFrameRate}",
request.ChannelNumber,
distinct,
24,
result);
return 24;
}
_logger.LogInformation(
"Normalizing frame rate for channel {ChannelNumber} from {Distinct} to {FrameRate}",
request.ChannelNumber,
distinct,
result);
return result;
}
if (distinct.Any())
{
_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);
}
return None;
}
private int ParseFrameRate(string frameRate)
{
if (!int.TryParse(frameRate, out int fr))
{
string[] split = (frameRate ?? string.Empty).Split("/");
if (int.TryParse(split[0], out int left) && int.TryParse(split[1], out int right))
{
fr = (int)Math.Round(left / (double)right);
}
else
{
fr = 24;
}
}
return fr;
}
}
@@ -1,7 +1,7 @@
using ErsatzTV.Core.Iptv;
using MediatR;
using ErsatzTV.Core;
using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelGuide(string Scheme, string Host) : IRequest<ChannelGuide>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelGuide
(string Scheme, string Host, string BaseUrl, string AccessToken) : IRequest<Either<BaseError, ChannelGuide>>;
@@ -1,20 +1,72 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using System.Text;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.IO;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, Either<BaseError, ChannelGuide>>
{
public class GetChannelGuideHandler : IRequestHandler<GetChannelGuide, ChannelGuide>
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
public GetChannelGuideHandler(
IDbContextFactory<TvContext> dbContextFactory,
RecyclableMemoryStreamManager recyclableMemoryStreamManager,
ILocalFileSystem localFileSystem)
{
private readonly IChannelRepository _channelRepository;
_dbContextFactory = dbContextFactory;
_recyclableMemoryStreamManager = recyclableMemoryStreamManager;
_localFileSystem = localFileSystem;
}
public GetChannelGuideHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public async Task<Either<BaseError, ChannelGuide>> Handle(
GetChannelGuide request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
public Task<ChannelGuide> Handle(GetChannelGuide request, CancellationToken cancellationToken) =>
_channelRepository.GetAllForGuide()
.Map(channels => new ChannelGuide(request.Scheme, request.Host, channels));
string channelsFile = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, "channels.xml");
if (!_localFileSystem.FileExists(channelsFile))
{
return BaseError.New($"Required file {channelsFile} is missing");
}
string accessTokenUri = string.Empty;
if (!string.IsNullOrWhiteSpace(request.AccessToken))
{
accessTokenUri = $"?access_token={request.AccessToken}";
}
string channelsFragment = await File.ReadAllTextAsync(channelsFile, Encoding.UTF8, cancellationToken);
// TODO: is regex faster?
channelsFragment = channelsFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
.Replace("{AccessTokenUri}", accessTokenUri);
var channelDataFragments = new Dictionary<string, string>();
foreach (string fileName in _localFileSystem.ListFiles(FileSystemLayout.ChannelGuideCacheFolder))
{
if (fileName.Contains("channels"))
{
continue;
}
string channelDataFragment = await File.ReadAllTextAsync(fileName, Encoding.UTF8, cancellationToken);
channelDataFragment = channelDataFragment
.Replace("{RequestBase}", $"{request.Scheme}://{request.Host}{request.BaseUrl}")
.Replace("{AccessTokenUri}", accessTokenUri);
channelDataFragments.Add(Path.GetFileNameWithoutExtension(fileName), channelDataFragment);
}
return new ChannelGuide(_recyclableMemoryStreamManager, channelsFragment, channelDataFragments);
}
}
@@ -1,8 +1,5 @@
using System.Collections.Generic;
using ErsatzTV.Core.Hdhr;
using MediatR;
using ErsatzTV.Core.Hdhr;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelLineup(string Scheme, string Host) : IRequest<List<LineupItem>>;
@@ -1,22 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Hdhr;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
{
public class GetChannelLineupHandler : IRequestHandler<GetChannelLineup, List<LineupItem>>
{
private readonly IChannelRepository _channelRepository;
private readonly IChannelRepository _channelRepository;
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public GetChannelLineupHandler(IChannelRepository channelRepository) => _channelRepository = channelRepository;
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}
public Task<List<LineupItem>> Handle(GetChannelLineup request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => channels.Map(c => new LineupItem(request.Scheme, request.Host, c)).ToList());
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelNameByPlayoutId(int PlayoutId) : IRequest<Option<string>>;
@@ -0,0 +1,22 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels;
public class GetChannelNameByPlayoutIdHandler : IRequestHandler<GetChannelNameByPlayoutId, Option<string>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public GetChannelNameByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory) =>
_dbContextFactory = dbContextFactory;
public async Task<Option<string>> Handle(GetChannelNameByPlayoutId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT(p => p.Channel.Name);
}
}
@@ -1,7 +1,6 @@
using ErsatzTV.Core.Iptv;
using MediatR;
using ErsatzTV.Core.Iptv;
namespace ErsatzTV.Application.Channels.Queries
{
public record GetChannelPlaylist(string Scheme, string Host, string Mode) : IRequest<ChannelPlaylist>;
}
namespace ErsatzTV.Application.Channels;
public record GetChannelPlaylist
(string Scheme, string Host, string BaseUrl, string Mode, string UserAgent, string AccessToken) : IRequest<ChannelPlaylist>;
@@ -1,52 +1,57 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Iptv;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Channels.Queries
namespace ErsatzTV.Application.Channels;
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
{
public class GetChannelPlaylistHandler : IRequestHandler<GetChannelPlaylist, ChannelPlaylist>
private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(
channels => new ChannelPlaylist(
request.Scheme,
request.Host,
request.BaseUrl,
channels,
request.UserAgent,
request.AccessToken));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
{
private readonly IChannelRepository _channelRepository;
public GetChannelPlaylistHandler(IChannelRepository channelRepository) =>
_channelRepository = channelRepository;
public Task<ChannelPlaylist> Handle(GetChannelPlaylist request, CancellationToken cancellationToken) =>
_channelRepository.GetAll()
.Map(channels => EnsureMode(channels, request.Mode))
.Map(channels => new ChannelPlaylist(request.Scheme, request.Host, channels));
private static List<Channel> EnsureMode(IEnumerable<Channel> channels, string mode)
var result = new List<Channel>();
foreach (Channel channel in channels)
{
var result = new List<Channel>();
foreach (Channel channel in channels)
switch (mode.ToLowerInvariant())
{
switch (mode.ToLowerInvariant())
{
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
case "segmenter":
channel.StreamingMode = StreamingMode.HttpLiveStreamingSegmenter;
result.Add(channel);
break;
case "hls-direct":
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
result.Add(channel);
break;
case "ts-legacy":
channel.StreamingMode = StreamingMode.TransportStream;
result.Add(channel);
break;
case "ts":
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
result.Add(channel);
break;
default:
result.Add(channel);
break;
}
return result;
}
return result;
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
}
namespace ErsatzTV.Application.Configuration;
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : IRequest;
@@ -1,21 +1,14 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration.Commands
namespace ErsatzTV.Application.Configuration;
public class SaveConfigElementByKeyHandler : IRequestHandler<SaveConfigElementByKey>
{
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
{
await _configElementRepository.Upsert(request.Key, request.Value);
return Unit.Default;
}
}
public async Task Handle(SaveConfigElementByKey request, CancellationToken cancellationToken) =>
await _configElementRepository.Upsert(request.Key, request.Value);
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Configuration;
public record UpdateGeneralSettings(GeneralSettingsViewModel GeneralSettings) : IRequest<Either<BaseError, Unit>>;
@@ -0,0 +1,32 @@
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;
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Configuration;
public record UpdateLibraryRefreshInterval(int LibraryRefreshInterval) : IRequest<Either<BaseError, Unit>>;
@@ -1,33 +1,31 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
namespace ErsatzTV.Application.Configuration;
public class UpdateLibraryRefreshIntervalHandler :
IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
public class UpdateLibraryRefreshIntervalHandler :
MediatR.IRequestHandler<UpdateLibraryRefreshInterval, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public UpdateLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(_ => _configElementRepository.Upsert(ConfigElementKey.LibraryRefreshInterval, request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
public Task<Either<BaseError, Unit>> Handle(
UpdateLibraryRefreshInterval request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(
_ => _configElementRepository.Upsert(
ConfigElementKey.LibraryRefreshInterval,
request.LibraryRefreshInterval))
.Bind(v => v.ToEitherAsync());
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Where(lri => lri > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Tuner count must be greater than zero")
.AsTask();
}
private static Task<Validation<BaseError, Unit>> Validate(UpdateLibraryRefreshInterval request) =>
Optional(request.LibraryRefreshInterval)
.Where(lri => lri is >= 0 and < 1_000_000)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Library refresh interval must be zero or greated")
.AsTask();
}
@@ -1,7 +0,0 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Configuration.Commands
{
public record UpdatePlayoutDaysToBuild(int DaysToBuild) : MediatR.IRequest<Either<BaseError, Unit>>;
}
@@ -1,66 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application.Playouts.Commands;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Data;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Configuration.Commands
{
public class
UpdatePlayoutDaysToBuildHandler : MediatR.IRequestHandler<UpdatePlayoutDaysToBuild, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutDaysToBuildHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutDaysToBuild request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.DaysToBuild));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, int daysToBuild)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, daysToBuild);
// build all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number)).Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutDaysToBuild request) =>
Optional(request.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Configuration;
public record UpdatePlayoutSettings(PlayoutSettingsViewModel PlayoutSettings) : IRequest<Either<BaseError, Unit>>;
@@ -0,0 +1,64 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Configuration;
public class UpdatePlayoutSettingsHandler : IRequestHandler<UpdatePlayoutSettings, Either<BaseError, Unit>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdatePlayoutSettingsHandler(
IConfigElementRepository configElementRepository,
IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
_configElementRepository = configElementRepository;
_dbContextFactory = dbContextFactory;
_workerChannel = workerChannel;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdatePlayoutSettings request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Unit> validation = await Validate(request);
return await validation.Apply<Unit, Unit>(_ => ApplyUpdate(dbContext, request.PlayoutSettings));
}
private async Task<Unit> ApplyUpdate(TvContext dbContext, PlayoutSettingsViewModel playoutSettings)
{
await _configElementRepository.Upsert(ConfigElementKey.PlayoutDaysToBuild, playoutSettings.DaysToBuild);
await _configElementRepository.Upsert(
ConfigElementKey.PlayoutSkipMissingItems,
playoutSettings.SkipMissingItems);
// continue all playouts to proper number of days
List<Playout> playouts = await dbContext.Playouts
.Include(p => p.Channel)
.ToListAsync();
foreach (int playoutId in playouts.OrderBy(p => decimal.Parse(p.Channel.Number, CultureInfo.InvariantCulture))
.Map(p => p.Id))
{
await _workerChannel.WriteAsync(new BuildPlayout(playoutId, PlayoutBuildMode.Continue));
}
return Unit.Default;
}
private static Task<Validation<BaseError, Unit>> Validate(UpdatePlayoutSettings request) =>
Optional(request.PlayoutSettings.DaysToBuild)
.Where(days => days > 0)
.Map(_ => Unit.Default)
.ToValidation<BaseError>("Days to build must be greater than zero")
.AsTask();
}
@@ -1,4 +1,3 @@
namespace ErsatzTV.Application.Configuration
{
public record ConfigElementViewModel(string Key, string Value);
}
namespace ErsatzTV.Application.Configuration;
public record ConfigElementViewModel(string Key, string Value);
@@ -0,0 +1,8 @@
using Serilog.Events;
namespace ErsatzTV.Application.Configuration;
public class GeneralSettingsViewModel
{
public LogEventLevel MinimumLogLevel { get; set; }
}
+5 -6
View File
@@ -1,10 +1,9 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Configuration
namespace ErsatzTV.Application.Configuration;
internal static class Mapper
{
internal static class Mapper
{
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
new(element.Key, element.Value);
}
@@ -0,0 +1,7 @@
namespace ErsatzTV.Application.Configuration;
public class PlayoutSettingsViewModel
{
public int DaysToBuild { get; set; }
public bool SkipMissingItems { get; set; }
}
@@ -1,8 +1,5 @@
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
}
namespace ErsatzTV.Application.Configuration;
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
@@ -1,22 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using ErsatzTV.Core.Interfaces.Repositories;
using static ErsatzTV.Application.Configuration.Mapper;
namespace ErsatzTV.Application.Configuration.Queries
namespace ErsatzTV.Application.Configuration;
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
}
public Task<Option<ConfigElementViewModel>> Handle(
GetConfigElementByKey request,
CancellationToken cancellationToken) =>
_configElementRepository.GetConfigElement(request.Key).MapT(ProjectToViewModel);
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record GetGeneralSettings : IRequest<GeneralSettingsViewModel>;
@@ -0,0 +1,24 @@
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)
};
}
}
@@ -1,6 +1,3 @@
using MediatR;
namespace ErsatzTV.Application.Configuration;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetLibraryRefreshInterval : IRequest<int>;
}
public record GetLibraryRefreshInterval : IRequest<int>;
@@ -1,21 +1,16 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
namespace ErsatzTV.Application.Configuration;
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
public class GetLibraryRefreshIntervalHandler : IRequestHandler<GetLibraryRefreshInterval, int>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IConfigElementRepository _configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public GetLibraryRefreshIntervalHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
public Task<int> Handle(GetLibraryRefreshInterval request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.Map(result => result.IfNone(6));
}
@@ -1,6 +0,0 @@
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public record GetPlayoutDaysToBuild : IRequest<int>;
}
@@ -1,21 +0,0 @@
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Configuration.Queries
{
public class GetPlayoutDaysToBuildHandler : IRequestHandler<GetPlayoutDaysToBuild, int>
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutDaysToBuildHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public Task<int> Handle(GetPlayoutDaysToBuild request, CancellationToken cancellationToken) =>
_configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.Map(result => result.IfNone(2));
}
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Configuration;
public record GetPlayoutSettings : IRequest<PlayoutSettingsViewModel>;
@@ -0,0 +1,26 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Application.Configuration;
public class GetPlayoutSettingsHandler : IRequestHandler<GetPlayoutSettings, PlayoutSettingsViewModel>
{
private readonly IConfigElementRepository _configElementRepository;
public GetPlayoutSettingsHandler(IConfigElementRepository configElementRepository) =>
_configElementRepository = configElementRepository;
public async Task<PlayoutSettingsViewModel> Handle(GetPlayoutSettings request, CancellationToken cancellationToken)
{
Option<int> daysToBuild = await _configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild);
Option<bool> skipMissingItems =
await _configElementRepository.GetValue<bool>(ConfigElementKey.PlayoutSkipMissingItems);
return new PlayoutSettingsViewModel
{
DaysToBuild = await daysToBuild.IfNoneAsync(2),
SkipMissingItems = await skipMissingItems.IfNoneAsync(false)
};
}
}
@@ -0,0 +1,83 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Emby;
public class CallEmbyCollectionScannerHandler : CallLibraryScannerHandler<SynchronizeEmbyCollections>,
IRequestHandler<SynchronizeEmbyCollections, Either<BaseError, Unit>>
{
public CallEmbyCollectionScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo) : base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
{
}
public async Task<Either<BaseError, Unit>>
Handle(SynchronizeEmbyCollections request, CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, Unit>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, Unit>>(error.Join());
});
}
protected override async Task<DateTimeOffset> GetLastScan(TvContext dbContext, SynchronizeEmbyCollections request)
{
DateTime minDateTime = await dbContext.EmbyMediaSources
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyMediaSourceId)
.Match(l => l.LastCollectionsScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
}
protected override bool ScanIsRequired(
DateTimeOffset lastScan,
int libraryRefreshInterval,
SynchronizeEmbyCollections request)
{
if (lastScan == SystemTime.MaxValueUtc)
{
return false;
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
private async Task<Either<BaseError, Unit>> PerformScan(
string scanner,
SynchronizeEmbyCollections request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
{
"scan-emby-collections", request.EmbyMediaSourceId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
{
arguments.Add("--force");
}
return await base.PerformScan(scanner, arguments, cancellationToken).MapT(_ => Unit.Default);
}
}
@@ -0,0 +1,102 @@
using System.Globalization;
using System.Threading.Channels;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Emby;
public class CallEmbyLibraryScannerHandler : CallLibraryScannerHandler<ISynchronizeEmbyLibraryById>,
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{
public CallEmbyLibraryScannerHandler(
IDbContextFactory<TvContext> dbContextFactory,
IConfigElementRepository configElementRepository,
ChannelWriter<ISearchIndexBackgroundServiceRequest> channel,
IMediator mediator,
IRuntimeInfo runtimeInfo)
: base(dbContextFactory, configElementRepository, channel, mediator, runtimeInfo)
{
}
Task<Either<BaseError, string>> IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>.Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
Task<Either<BaseError, string>> IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>.
Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request, cancellationToken);
private async Task<Either<BaseError, string>> Handle(
ISynchronizeEmbyLibraryById request,
CancellationToken cancellationToken)
{
Validation<BaseError, string> validation = await Validate(request);
return await validation.Match(
scanner => PerformScan(scanner, request, cancellationToken),
error =>
{
foreach (ScanIsNotRequired scanIsNotRequired in error.OfType<ScanIsNotRequired>())
{
return Task.FromResult<Either<BaseError, string>>(scanIsNotRequired);
}
return Task.FromResult<Either<BaseError, string>>(error.Join());
});
}
private async Task<Either<BaseError, string>> PerformScan(
string scanner,
ISynchronizeEmbyLibraryById request,
CancellationToken cancellationToken)
{
var arguments = new List<string>
{
"scan-emby", request.EmbyLibraryId.ToString(CultureInfo.InvariantCulture)
};
if (request.ForceScan)
{
arguments.Add("--force");
}
if (request.DeepScan)
{
arguments.Add("--deep");
}
return await base.PerformScan(scanner, arguments, cancellationToken);
}
protected override async Task<DateTimeOffset> GetLastScan(
TvContext dbContext,
ISynchronizeEmbyLibraryById request)
{
DateTime minDateTime = await dbContext.EmbyLibraries
.SelectOneAsync(l => l.Id, l => l.Id == request.EmbyLibraryId)
.Match(l => l.LastScan ?? SystemTime.MinValueUtc, () => SystemTime.MaxValueUtc);
return new DateTimeOffset(minDateTime, TimeSpan.Zero);
}
protected override bool ScanIsRequired(
DateTimeOffset lastScan,
int libraryRefreshInterval,
ISynchronizeEmbyLibraryById request)
{
if (lastScan == SystemTime.MaxValueUtc)
{
return false;
}
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(libraryRefreshInterval);
return request.ForceScan || libraryRefreshInterval > 0 && nextScan < DateTimeOffset.Now;
}
}
@@ -1,7 +1,5 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record DisconnectEmby : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Emby;
public record DisconnectEmby : IRequest<Either<BaseError, Unit>>;
@@ -1,46 +1,41 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class DisconnectEmbyHandler : IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
{
public class DisconnectEmbyHandler : MediatR.IRequestHandler<DisconnectEmby, Either<BaseError, Unit>>
private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{
private readonly IEmbySecretStore _embySecretStore;
private readonly IEntityLocker _entityLocker;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_entityLocker = entityLocker;
_searchIndex = searchIndex;
}
public DisconnectEmbyHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEntityLocker entityLocker,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_entityLocker = entityLocker;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
public async Task<Either<BaseError, Unit>> Handle(
DisconnectEmby request,
CancellationToken cancellationToken)
{
List<int> ids = await _mediaSourceRepository.DeleteAllEmby();
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
await _embySecretStore.DeleteAll();
_entityLocker.UnlockRemoteMediaSource<EmbyMediaSource>();
return Unit.Default;
}
return Unit.Default;
}
}
@@ -1,8 +1,6 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SaveEmbySecrets(EmbySecrets Secrets) : MediatR.IRequest<Either<BaseError, Unit>>;
}
namespace ErsatzTV.Application.Emby;
public record SaveEmbySecrets(EmbySecrets Secrets) : IRequest<Either<BaseError, Unit>>;
@@ -1,60 +1,56 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SaveEmbySecretsHandler : IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
{
public class SaveEmbySecretsHandler : MediatR.IRequestHandler<SaveEmbySecrets, Either<BaseError, Unit>>
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SaveEmbySecretsHandler(
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
{
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
}
public Task<Either<BaseError, Unit>> Handle(SaveEmbySecrets request, CancellationToken cancellationToken) =>
Validate(request)
.MapT(PerformSave)
.Bind(v => v.ToEitherAsync());
private async Task<Validation<BaseError, Parameters>> Validate(SaveEmbySecrets request)
{
Either<BaseError, EmbyServerInformation> maybeServerInformation = await _embyApiClient
.GetServerInformation(request.Secrets.Address, request.Secrets.ApiKey);
return maybeServerInformation.Match(
info => Validation<BaseError, Parameters>.Success(new Parameters(request.Secrets, info)),
error => error);
}
private async Task<Unit> PerformSave(Parameters parameters)
{
await _embySecretStore.SaveSecrets(parameters.Secrets);
await _mediaSourceRepository.UpsertEmby(
parameters.Secrets.Address,
parameters.ServerInformation.ServerName,
parameters.ServerInformation.OperatingSystem);
await _channel.WriteAsync(new SynchronizeEmbyMediaSources());
return Unit.Default;
}
private sealed record Parameters(EmbySecrets Secrets, EmbyServerInformation ServerInformation);
}
@@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyCollections(int EmbyMediaSourceId, bool ForceScan) : IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest;
@@ -1,8 +1,6 @@
using ErsatzTV.Core;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : MediatR.IRequest<Either<BaseError, Unit>>,
IEmbyBackgroundServiceRequest;
}
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyLibraries(int EmbyMediaSourceId) : IRequest<Either<BaseError, Unit>>,
IScannerBackgroundServiceRequest;
@@ -1,118 +1,109 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyLibrariesHandler : IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
{
public class
SynchronizeEmbyLibrariesHandler : MediatR.IRequestHandler<SynchronizeEmbyLibraries, Either<BaseError, Unit>>
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
{
private readonly IEmbyApiClient _embyApiClient;
private readonly IEmbySecretStore _embySecretStore;
private readonly ILogger<SynchronizeEmbyLibrariesHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_logger = logger;
_searchIndex = searchIndex;
}
public SynchronizeEmbyLibrariesHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyApiClient embyApiClient,
ILogger<SynchronizeEmbyLibrariesHandler> logger,
ISearchIndex searchIndex)
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
foreach (BaseError error in maybeLibraries.LeftToSeq())
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyApiClient = embyApiClient;
_logger = logger;
_searchIndex = searchIndex;
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
}
public Task<Either<BaseError, Unit>> Handle(
SynchronizeEmbyLibraries request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(SynchronizeLibraries)
.Bind(v => v.ToEitherAsync());
private Task<Validation<BaseError, ConnectionParameters>> Validate(SynchronizeEmbyLibraries request) =>
MediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> MediaSourceMustExist(
SynchronizeEmbyLibraries request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(o => o.ToValidation<BaseError>("Emby media source does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
foreach (List<EmbyLibrary> libraries in maybeLibraries.RightToSeq())
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
var toUpdate = libraries
.Filter(l => toAdd.All(a => a.ItemId != l.ItemId) && toRemove.All(r => r.ItemId != l.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove,
toUpdate);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
return Unit.Default;
}
private async Task<Unit> SynchronizeLibraries(ConnectionParameters connectionParameters)
{
Either<BaseError, List<EmbyLibrary>> maybeLibraries = await _embyApiClient.GetLibraries(
connectionParameters.ActiveConnection.Address,
connectionParameters.ApiKey);
await maybeLibraries.Match(
async libraries =>
{
var existing = connectionParameters.EmbyMediaSource.Libraries.OfType<EmbyLibrary>()
.ToList();
var toAdd = libraries.Filter(library => existing.All(l => l.ItemId != library.ItemId)).ToList();
var toRemove = existing.Filter(library => libraries.All(l => l.ItemId != library.ItemId)).ToList();
List<int> ids = await _mediaSourceRepository.UpdateLibraries(
connectionParameters.EmbyMediaSource.Id,
toAdd,
toRemove);
if (ids.Any())
{
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
}
},
error =>
{
_logger.LogWarning(
"Unable to synchronize libraries from emby server {EmbyServer}: {Error}",
connectionParameters.EmbyMediaSource.ServerName,
error.Value);
return Task.CompletedTask;
});
return Unit.Default;
}
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
private sealed record ConnectionParameters(EmbyMediaSource EmbyMediaSource, EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
@@ -1,23 +1,21 @@
using ErsatzTV.Core;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>, IScannerBackgroundServiceRequest
{
public interface ISynchronizeEmbyLibraryById : IRequest<Either<BaseError, string>>,
IEmbyBackgroundServiceRequest
{
int EmbyLibraryId { get; }
bool ForceScan { get; }
}
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
int EmbyLibraryId { get; }
bool ForceScan { get; }
bool DeepScan { get; }
}
public record SynchronizeEmbyLibraryByIdIfNeeded(int EmbyLibraryId) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => false;
public bool DeepScan => false;
}
public record ForceSynchronizeEmbyLibraryById(int EmbyLibraryId, bool DeepScan) : ISynchronizeEmbyLibraryById
{
public bool ForceScan => true;
}
@@ -1,181 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;
namespace ErsatzTV.Application.Emby.Commands
{
public class SynchronizeEmbyLibraryByIdHandler :
IRequestHandler<ForceSynchronizeEmbyLibraryById, Either<BaseError, string>>,
IRequestHandler<SynchronizeEmbyLibraryByIdIfNeeded, Either<BaseError, string>>
{
private readonly IConfigElementRepository _configElementRepository;
private readonly IEmbyMovieLibraryScanner _embyMovieLibraryScanner;
private readonly IEmbySecretStore _embySecretStore;
private readonly IEmbyTelevisionLibraryScanner _embyTelevisionLibraryScanner;
private readonly IEntityLocker _entityLocker;
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyLibraryByIdHandler(
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
IEmbyMovieLibraryScanner embyMovieLibraryScanner,
IEmbyTelevisionLibraryScanner embyTelevisionLibraryScanner,
ILibraryRepository libraryRepository,
IEntityLocker entityLocker,
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
_embyMovieLibraryScanner = embyMovieLibraryScanner;
_embyTelevisionLibraryScanner = embyTelevisionLibraryScanner;
_libraryRepository = libraryRepository;
_entityLocker = entityLocker;
_configElementRepository = configElementRepository;
_logger = logger;
}
public Task<Either<BaseError, string>> Handle(
ForceSynchronizeEmbyLibraryById request,
CancellationToken cancellationToken) => Handle(request);
public Task<Either<BaseError, string>> Handle(
SynchronizeEmbyLibraryByIdIfNeeded request,
CancellationToken cancellationToken) => Handle(request);
private Task<Either<BaseError, string>>
Handle(ISynchronizeEmbyLibraryById request) =>
Validate(request)
.MapT(parameters => Synchronize(parameters).Map(_ => parameters.Library.Name))
.Bind(v => v.ToEitherAsync());
private async Task<Unit> Synchronize(RequestParameters parameters)
{
var lastScan = new DateTimeOffset(parameters.Library.LastScan ?? SystemTime.MinValueUtc, TimeSpan.Zero);
DateTimeOffset nextScan = lastScan + TimeSpan.FromHours(parameters.LibraryRefreshInterval);
if (parameters.ForceScan || nextScan < DateTimeOffset.Now)
{
switch (parameters.Library.MediaKind)
{
case LibraryMediaKind.Movies:
await _embyMovieLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
case LibraryMediaKind.Shows:
await _embyTelevisionLibraryScanner.ScanLibrary(
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFprobePath);
break;
}
parameters.Library.LastScan = DateTime.UtcNow;
await _libraryRepository.UpdateLastScan(parameters.Library);
}
else
{
_logger.LogDebug(
"Skipping unforced scan of emby media library {Name}",
parameters.Library.Name);
}
_entityLocker.UnlockLibrary(parameters.Library.Id);
return Unit.Default;
}
private async Task<Validation<BaseError, RequestParameters>> Validate(
ISynchronizeEmbyLibraryById request) =>
(await ValidateConnection(request), await EmbyLibraryMustExist(request),
await ValidateLibraryRefreshInterval(), await ValidateFFprobePath())
.Apply(
(connectionParameters, embyLibrary, libraryRefreshInterval, ffprobePath) => new RequestParameters(
connectionParameters,
embyLibrary,
request.ForceScan,
libraryRefreshInterval,
ffprobePath
));
private Task<Validation<BaseError, ConnectionParameters>> ValidateConnection(
ISynchronizeEmbyLibraryById request) =>
EmbyMediaSourceMustExist(request)
.BindT(MediaSourceMustHaveActiveConnection)
.BindT(MediaSourceMustHaveApiKey);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyByLibraryId(request.EmbyLibraryId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source for library {request.EmbyLibraryId} does not exist."));
private Validation<BaseError, ConnectionParameters> MediaSourceMustHaveActiveConnection(
EmbyMediaSource embyMediaSource)
{
Option<EmbyConnection> maybeConnection = embyMediaSource.Connections.HeadOrNone();
return maybeConnection.Map(connection => new ConnectionParameters(embyMediaSource, connection))
.ToValidation<BaseError>("Emby media source requires an active connection");
}
private async Task<Validation<BaseError, ConnectionParameters>> MediaSourceMustHaveApiKey(
ConnectionParameters connectionParameters)
{
EmbySecrets secrets = await _embySecretStore.ReadSecrets();
return Optional(secrets.Address == connectionParameters.ActiveConnection.Address)
.Where(match => match)
.Map(_ => connectionParameters with { ApiKey = secrets.ApiKey })
.ToValidation<BaseError>("Emby media source requires an api key");
}
private Task<Validation<BaseError, EmbyLibrary>> EmbyLibraryMustExist(
ISynchronizeEmbyLibraryById request) =>
_mediaSourceRepository.GetEmbyLibrary(request.EmbyLibraryId)
.Map(v => v.ToValidation<BaseError>($"Emby library {request.EmbyLibraryId} does not exist."));
private Task<Validation<BaseError, int>> ValidateLibraryRefreshInterval() =>
_configElementRepository.GetValue<int>(ConfigElementKey.LibraryRefreshInterval)
.FilterT(lri => lri > 0)
.Map(lri => lri.ToValidation<BaseError>("Library refresh interval is invalid"));
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
.FilterT(File.Exists)
.Map(
ffprobePath =>
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
private record RequestParameters(
ConnectionParameters ConnectionParameters,
EmbyLibrary Library,
bool ForceScan,
int LibraryRefreshInterval,
string FFprobePath);
private record ConnectionParameters(
EmbyMediaSource EmbyMediaSource,
EmbyConnection ActiveConnection)
{
public string ApiKey { get; set; }
}
}
}
@@ -1,11 +1,7 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
{
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
}
namespace ErsatzTV.Application.Emby;
public record SynchronizeEmbyMediaSources : IRequest<Either<BaseError, List<EmbyMediaSource>>>,
IEmbyBackgroundServiceRequest;
@@ -1,41 +1,35 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
using MediatR;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
{
public class SynchronizeEmbyMediaSourcesHandler : IRequestHandler<SynchronizeEmbyMediaSources,
Either<BaseError, List<EmbyMediaSource>>>
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ChannelWriter<IScannerBackgroundServiceRequest> _scannerWorkerChannel;
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IScannerBackgroundServiceRequest> scannerWorkerChannel)
{
private readonly ChannelWriter<IEmbyBackgroundServiceRequest> _channel;
private readonly IMediaSourceRepository _mediaSourceRepository;
_mediaSourceRepository = mediaSourceRepository;
_scannerWorkerChannel = scannerWorkerChannel;
}
public SynchronizeEmbyMediaSourcesHandler(
IMediaSourceRepository mediaSourceRepository,
ChannelWriter<IEmbyBackgroundServiceRequest> channel)
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
_mediaSourceRepository = mediaSourceRepository;
_channel = channel;
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _scannerWorkerChannel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
public async Task<Either<BaseError, List<EmbyMediaSource>>> Handle(
SynchronizeEmbyMediaSources request,
CancellationToken cancellationToken)
{
List<EmbyMediaSource> mediaSources = await _mediaSourceRepository.GetAllEmby();
foreach (EmbyMediaSource mediaSource in mediaSources)
{
// await _channel.WriteAsync(new SynchronizeEmbyAdminUserId(mediaSource.Id), cancellationToken);
await _channel.WriteAsync(new SynchronizeEmbyLibraries(mediaSource.Id), cancellationToken);
}
return mediaSources;
}
return mediaSources;
}
}
@@ -1,11 +1,8 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : MediatR.IRequest<Either<BaseError, Unit>>;
namespace ErsatzTV.Application.Emby;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
}
public record UpdateEmbyLibraryPreferences
(List<EmbyLibraryPreference> Preferences) : IRequest<Either<BaseError, Unit>>;
public record EmbyLibraryPreference(int Id, bool ShouldSyncItems);
@@ -1,42 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Search;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class
UpdateEmbyLibraryPreferencesHandler : IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
{
public class
UpdateEmbyLibraryPreferencesHandler : MediatR.IRequestHandler<UpdateEmbyLibraryPreferences,
Either<BaseError, Unit>>
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{
private readonly IMediaSourceRepository _mediaSourceRepository;
private readonly ISearchIndex _searchIndex;
_mediaSourceRepository = mediaSourceRepository;
_searchIndex = searchIndex;
}
public UpdateEmbyLibraryPreferencesHandler(
IMediaSourceRepository mediaSourceRepository,
ISearchIndex searchIndex)
{
_mediaSourceRepository = mediaSourceRepository;
_searchIndex = searchIndex;
}
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
public async Task<Either<BaseError, Unit>> Handle(
UpdateEmbyLibraryPreferences request,
CancellationToken cancellationToken)
{
var toDisable = request.Preferences.Filter(p => p.ShouldSyncItems == false).Map(p => p.Id).ToList();
List<int> ids = await _mediaSourceRepository.DisableEmbyLibrarySync(toDisable);
await _searchIndex.RemoveItems(ids);
_searchIndex.Commit();
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
IEnumerable<int> toEnable = request.Preferences.Filter(p => p.ShouldSyncItems).Map(p => p.Id);
await _mediaSourceRepository.EnableEmbyLibrarySync(toEnable);
return Unit.Default;
}
return Unit.Default;
}
}
@@ -1,12 +1,9 @@
using System.Collections.Generic;
using ErsatzTV.Core;
using LanguageExt;
using ErsatzTV.Core;
namespace ErsatzTV.Application.Emby.Commands
{
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : MediatR.IRequest<Either<BaseError, Unit>>;
namespace ErsatzTV.Application.Emby;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
}
public record UpdateEmbyPathReplacements(
int EmbyMediaSourceId,
List<EmbyPathReplacementItem> PathReplacements) : IRequest<Either<BaseError, Unit>>;
public record EmbyPathReplacementItem(int Id, string EmbyPath, string LocalPath);
@@ -1,55 +1,49 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using LanguageExt;
namespace ErsatzTV.Application.Emby.Commands
namespace ErsatzTV.Application.Emby;
public class UpdateEmbyPathReplacementsHandler : IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
{
public class UpdateEmbyPathReplacementsHandler : MediatR.IRequestHandler<UpdateEmbyPathReplacements,
Either<BaseError, Unit>>
private readonly IMediaSourceRepository _mediaSourceRepository;
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
public Task<Either<BaseError, Unit>> Handle(
UpdateEmbyPathReplacements request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
private Task<Unit> MergePathReplacements(
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{
private readonly IMediaSourceRepository _mediaSourceRepository;
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
public UpdateEmbyPathReplacementsHandler(IMediaSourceRepository mediaSourceRepository) =>
_mediaSourceRepository = mediaSourceRepository;
var incoming = request.PathReplacements.Map(Project).ToList();
public Task<Either<BaseError, Unit>> Handle(
UpdateEmbyPathReplacements request,
CancellationToken cancellationToken) =>
Validate(request)
.MapT(pms => MergePathReplacements(request, pms))
.Bind(v => v.ToEitherAsync());
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
private Task<Unit> MergePathReplacements(
UpdateEmbyPathReplacements request,
EmbyMediaSource embyMediaSource)
{
embyMediaSource.PathReplacements ??= new List<EmbyPathReplacement>();
var incoming = request.PathReplacements.Map(Project).ToList();
var toAdd = incoming.Filter(r => r.Id < 1).ToList();
var toRemove = embyMediaSource.PathReplacements.Filter(r => incoming.All(pr => pr.Id != r.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
return _mediaSourceRepository.UpdatePathReplacements(embyMediaSource.Id, toAdd, toUpdate, toRemove);
}
private static EmbyPathReplacement Project(EmbyPathReplacementItem vm) =>
new() { Id = vm.Id, EmbyPath = vm.EmbyPath, LocalPath = vm.LocalPath };
private Task<Validation<BaseError, EmbyMediaSource>> Validate(UpdateEmbyPathReplacements request) =>
EmbyMediaSourceMustExist(request);
private Task<Validation<BaseError, EmbyMediaSource>> EmbyMediaSourceMustExist(
UpdateEmbyPathReplacements request) =>
_mediaSourceRepository.GetEmby(request.EmbyMediaSourceId)
.Map(
v => v.ToValidation<BaseError>(
$"Emby media source {request.EmbyMediaSourceId} does not exist."));
}

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