Compare commits

...

585 Commits

Author SHA1 Message Date
vexorian
826f21763d
Merge pull request #496 from vexorian/vexorian-patch-1
Pull Request Template
2025-12-16 10:09:18 -04:00
vexorian
fd998e4834
Pull Request Template
Pull Request Template
2025-12-16 10:08:33 -04:00
vexorian
564dceb303
AI Policy
AI Policy
2025-12-16 10:07:53 -04:00
vexorian
30cc9f539f
AI Policy
AI Policy
2025-12-16 10:07:15 -04:00
vexorian
3f7dcf6e77
Code of Conduct
Code of Conduct
2025-12-16 09:30:46 -04:00
vexorian
8be607a81b Updating the workflow, now with less branch renames. 2025-12-14 21:12:04 -04:00
vexorian
1ed31d22e9 The new development branch is dev/1.6.x 2025-12-01 11:28:31 -04:00
vexorian
1abdaa68f1 Update PR template 2025-12-01 11:24:23 -04:00
vexorian
0f4dd1c464 Add thanks page 2024-10-21 03:12:05 -04:00
vexorian
196033844a 1.5.5 2024-10-21 02:44:51 -04:00
vexorian
7e9db8c1f6 Merge commit 'f59cca4ea4f141b220e4586100ba541b2ba8fe96' into edge 2024-10-21 02:02:25 -04:00
vexorian
f59cca4ea4 1.5.4 2024-10-21 02:00:48 -04:00
vexorian
0ea92271cb More weight 2024-10-20 23:12:12 -04:00
vexorian
08c81f97a2 Merge remote-tracking branch 'origin/main' into edge 2023-11-28 23:24:26 -04:00
vexorian
869987bdb1 Merge remote-tracking branch 'origin/dev/1.5.x' 2023-11-28 23:23:45 -04:00
vexorian
e0fda77c56 1.5.3 2023-11-28 23:21:25 -04:00
vexorian
cdc13a0267 'Preludes' can be disabled in ffmpeg options 2023-11-28 23:20:08 -04:00
vexorian
56a4f3fdd8 Fix 'blocky' playback with mpeg2video. Add preset for libx264, might be unnecessary/wrong 2023-11-28 23:03:45 -04:00
vexorian
a72967e575 Bootstrap is now bundled as part of dizqueTV, so that the web UI can be used offline. 2023-11-27 23:21:15 -04:00
vexorian
a66ade1a8d FontAwesome is now bundled as part of dizqueTV, so that the web UI can be used offline. 2023-11-27 23:08:52 -04:00
vexorian
1468d112a9 1.5.3-development 2023-11-27 09:17:26 -04:00
vexorian
5f405d39e6 Remove gigantic logs that show all of the channel's contents some times 2023-11-27 09:16:52 -04:00
vexorian
bc7b70ef12 Merge branch 'dev/1.5.x' 2023-11-27 08:27:50 -04:00
vexorian
323caa2739 Merge remote-tracking branch 'origin/dev/1.5.x' into edge 2023-11-27 08:27:27 -04:00
vexorian
a01c8e4373 1.5.2 2023-11-27 08:26:31 -04:00
vexorian
c608670b5a #427 Fix nvidia build not using the correct Dockerfile in github actions. 2023-11-27 08:25:57 -04:00
vexorian
c360ddae05 Merge remote-tracking branch 'origin/dev/1.5.x' into edge 2023-11-26 22:18:12 -04:00
vexorian
393de7429c 1.5.1 2023-11-26 22:15:22 -04:00
vexorian
acc49fcd34 Refresh button for the library. 2023-11-26 22:14:33 -04:00
vexorian
c427322f37 Disable interlude in the same cases where loading screen is disabled. 2023-11-26 21:02:36 -04:00
vexorian
c2bb2c8df1 Make median logic be based on the specific filler list instead of the whole combined fillers 2023-11-23 11:46:15 -04:00
vexorian
189a2adf4e The registry of when a filler was last played is now persistent. 2023-11-22 00:30:57 -04:00
vexorian
a8cdc6f449 Fix median condition not working when the number of filler videos in a channel is odd. 2023-11-21 14:37:45 -04:00
vexorian
447c33027b Ability to choose the place in the list to insert new programs. 2023-11-17 21:01:26 -04:00
vexorian
de3a64c4c0 Tweak the library browser, the placement of the add button is now consistent. The background of the row gets highlighted blue so that you know that the plus button is for a specific library element. 2023-11-15 01:04:05 -04:00
vexorian
17094ea64d Sort fillter and custom shows lists. 2023-11-15 01:02:37 -04:00
vexorian
66804fe26c Improve interlude 2023-11-14 09:39:27 -04:00
vexorian
92cd5ecf8e Do not stop the queue after 100 videos played. The workaround is hopefully temporary because the transition between the batches of 100 is sort of glitchy 2023-11-14 09:38:50 -04:00
vexorian
c8a9c9ea53 Change GAP. 2023-11-12 12:47:22 -04:00
vexorian
4b80c6f0e5 Remove git hooks. Sorry, it's annoying. 2023-11-12 11:01:33 -04:00
vexorian
ad1302aae4 Use the median to decide what filler to play. This has memory consequences unfortunately. 2023-11-12 11:00:22 -04:00
vexorian
2e3c0b63b2 I *think* this change fixes somethign with music libraries. Let's see 2023-11-11 12:03:01 -04:00
vexorian
18491bf70f Docker login not needed for binaries build. Fix Typo. 2023-11-11 12:00:53 -04:00
vexorian
f5a070eb80 Merge branch '1.5.1-nvidiatest' into dev/1.5.x 2023-11-11 11:49:46 -04:00
vexorian
a401703304 Use github actions cache for Docker builds. 2023-11-10 23:13:12 -04:00
vexorian
de26a312f7 Also build nvidia image 2023-11-10 22:50:38 -04:00
vexorian
60a619062a Merge remote-tracking branch 'origin/main' into dev/1.5.x 2023-11-10 19:49:23 -04:00
vexorian
274f87dd7d Bump version. 2023-11-10 19:48:17 -04:00
vexorian
400ceb4b9e
Merge pull request #455 from vexorian/executables-action
Automatically build and upload binary files for releases and development branch..
2023-11-10 19:39:38 -04:00
vexorian
27375a21ae Automatically build and upload binary files for releases and development branch.. 2023-11-10 19:36:55 -04:00
vexorian
d1b3aa91ea
Merge pull request #453 from vexorian/docker-action
Github actions to build and push docker images.
2023-11-10 13:22:43 -04:00
vexorian
5fa414af5a Github actions to build and push docker images. 2023-11-10 13:21:35 -04:00
vexorian
507decff77 black.png 2022-10-15 15:47:58 -04:00
vexorian
35b828ed63 Various changes, including nvidia test 2022-08-27 18:33:19 -04:00
vexorian
32ddac1df0
Merge pull request #420 from nwithan8/track_title
Track title rather than album title in XML
2022-06-01 17:47:57 -04:00
Nate Harris
88f03638fb - Show track title rather than album title on XML for songs 2022-05-31 14:56:20 -06:00
vexorian
6898b9f31d 1.5.0 2021-09-21 22:10:34 -04:00
vexorian
7665dcf6e8 Merge branch 'dev/1.5.x' into edge 2021-09-21 22:08:58 -04:00
vexorian
005789a757
Merge pull request #381 from vexorian/20210921-dev
Button to sort fillers. Display show title (if any) in filler list. S…
2021-09-21 22:03:48 -04:00
vexorian
6af399a689 Button to sort fillers. Display show title (if any) in filler list. Sort movies by name in 'Sort Shows' 2021-09-21 21:28:36 -04:00
vexorian
31f7011c86
Merge pull request #379 from vexorian/20210921_dev
20210921 dev
2021-09-21 08:57:17 -04:00
vexorian
f18b853575 Channel Tools now have a compact version with less tools. Time/Random slots now have a reroll button to quickly refresh the schedule. Time/random slots data can be cleared from a channel. 2021-09-21 08:49:50 -04:00
vexorian
3214febb38 Merge branch 'dev/1.4.x' into dev/1.5.x 2021-09-21 08:49:25 -04:00
vexorian
cbf907788c
Merge pull request #378 from vexorian/20210921_dev
20210921 dev
2021-09-21 08:43:24 -04:00
vexorian
7b174e95a7 Fix generator name in xmltv 2021-09-21 08:41:40 -04:00
vexorian
0ad1b16369 Fix throttler bug 2021-09-21 08:41:29 -04:00
vexorian
1e72f73543 1.4.6-development 2021-09-21 08:41:09 -04:00
vexorian
ee53210f2f Random episode order in random/time slots is preserved in consecutive runs. 2021-09-20 13:42:17 -04:00
vexorian
1e2336d627 Double the slack period to prevent rewinding on-demand channels when it is a bit late. 2021-09-20 10:51:16 -04:00
vexorian
670b9a015d Merge branch 'dev/1.4.x' into dev/1.5.x 2021-09-20 10:50:47 -04:00
vexorian
382a3796e1 1.4.5 2021-09-20 09:32:48 -04:00
vexorian
989d37dcfc Merge branch 'dev/1.4.x' into main 2021-09-20 09:32:11 -04:00
vexorian
c2ffc65f83
Merge pull request #375 from vexorian/20210920_dev
20210920 dev
2021-09-20 09:30:52 -04:00
vexorian
0b1dc22e6c nodejs version warning and it also appears in version page. 2021-09-20 09:24:48 -04:00
vexorian
b7d61cb707 #374 Fix channel numbers smaller than 10 not being allowed. 2021-09-20 09:24:48 -04:00
vexorian
7b1e00d547 Fix #373 nvidia docker builds. 2021-09-20 09:24:48 -04:00
vexorian
4c5739d659 1.4.5-development 2021-09-20 09:24:48 -04:00
vexorian
b2024479f2 Merge remote-tracking branch 'origin/dev/1.4.x' into dev/1.5.x 2021-09-19 16:53:19 -04:00
vexorian
41627caad1 1.4.4 2021-09-19 14:08:20 -04:00
vexorian
601d52a4c2 Merge branch 'dev/1.4.x' into main 2021-09-19 14:07:56 -04:00
vexorian
542d42cb76
Merge pull request #372 from vexorian/20210919_dev
20210919 dev
2021-09-19 14:06:28 -04:00
vexorian
dcceb19a95 #336 make 'send guide updates' false by default. 2021-09-19 14:02:09 -04:00
vexorian
395bc48c01 TV Guide error retries with backoff so that they don't generate a tremendous amount of logs. Detect issues with channels provided to TV guide early-on. 2021-09-19 13:28:14 -04:00
vexorian
a56924463e #356 Improve channel number validations, server and client-side. 2021-09-19 13:27:28 -04:00
vexorian
78a5fa0429
Merge pull request #370 from vexorian/20210919_dev
20210919 dev
2021-09-19 01:00:00 -04:00
vexorian
460b552d37 New Collections browser now supports Smart Collections too! 2021-09-19 00:53:46 -04:00
vexorian
84abfac78b Fix #369 : A case where flex can cut abruptly, redirect-related 2021-09-19 00:53:17 -04:00
vexorian
2c11bb45fc
Merge pull request #368 from vexorian/20210918_dev
Remove some unnecessary logs
2021-09-18 18:22:53 -04:00
vexorian
388bf11e16 Remove some unnecessary logs 2021-09-18 18:20:46 -04:00
vexorian
a8f8622072 Merge branch 'dev/1.4.x' into dev/1.5.x 2021-09-13 10:27:46 -04:00
vexorian
26e883aef8
Merge pull request #367 from vexorian/20210913_dev
#365 Fix Libraries with many elements in multiple collections taking too long to load.
2021-09-13 09:50:28 -04:00
vexorian
b0220b438b #365 Fix Libraries with many elements in multiple collections taking too long to load. 2021-09-13 09:47:02 -04:00
vexorian
32dac911aa
Merge pull request #364 from vexorian/20210912_dev
20210912 dev
2021-09-12 22:29:28 -04:00
vexorian
2fed574577 Reworked the filler picked algorithm. Is it better? I don't know. But it is certainly different. 2021-09-12 22:26:16 -04:00
vexorian
5fd21137e5 #363 m3u sorting. 2021-09-11 12:05:54 -04:00
vexorian
08f0edcc33
Merge pull request #357 from vexorian/20210809_dev
Fix playback issues after the latest commit. Specially the fiurst video not playing in an on-demand channel
2021-08-09 22:38:18 -04:00
vexorian
b6b5495dd5 Fix playback issues after the latest commit. Specially the fiurst video not playing in an on-demand channel 2021-08-09 22:36:25 -04:00
vexorian
247c6902e4
Merge pull request #355 from vexorian/20210809_dev
20210809 dev
2021-08-09 00:24:12 -04:00
vexorian
30252b7d07 Channel Service refactor. Editing a channel that's currently playing will also change its current stream. 2021-08-09 00:18:35 -04:00
vexorian
9fb4db8d86 Fix rewind/fast forward not working correctly in on-demand channels. 2021-08-09 00:18:24 -04:00
vexorian
c409392797 Merge branch 'dev/1.4.x' into dev/1.5.x 2021-08-09 00:18:04 -04:00
vexorian
e001fc2ce7
Merge pull request #354 from vexorian/20210808_dev
Do not clone whole channel just to combine the channel
2021-08-08 23:37:26 -04:00
vexorian
58aa84d19f Do not clone whole channel just to combine the channel 2021-08-08 21:42:15 -04:00
vexorian
78087c16cc
Merge pull request #352 from vexorian/20210808_dev
20210808 dev
2021-08-08 15:41:14 -04:00
vexorian
a474f5a7ce Merge branch 'dev/1.4.x' into dev/1.5.x 2021-08-08 15:37:48 -04:00
vexorian
f1db4742b5
Merge pull request #351 from vexorian/20210808_dev
Fix #350 : random slots bug
2021-08-08 15:23:14 -04:00
vexorian
0ff6495872 Fix #350 : random slots no longer have a chance to generate flex times with wrong duration. Api protection to protect against bad durations. Migration step to fix existing channels that were affected by the bug 2021-08-08 15:13:24 -04:00
vexorian
0b4a8c4f6a
Merge pull request #349 from vexorian/20210806-dev
localization fixes
2021-08-06 13:52:11 -04:00
vexorian
a16e6dce66 Fix localization issues. Node version in the docker image is pointless because nexe keeps using 12. Using 14 for nexe doesn't work, so it will stay at 12 for the time being. 2021-08-06 13:49:10 -04:00
vexorian
b9d6fe9ff1
Merge pull request #339 from rafaelvieiras/ft/i18next
Internationalization System
2021-08-06 13:47:28 -04:00
vexorian
2ca8b37971
Merge pull request #348 from vexorian/dev20210806_dev
#347 On-demand channels.
2021-08-06 12:40:41 -04:00
vexorian
5d072b76bb #347 On-demand channels. 2021-08-06 11:39:38 -04:00
Rafael Vieira
eb2c3a2199 fix(i18next): focer require to commonjs module for i18next libs 2021-07-24 15:30:32 -03:00
Rafael Vieira
168838dbae chore(docker): update node image from legacy v12 to LTS v14 2021-07-24 15:28:34 -03:00
Rafael Vieira
8c22340978 feat(web): add ng-i18next and implements on xmltv-settings, plex-settings, guide and top-menu
close #266
2021-07-06 01:27:32 -03:00
Rafael Vieira
5913b7b3e9 feat(server): add i18n for translation of api
Implementation of i18n system to API translation. This change use i18next lib, simple and
extensible internationalization-framework, with support to plugins.

- out of router: use passing i18next on index.js for any class. Look tv-guide-service.js
- on a route: use req.t(). look api.js
2021-07-06 01:00:39 -03:00
vexorian
1002b0dc76
Merge pull request #338 from rafaelvieiras/ft/conventional-commits
Conventional Commits
2021-07-05 11:15:21 -04:00
Rafael Vieira
110be23b1b docs(readme): add convential-commits instructions 2021-07-04 19:14:12 -03:00
Rafael Vieira
2a843b8d0f chore(git-hooks): add Conventional Commits with lint and cli utility
Implementation of conventional commits, a pattern to commit's message with a focus on
automation and human read.

These changes don't affect the application development, build or run.
- `Commitlint` is added to validate all new commits and help developers remember to use patter,
before to send to remote repository.
- `cz-cli` aka `Commitizen` makes available a cli utility to help developer to create commits.
- `Husky` are add for easy integrate on git's hooks.
2021-07-04 18:57:06 -03:00
vexorian
7e1d48226c Merge branch 'dev/1.4.x' into dev/1.5.x 2021-07-03 12:53:19 -04:00
vexorian
43fc475d01
Merge pull request #337 from vexorian/20210703-dev
Prepare 1.4.4 development
2021-07-03 12:49:10 -04:00
vexorian
9ceb87a351 Prepare 1.4.4 development 2021-07-03 12:47:11 -04:00
vexorian
a47bc2d600
Merge pull request #333 from vexorian/20210622_dev
Align 1.4.x changes into 1.5.x
2021-06-22 01:03:45 -04:00
vexorian
1390a94642 Merge branch 'dev/1.4.x' into dev/1.5.x 2021-06-22 01:01:14 -04:00
vexorian
588c940494
Merge pull request #332 from vexorian/20210622_dev
Fix #330, programs cut off the last few milliseconds of a program, up to 30 seconds
2021-06-22 01:00:10 -04:00
vexorian
4835763e58
Merge pull request #331 from vexorian/20210622_dev
Fix #330, programs cut off the last few milliseconds of a program, up to 30 seconds
2021-06-22 00:58:22 -04:00
vexorian
fb5791d0d6 Fix #330, programs cut off the last few milliseconds of a program, up to 30 seconds 2021-06-22 00:56:50 -04:00
vexorian
06d877cd91 Last minute change: stop including package-lock.json in the git 2021-06-01 20:58:28 -04:00
vexorian
485144c791 1.4.3 2021-06-01 20:50:11 -04:00
vexorian
9bda2960e8
Merge pull request #327 from vexorian/20210601
Various small changes , preparing 1.4.3 release
2021-06-01 20:47:56 -04:00
vexorian
ce22bcd12a Processing Time Slots / Random Slots will be less likely to cause a stream freeze 2021-06-01 20:46:24 -04:00
vexorian
ae7f0ce703 Random/Time Slots should be quicker to process. 2021-06-01 20:46:24 -04:00
vexorian
a2ed1c1ec8 Fix HTMLs after angular version bump 2021-06-01 20:46:24 -04:00
vexorian
184eb2a42c Force merge 2.1.1 version 2021-06-01 20:46:24 -04:00
vexorian
5db00c2ec9
Merge pull request #326 from vexorian/20210531_dev
#308 Fix shows appearing only in two collections
2021-05-31 22:31:16 -04:00
vexorian
1ea8b93a7b #308 Fix shows appearing only in two collections 2021-05-31 22:28:06 -04:00
vexorian
8675a84b57
Merge pull request #325 from vexorian/20210531_dev
20210531 dev
2021-05-31 09:12:30 -04:00
vexorian
44341f9c9f Merge branch 'main' into dev/1.4.x 2021-05-31 09:07:24 -04:00
vexorian
b7770ca800
Merge pull request #320 from vexorian/dependabot/npm_and_yarn/ini-1.3.8
Bump ini from 1.3.5 to 1.3.8
2021-05-31 09:06:09 -04:00
vexorian
01ecb7e5f9
Merge pull request #321 from vexorian/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.5.2 to 6.5.4
2021-05-31 09:05:55 -04:00
vexorian
b87e2069cc
Merge pull request #322 from vexorian/dependabot/npm_and_yarn/axios-0.21.1
Bump axios from 0.19.2 to 0.21.1
2021-05-31 09:05:35 -04:00
vexorian
ac8bde9e75
Merge pull request #323 from vexorian/dependabot/npm_and_yarn/browserslist-4.16.6
Bump browserslist from 4.12.0 to 4.16.6
2021-05-31 09:05:23 -04:00
vexorian
d682394aca
Merge pull request #319 from vexorian/dependabot/npm_and_yarn/hosted-git-info-2.8.9
Bump hosted-git-info from 2.8.8 to 2.8.9
2021-05-31 09:05:01 -04:00
vexorian
5e3ea57e89 Fix channel config html which broke after the angular version bump 2021-05-31 09:04:03 -04:00
vexorian
a2f737649f
Merge pull request #324 from vexorian/dependabot/npm_and_yarn/lodash-4.17.21
Bump lodash from 4.17.15 to 4.17.21
2021-05-31 08:49:05 -04:00
vexorian
d6a992279b
Merge pull request #318 from vexorian/dependabot/npm_and_yarn/angular-1.8.0
Bump angular from 1.7.9 to 1.8.0
2021-05-31 08:15:44 -04:00
vexorian
98c9ed2f81 PR template 2021-05-30 23:32:42 -04:00
vexorian
e4b3acf9bc Prepare 1.5.0 development 2021-05-30 23:28:17 -04:00
vexorian
1e47a999a5 Prepare 1.4.3 development 2021-05-30 23:22:56 -04:00
dependabot[bot]
f2888ab4d8
Bump lodash from 4.17.15 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:29 +00:00
dependabot[bot]
cedf8e036c
Bump browserslist from 4.12.0 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.12.0 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.12.0...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:26 +00:00
dependabot[bot]
8a93b7af05
Bump axios from 0.19.2 to 0.21.1
Bumps [axios](https://github.com/axios/axios) from 0.19.2 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.19.2...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:25 +00:00
dependabot[bot]
6151216121
Bump elliptic from 6.5.2 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:24 +00:00
dependabot[bot]
dbbf1b0ae2
Bump ini from 1.3.5 to 1.3.8
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:23 +00:00
dependabot[bot]
b7209427bc
Bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:23 +00:00
dependabot[bot]
337c29433d
Bump angular from 1.7.9 to 1.8.0
Bumps [angular](https://github.com/angular/angular.js) from 1.7.9 to 1.8.0.
- [Release notes](https://github.com/angular/angular.js/releases)
- [Changelog](https://github.com/angular/angular.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular.js/compare/v1.7.9...v1.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 12:44:19 +00:00
vexorian
535b6cec5d Merge branch 'edge' into main 2021-05-30 08:43:24 -04:00
vexorian
cbe7a53667 1.4.2 2021-05-30 08:04:44 -04:00
vexorian
56053f1d8e Merge branch 'dev/1.4.x' into edge 2021-05-30 08:03:58 -04:00
vexorian
65507f8cb2
Merge pull request #317 from vexorian/20210530_dev
20210530 dev
2021-05-30 07:58:29 -04:00
vexorian
f134a75e98 Fix #316 . Opening a large channel in the UI causes playback to freeze for a second. 2021-05-30 07:25:24 -04:00
vexorian
df382f26f7 TV Guide will be updater much quicker than before. Thanks to using setIPmmediate instead of 0-seconds timer. 2021-05-29 16:53:25 -04:00
vexorian
ad6dcb4a33 #299 Hopefully fix the playback issues introduced when adding music library-support 2021-05-29 16:13:01 -04:00
vexorian
27b5184852
Merge pull request #314 from vexorian/20210516_dev
20210516 dev
2021-05-16 08:00:49 -04:00
vexorian
210a93043a Fix #304 arChannels was forced to be equal to arGuide 2021-05-16 07:57:57 -04:00
vexorian
ceb9a4574b Prepare 1.4.2 development 2021-05-16 07:33:23 -04:00
vexorian
35553f8285 #305 Fix custom shows can't be deleted. 2021-05-16 07:32:31 -04:00
vexorian
4236867992 Related to #308 , stop hiding collections with only 1 element. 2021-05-16 07:12:38 -04:00
vexorian
4ec285fecb #313 Fix console.err bug 2021-05-16 06:47:55 -04:00
vexorian
6b122aae5f 1.4.1 2021-03-26 10:26:27 -04:00
vexorian
2c27a87b6b Merge branch 'dev/1.4.x' into edge 2021-03-26 10:25:24 -04:00
vexorian
fb001f22f0
Update PR template
Update PR template
2021-03-26 10:15:10 -04:00
vexorian
c75c9bc8e1 Rename 1.3.3 to 1.4.1 2021-03-26 10:05:39 -04:00
vexorian
c2731f0a34 Maybe this helps? 2021-03-26 10:04:31 -04:00
vexorian
8d844f0ae3 Slots improvements. Fix rare bug in which some times the starting times would get completely messed up. Consecutive flex times are now guaranteed to be merged into a bigger one. 2021-03-26 09:59:00 -04:00
vexorian
91a5f6337e #297 include year in plex library. 2021-03-25 22:41:31 -04:00
vexorian
a742da3ae0 #286 : Update guide images when the Plex server configuration is changed. Also make sure that programs inside of filler lists and custom shows are fixed up when modifying the server or deleting it. 2021-03-25 17:13:43 -04:00
vexorian
d0f17417b7 Close button for toast notifications 2021-03-25 17:13:43 -04:00
vexorian
1854417b6c
Merge pull request #294 from vexorian/20210323_dev
20210323 dev
2021-03-23 22:15:04 -04:00
vexorian
0cf9bb314a #293 Fix Sort TV shows deleting movies 2021-03-23 22:13:12 -04:00
vexorian
715e03d154 Prepare 1.3.3 release 2021-03-23 22:03:09 -04:00
vexorian
445e9a5072 1.3.2 2021-03-23 20:03:14 -04:00
vexorian
f12940bcca Merge branch 'dev/1.3.x' into edge 2021-03-23 20:02:39 -04:00
vexorian
ac5018665c
Merge pull request #292 from vexorian/20210323_dev
20210323 dev
2021-03-23 19:59:59 -04:00
vexorian
8cce4a5d4b Merge branch 'dev/1.2.x' into dev/1.3.x 2021-03-23 19:58:19 -04:00
vexorian
543209a087
Merge pull request #291 from vexorian/20210323_dev
#287 Increase max filler cooldowns to 7 day
2021-03-23 19:57:58 -04:00
vexorian
3cdc027c83 #287 Increase max filler cooldowns to 7 day 2021-03-23 19:56:08 -04:00
vexorian
21371febd2 Custom select control in plex library, looks nicer 2021-03-23 19:52:36 -04:00
vexorian
62339d2de3 Player 2021-03-23 19:52:36 -04:00
vexorian
fe2e7770fa
Merge pull request #290 from vexorian/20210323_dev
20210323 dev
2021-03-23 17:02:30 -04:00
vexorian
8946809ba3 Custom Shows 2021-03-23 16:37:37 -04:00
vexorian
748677ac50 Episode title in TV Guide. I don't like it but I can customize the CSS now :D 2021-03-21 18:51:50 -04:00
vexorian
7a8031adc5 Only guide is wide 2021-03-21 18:43:54 -04:00
vexorian
fbbcf95bdd Customizable style.css 2021-03-21 18:12:19 -04:00
vexorian
542fa93b5b Make ffmpeg 4.3 the default in docker 2021-03-21 18:11:18 -04:00
vexorian
eca8d44af0 Prepare 1.3.2 development 2021-03-21 18:10:53 -04:00
vexorian
06e6232ce8 1.3.1 2021-03-15 00:41:18 -04:00
vexorian
a6b3bfc9f3 Merge branch 'dev/1.3.x' into edge 2021-03-15 00:40:14 -04:00
vexorian
2341c1c7d7 1.2.5 2021-03-15 00:24:07 -04:00
vexorian
ed4872ce62 Merge branch 'dev/1.2.x' into main 2021-03-15 00:23:10 -04:00
vexorian
4f80719233
Merge pull request #284 from vexorian/20210315_dev
20210315 dev
2021-03-15 00:21:02 -04:00
vexorian
de3f859dea Add flex time so that the next program in time slots happens AFTER the current time. This is specially good for weekly slots, because programming won't start at Thursday for no reason anymore. 2021-03-15 00:18:23 -04:00
vexorian
8889d5a456 #263 Remove duplicates in programming before sending to random slots endpoint 2021-03-15 00:03:55 -04:00
vexorian
99a150c9cf Merge branch 'dev/1.2.x' into dev/1.3.x 2021-03-15 00:01:53 -04:00
vexorian
8935944c88
Merge pull request #283 from vexorian/20210315_dev
20210315 dev
2021-03-15 00:00:57 -04:00
vexorian
3b422b9bea #263 remove duplicates in programming before sending to time slots endpoint, so that it doesn't needlessly give 'request entity too large' 2021-03-14 23:58:53 -04:00
vexorian
3a3567bc77 #264 fix typo 2021-03-14 23:58:53 -04:00
vexorian
4ee9f5c3e7 Make Discord invite take to the #rules channel so that members are less confused. 2021-03-14 23:58:53 -04:00
vexorian
b4941a4e44 #277 TV Guide start with correct zoom settings. 2021-03-14 23:58:53 -04:00
vexorian
a2e698bf09 Make the UI use more horizontal space, this is specially good for tv guide. 2021-03-14 23:58:53 -04:00
vexorian
7e1c5095f5
Merge pull request #278 from TimeBomb/update-plex-status-log
Improve logging around getting plex status
2021-03-07 11:02:32 -04:00
timebomb0
80b1fb8ce7 Improve logging around getting plex status 2021-03-07 03:06:09 -08:00
vexorian
276b40006d
Merge pull request #272 from vexorian/20210228_dev
20210228 dev
2021-02-28 23:18:23 -04:00
vexorian
b9365115aa Remote attachment content-disposition from xmltv tv. Using URL rather than file is recommended for xmltv setup, and the attachment disposition encourages using the file. 2021-02-28 23:15:13 -04:00
vexorian
415add6a06 Prepare 1.3.1 development 2021-02-28 23:09:32 -04:00
vexorian
bb0a480af1 Merge remote-tracking branch 'dev/1.2.x' into dev/1.3.x 2021-02-28 23:08:47 -04:00
vexorian
129e59deb9
Merge pull request #271 from vexorian/20210228_dev
20210228 dev
2021-02-28 23:06:59 -04:00
vexorian
275ae23e1e Fix season/episode numbers not appearing in TiViMate, possibly other clients 2021-02-28 23:05:46 -04:00
vexorian
cf4a60cd91 Prepare 1.2.5 development 2021-02-28 23:05:46 -04:00
vexorian
c836528e50 1.3.0 2021-02-21 00:56:24 -04:00
vexorian
921b48aa3b 1.2.4 2021-02-21 00:38:48 -04:00
vexorian
42b9bf305e
Merge pull request #262 from vexorian/20210221_dev
20210221 dev
2021-02-21 00:36:54 -04:00
vexorian
87b6bb6d85 #144 Notification toast when updating settings (and other things) 2021-02-21 00:33:25 -04:00
vexorian
d6b2bd1d5e Random Slots 2021-02-19 16:20:46 -04:00
vexorian
3fadcc487c Time Slots 2021-02-18 09:06:22 -04:00
vexorian
33f8d59959 Merge branch 'dev/1.2.x' into dev/1.3.x 2021-02-18 09:06:07 -04:00
vexorian
f239a18a0b
Merge pull request #260 from vexorian/20210215_dev
20210215 dev
2021-02-15 10:10:52 -04:00
vexorian
d39be13bf2 4 hours rerun blocks 2021-02-15 10:06:57 -04:00
vexorian
47b2fe8dd4 Max time for last-played is 7 days up from 24. I think this makes flex work nicer when the dizquetv session is long. But it's subjective. 2021-02-15 10:06:10 -04:00
vexorian
415ed27196
Merge pull request #254 from rafaelvieiras/fix/metadata-undefined
Fix Metadata Undefined bug
2021-02-15 10:03:57 -04:00
Rafael Vieira
7756bcc0da Fix Metadata Undefined bug 2021-01-26 01:26:10 -03:00
vexorian
1998d5c1e1
Merge pull request #253 from vexorian/20210124_dev
20210124 dev
2021-01-24 21:37:31 -04:00
vexorian
1978a9e837 #106 Allow group-title customization in channels. 2021-01-24 21:33:42 -04:00
vexorian
42330a1215 Cleanup settings service stuff. 2021-01-24 21:33:42 -04:00
vexorian
b118a123c1
Merge pull request #252 from vexorian/20210124_dev
Tweaks to caches
2021-01-24 16:34:27 -04:00
vexorian
0fc689bc3e Tweaks to caches 2021-01-24 16:23:34 -04:00
Rafael Vieira Santos
c97ff8f24e
File cache system (#242)
* Create a File Cache Service and Channels M3U Cache

* Create a Cache Image Service for external images

* Singleton, db configurations and repairs
2021-01-24 11:56:36 -04:00
vexorian
56d6ae3bde
Merge pull request #251 from vexorian/20210124_dev
Tweaks to music library UI. Playlists were acting strange. Year and d…
2021-01-24 11:39:39 -04:00
vexorian
fa42a3a687 Tweaks to music library UI. Playlists were acting strange. Year and date weren't getting added at times. 2021-01-24 11:37:54 -04:00
vexorian
480f12b4e9
Merge pull request #250 from vexorian/20210124_dev
Tweaks and fixes to music player.
2021-01-24 00:02:59 -04:00
vexorian
766241eaec Tweaks and fixes to music player. 2021-01-24 00:00:37 -04:00
vexorian
da8ac07567
Merge pull request #249 from vexorian/20210123_dev
Music Libraries UI
2021-01-23 20:44:08 -04:00
vexorian
9982f3c3db Music Libraries UI 2021-01-23 20:40:00 -04:00
vexorian
5650d07a54
Merge pull request #248 from vexorian/20210123_dev
#18  Allowing to play audio files in channels.
2021-01-23 13:13:04 -04:00
vexorian
9f194e62c6 #18 Allowing to play audio files in channels. They actually play now, but requires editing the channel json manually because there is no UI to import them yet. 2021-01-23 13:03:53 -04:00
vexorian
f32a6d1397
Merge pull request #247 from vexorian/20210121_dev
Align 1.2.x into 1.3.x
2021-01-21 23:58:25 -04:00
vexorian
755cf3ada8 Merge branch 'dev/1.2.x' into dev/1.3.x 2021-01-21 23:56:46 -04:00
vexorian
6c88a9f414
Merge pull request #246 from vexorian/20210121_dev
Fix enabling watermark not always loading the defaults.
2021-01-21 23:55:43 -04:00
vexorian
027e9faaa8 Fix enabling watermark not always loading the defaults. 2021-01-21 23:54:19 -04:00
vexorian
b7b489791a
Merge pull request #245 from vexorian/20210121_dev
Tweaks to image upload . Now supports watermark as well. The api has changed. Dialog browses for Images.
2021-01-21 19:38:07 -04:00
vexorian
54a6f14ff6 Tweaks to image upload . Now supports watermark as well. The api has changed. Dialog browses for Images. 2021-01-21 19:36:17 -04:00
vexorian
15f3da9434
Merge pull request #244 from rafaelvieiras/ft/channel-logo-upload
Channel Logo Upload
2021-01-20 10:55:33 -04:00
Rafael Vieira
f69b23e10e Fixed images on readme.md for docker hub 2021-01-20 00:38:48 -03:00
Rafael Vieira
3bf63be768 Create a upload for channel logos 2021-01-20 00:30:46 -03:00
vexorian
e64dc93dca
Merge pull request #241 from vexorian/20210117_dev
20210117 dev
2021-01-17 16:34:31 -04:00
vexorian
d40831a019 Audio-only streams with /radio?channel=x endpoint 2021-01-17 16:19:45 -04:00
vexorian
1b264b48b2 Fix db-migration after merge 2021-01-17 16:15:11 -04:00
vexorian
1d502f212a Merge remote-tracking branch 'fork/dev/1.2.x' into dev/1.3.x 2021-01-17 16:14:27 -04:00
vexorian
a29c1ce8dc
Merge pull request #240 from vexorian/20210117_dev
20210117 dev
2021-01-17 16:12:12 -04:00
vexorian
c7703a2b77 Try to play error stream when offline stream fails 2021-01-17 16:10:05 -04:00
vexorian
4c3e8f8d83 Use correct input file indexes in offline filters. Fix white noise, beep sometimes not making a sound at all. 2021-01-17 16:10:05 -04:00
vexorian
c495a5ae36 Fix typo in logs. 2021-01-17 16:10:05 -04:00
vexorian
62cf02addf
Merge pull request #238 from vexorian/20210116_dev
Visual Update
2021-01-16 15:18:24 -04:00
vexorian
f6537bad61 Visual Update 2021-01-16 15:15:17 -04:00
vexorian
6da809a57f
Merge pull request #237 from rafaelvieiras/fix/direct-play
Fixed Direct Play with Transcoding
2021-01-16 15:12:56 -04:00
Rafael Vieira
f0ad76b76c Fixed Direct Play with Transcoding 2021-01-15 17:28:03 -03:00
vexorian
edbfce11e7
Merge pull request #235 from jasongdove/ffmpeg-deinterlace
add ffmpeg deinterlace options
2021-01-14 17:17:54 -04:00
Jason Dove
6696d626fc add ffmpeg deinterlace options 2021-01-14 13:18:11 -06:00
vexorian
c8109aaa71 Merge branch 'dev/1.2.x' into dev/1.3.x 2021-01-10 19:04:46 -04:00
vexorian
52af53eed9 Prepare 1.2.4 development 2021-01-10 19:01:59 -04:00
vexorian
41baddedba Update PR template to point to 1.3.x 2021-01-10 18:52:19 -04:00
vexorian
4d3998b5a6
Merge pull request #229 from rafaelvieiras/fix/web-errors
Improvements on Add Plex Server UX and fixed 'of null' common errors
2021-01-10 18:49:09 -04:00
vexorian
9755d11689 Prepare 1.3.0 development 2021-01-10 18:39:00 -04:00
Rafael Vieira
dfac30b4ce Improvement of UX with a Plex Authorization Modal 2021-01-10 18:50:47 -03:00
Rafael Vieira
73cc9fb772 Fixed browser errors on Plex configuration 2021-01-10 18:48:57 -03:00
Rafael Vieira
5f4ef4386d Removed unnecessary spaces on package.json 2021-01-10 18:44:18 -03:00
vexorian
3661aa9aba
Merge pull request #227 from vexorian/20200110_main
1.2.3
2021-01-10 14:06:59 -04:00
vexorian
d0ac96acf4 Merge branch 'edge' into main 2021-01-10 14:04:24 -04:00
vexorian
ee50957974
Merge pull request #226 from vexorian/20200110_edge
1.2.3
2021-01-10 14:01:46 -04:00
vexorian
526b18275b 1.2.3 2021-01-10 13:57:46 -04:00
vexorian
9169ec65e3 Merge remote-tracking branch 'dev/1.2.x' into edge 2021-01-10 13:56:44 -04:00
vexorian
90e45ee707
Merge pull request #225 from vexorian/20200110_dev
Ability to change the scaling algorithm and default is now bicubic instead of fast_bilinear. Related to #116
2021-01-10 13:50:03 -04:00
vexorian
17e565ee6f Ability to change the scaling algorithm and default is now bicubic instead of fast_bilinear. Related to #116 2021-01-10 13:48:04 -04:00
vexorian
59253cd9ca
Merge pull request #224 from vexorian/20210109_dev
#212 add CUID tag to channels in the m3u
2021-01-09 23:32:44 -04:00
vexorian
2ac71e5864 #212 add CUID tag to channels in the m3u 2021-01-09 23:29:48 -04:00
vexorian
c30f5713ba
Merge pull request #223 from vexorian/20210109_dev
#185 : animated watermark causes stream to lock when switching between videos.
2021-01-09 20:20:29 -04:00
vexorian
1a26be30d6 #185 : animated watermark causes stream to lock when switching between videos. 2021-01-09 20:18:26 -04:00
vexorian
5fc2f7a00f
Merge pull request #222 from vexorian/20210109_dev
20210109 dev
2021-01-09 18:29:47 -04:00
vexorian
de4a80d39f #221 Fix ghost ffmpeg processes 2021-01-09 18:26:48 -04:00
vexorian
86d1329b8c Fix bug introduced with #219 , it wasn't reading a normal ffmpeg process' output 2021-01-09 18:26:15 -04:00
vexorian
a1ca124c5a
Merge pull request #220 from vexorian/20210109_dev
20210109 dev
2021-01-09 10:09:29 -04:00
vexorian
a3b74e8af5 #219 Better ffmpeg version handling. 2021-01-09 10:08:49 -04:00
vexorian
7bea25db75 Prepare 1.2.3 development 2021-01-09 10:08:38 -04:00
vexorian
1989f5ca83 1.2.2 2020-11-22 21:51:49 -04:00
vexorian
3762f032d3 Merge branch 'dev/1.2.x' into edge 2020-11-22 21:46:54 -04:00
vexorian
f428dbecf0 1.1.4 2020-11-22 21:20:44 -04:00
vexorian
d52f0adf87 Merge branch 'dev/1.1.x' into main 2020-11-22 21:19:24 -04:00
vexorian
1694a45721
Merge pull request #204 from vexorian/20201122_dev
20201122 dev
2020-11-22 21:17:37 -04:00
vexorian
e939ddb306 Merge branch 'dev/1.1.x' into dev/1.2.x 2020-11-22 21:16:17 -04:00
vexorian
c176afa8a4
Merge pull request #203 from vexorian/20201122_dev
Fix bug with remove duplicates making things break in the channel schedule editor until other tool is used.
2020-11-22 21:13:07 -04:00
vexorian
f5d073ddc1 Fix bug with remove duplicates making things break in the channel schedule editor until other tool is used. 2020-11-22 21:11:08 -04:00
vexorian
c06f1b0aad
Merge pull request #202 from vexorian/20201122_dev
20201122 dev
2020-11-22 20:28:42 -04:00
vexorian
fc79a1d79f Make time slots use the 'better' random too. Ensure that time slots are cyclic and the end of the schedule ends at the same time of day as the beginning of the schedule 2020-11-22 20:24:58 -04:00
vexorian
6329641d4c Merge branch 'dev/1.1.x' into dev/1.2.x 2020-11-22 19:25:52 -04:00
vexorian
4e842c71ee
Merge pull request #201 from vexorian/20201122_dev
20201122 dev
2020-11-22 19:21:27 -04:00
vexorian
110bd19a61 Improve random number generation for picking the next filler to play. 2020-11-22 19:18:11 -04:00
vexorian
1254928cd6 Fix bug with the direct path bug fix 2020-11-22 19:18:11 -04:00
vexorian
e7030764b0
Merge pull request #200 from vexorian/20201122_dev
Implement graceful shutdown procedure.
2020-11-22 17:02:24 -04:00
vexorian
852d09fc55 Implement graceful shutdown procedure. So far the only aspect that seems to need is is the xmltv writer, we want to make sure the xmltv is completely written before closing the app. #176 2020-11-22 16:58:59 -04:00
vexorian
63e1a74151
Merge pull request #199 from vexorian/20201122_dev
20201122 dev
2020-11-22 11:13:48 -04:00
vexorian
ad08fca671 Time Slots improvement. Now by default tries to distribute flex times between videos in the same slot #184. Option to make it work like before. 10 minutes padding option. I don't care about lateness option. Fix bug that could be caused by two consecutive time slots with the same tv show (or both are movies) causing HUGE flex times. I basically rewrote the whole thing, so enjoy. 2020-11-22 11:12:24 -04:00
vexorian
20ed585c99 Merge branch 'dev/1.1.x' into dev/1.2.x 2020-11-22 11:11:39 -04:00
vexorian
fdeff28979 Set discord link to #rules channel instead of #new-features 2020-11-21 18:00:00 -04:00
vexorian
7a1cc1632c
Merge pull request #198 from vexorian/20201121_dev
#171 Do not require Plex connection to play videos when direct paths are enabled.
2020-11-21 17:46:52 -04:00
vexorian
f878125320 #171 Do not require Plex connection to play videos when direct paths are enabled. 2020-11-21 17:45:19 -04:00
vexorian
77145d09d0
Merge pull request #196 from vexorian/20201121_dev
#183 Enable 10 and 15 seconds breaks in Add Breaks. fix issue where the 30 seconds option was actually the same as the 20 seconds option...
2020-11-21 16:14:22 -04:00
vexorian
a8e4e1ae90 #183 Enable 10 and 15 seconds breaks in Add Breaks. fix issue where the 30 seconds option was actually the same as the 20 seconds option... 2020-11-21 16:12:47 -04:00
vexorian
b1dc992e6d
Merge pull request #195 from vexorian/20201121_dev
Add arrows to control channel programming height.
2020-11-21 00:13:17 -04:00
vexorian
8a9043067b Add arrows to control channel programming height.
work-around for #182
2020-11-21 00:11:55 -04:00
vexorian
ad87a746bd
Merge pull request #193 from vexorian/20201120_dev
20201120 dev
2020-11-20 08:10:44 -04:00
vexorian
d95187a809 Fix channel list drag and drop after merge. 2020-11-20 08:09:04 -04:00
vexorian
4a19dc69fc Merge branch 'dev/1.1.x' into dev/1.2.x 2020-11-20 08:01:25 -04:00
vexorian
573527ebee
Merge pull request #192 from vexorian/20201120_dev
Fix all drag and drop issues in channel and filler lists.
2020-11-20 07:59:09 -04:00
vexorian
61e9f0cc67 Fix all drag and drop issues in channel and filler lists. 2020-11-20 07:57:34 -04:00
vexorian
1f498d611c Merge branch 'dev/1.1.x' into dev/1.2.x 2020-11-19 22:21:14 -04:00
vexorian
5488e4a2bf Prepare 1.2.2 development 2020-11-19 22:18:18 -04:00
vexorian
88c9c8e17d Fix #190 , #191 and also drag and drop issue for Filler List Editor. 2020-11-19 22:16:19 -04:00
vexorian
1d3b5c9408 Prepare 1.1.4 development 2020-11-19 18:51:40 -04:00
vexorian
dbfc8bcbf7 1.2.1 2020-10-30 01:42:19 -04:00
vexorian
ed4224f3d7 Merge branch 'dev/1.2.x' into edge 2020-10-30 01:41:20 -04:00
vexorian
3ab4b3328c 1.1.3 2020-10-30 01:27:39 -04:00
vexorian
ca99b0c4ef Merge branch 'dev/1.1.x' into main 2020-10-30 01:26:57 -04:00
vexorian
f8b842bf0e
Merge pull request #179 from vexorian/20201030_dev
20201030 dev
2020-10-30 01:25:24 -04:00
vexorian
aac156ba85 Fix #163, 0 seconds for minimum time not saving 2020-10-30 01:22:03 -04:00
vexorian
759ac4f2ff Merge branch 'dev/1.1.x' into dev/1.2.x 2020-10-30 01:14:15 -04:00
vexorian
27c06f5ea0
Merge pull request #178 from vexorian/20201030_dev
20201030 dev
2020-10-30 01:10:31 -04:00
vexorian
82a6ad8acf Hopefully improvements to the TV Guide service. Let's detect the random bug that was reported and at least repair the TV guide in that case, instead of crashing everything. 2020-10-30 01:08:17 -04:00
vexorian
3ae972841a nvidia unraid template should use latest-nvidia 2020-10-30 01:08:17 -04:00
vexorian
c548ede724
Merge pull request #175 from vexorian/20201017_dev
Fix #167, extra / in plex uri gives no error during backend route check, but fails when playing the video.
2020-10-17 15:55:11 -04:00
vexorian
09e19c5d20 Fix #167, extra / in plex uri gives no error during backend route check, but fails when playing the video. 2020-10-17 15:51:45 -04:00
vexorian
218dcc9524
Merge pull request #174 from vexorian/20201017_dev
20201017 dev
2020-10-17 12:52:46 -04:00
vexorian
cd28ee24a5 Fix #164 also for channel's program list post-1.2.x 2020-10-17 12:51:02 -04:00
vexorian
23ef2ca7c6 Merge branch 'dev/1.1.x' into dev/1.2.x 2020-10-17 12:44:17 -04:00
vexorian
b561806f13 Prepare 1.2.1 development 2020-10-17 12:37:42 -04:00
vexorian
59aa843fb6
Merge pull request #173 from vexorian/20201017_dev
Fix #164 : Filler List getting compressed in Webkit (Safari)
2020-10-17 12:36:04 -04:00
vexorian
f2bb9f69cd Prepare 1.1.3 development 2020-10-17 12:34:24 -04:00
vexorian
e864de124b Fix #164 : Filler List getting compressed in Webkit (Safari) 2020-10-17 12:33:30 -04:00
vexorian
cdcc2abb6c
Merge pull request #168 from vexorian/20201013_dev
New 'visual identity' aka I just changed the logo and favicons again
2020-10-13 20:01:42 -04:00
vexorian
734636cb95 New 'visual identity' aka I just changed the logo and favicons again 2020-10-13 19:59:34 -04:00
vexorian
35cef7ec10
Merge pull request #161 from vexorian/20201010_112
20201010 112
2020-10-10 11:23:36 -04:00
vexorian
c967542922 1.2.0 2020-10-10 11:05:13 -04:00
vexorian
bfffe11dbd Merge branch 'dev/1.2.x' into edge 2020-10-10 11:04:27 -04:00
vexorian
397f7c6ace
Merge pull request #160 from vexorian/20201010_dev
20201010 dev
2020-10-10 11:01:25 -04:00
vexorian
cdc9ebceff Merge branch 'dev/1.1.x' into dev/1.2.x 2020-10-10 10:59:30 -04:00
vexorian
6a327a937e 1.1.2 2020-10-10 10:42:27 -04:00
vexorian
863d8c0bfc Merge branch 'dev/1.1.x' into main 2020-10-10 10:41:44 -04:00
vexorian
40a34d2bba
Merge pull request #159 from vexorian/20201010_dev
Update the favicon so that it (hopefully) looks less like ebay's colorful shopping bag that looks bizaringly a lot like a TV
2020-10-10 10:32:33 -04:00
vexorian
1d853d73f5 Update the favicon so that it (hopefully) looks less like ebay's colorful shopping bag that looks bizaringly a lot like a TV 2020-10-10 10:31:27 -04:00
vexorian
8c35d09895
Merge pull request #158 from vexorian/20201010_dev
20201010 dev
2020-10-10 09:35:02 -04:00
vexorian
70078c0140 Time Slots 2020-10-10 09:10:15 -04:00
vexorian
a827316723 Fix bufSize bug 2020-10-07 10:29:02 -04:00
vexorian
8ccbef998a
Merge pull request #156 from vexorian/20201006_dev
Channel resolution and bitrate settings.
2020-10-06 23:35:58 -04:00
vexorian
7aef9f21c7 Channel resolution and bitrate settings. 2020-10-06 23:33:29 -04:00
vexorian
5962022ef3
Merge pull request #155 from vexorian/20201005_dev
Fixerror when channels don't have the watermark enabled.
2020-10-05 18:39:20 -04:00
vexorian
3b5eb0475c Fix that crash when channels don't have the watermark enabled. 2020-10-05 18:36:44 -04:00
vexorian
31b1c4b9b1
Merge pull request #154 from vexorian/20201005_dev
FFMPEG channel editor tab. Channel watermark options.
2020-10-05 18:04:46 -04:00
vexorian
1067d03442 FFMPEG channel editor tab. Channel watermark options. 2020-10-05 18:03:20 -04:00
vexorian
de73640753
Merge pull request #152 from vexorian/20201004_dev
FF/REW channel tools
2020-10-04 00:19:21 -04:00
vexorian
35f0d814db FF/REW channel tools 2020-10-04 00:17:49 -04:00
vexorian
de04fa2c15
Merge pull request #151 from vexorian/20201004_dev
Channel tools can be changed. The editor will remember tools being open and their position. Some visual tweaks
2020-10-03 18:03:50 -04:00
vexorian
fa3cea0d52 Channel tools can be changed. The editor will remember tools being open and their position. Some visual tweaks 2020-10-03 18:01:03 -04:00
vexorian
0c5e54195f
Merge pull request #148 from TimeBomb/tools-button-polish
Polish styles for Tools buttons
2020-10-03 14:28:38 -04:00
Jason Baler
624de57ae9 Implement custom select into tools 2020-10-03 10:09:53 -07:00
vexorian
189deacba0
Merge pull request #150 from vexorian/20201003_dev
Flex tab for channel editor. Can change how flex time and short videos behave in the TV guide
2020-10-03 11:50:42 -04:00
vexorian
8077d7de53 Flex tab for channel editor. Can change how flex time and short videos behave in the TV guide 2020-10-03 11:44:27 -04:00
vexorian
0435787761
Merge pull request #149 from vexorian/20201003_dev
Flex tab in channel editor
2020-10-03 10:11:17 -04:00
vexorian
6cc0cce2d2 Flex tab in channel editor 2020-10-03 10:09:36 -04:00
Jason Baler
0378577fc6 Polish styles for Tools buttons 2020-10-02 22:27:03 -07:00
vexorian
398ee0e83f
Merge pull request #147 from vexorian/20201002_dev
Channel Editor Rework
2020-10-02 23:20:07 -04:00
vexorian
f719fc2a91 Channel Editor Rework 2020-10-02 23:18:47 -04:00
vexorian
23449b5021
Merge pull request #145 from vexorian/20200929_dev
Option  to limit the framerate of dizqueTV's output
2020-09-29 11:52:46 -04:00
vexorian
bd0ca01281 Option to limit the framerate of dizqueTV's output 2020-09-29 11:50:04 -04:00
vexorian
9a01c4dc4d
Update pull_request_template.md
Update PR template for 1.2
2020-09-28 20:16:28 -04:00
vexorian
5a1db96837 Prepare 1.2.0 development 2020-09-28 20:07:58 -04:00
vexorian
7250248345 1.1.1 2020-09-26 15:59:58 -04:00
vexorian
f18f1600c3 Merge branch 'dev/1.1.x' into edge 2020-09-26 15:59:01 -04:00
vexorian
715088ef0a
Merge pull request #143 from vexorian/20200926_dev
20200926 dev
2020-09-26 15:56:07 -04:00
vexorian
7a4c3bd709 Merge branch 'dev/1.0.x' into dev/1.1.x 2020-09-26 15:53:15 -04:00
vexorian
c1269e48e6 Merge branch 'dev/1.0.x' into main 2020-09-26 15:19:00 -04:00
vexorian
de89618c88 1.0.2 2020-09-26 15:07:30 -04:00
vexorian
e9a269e1f2
Merge pull request #142 from vexorian/20200926_dev
20200926 dev
2020-09-26 15:05:24 -04:00
vexorian
435e151258 Phonetic pronuntiation to avoid confusion 2020-09-26 15:02:35 -04:00
vexorian
c5a3a0de89 Tweak default bitrates a bit. Clarify that they are in Kbps in Plex settings 2020-09-26 15:02:35 -04:00
vexorian
10e231adb1 Add explanations to EPG settings. 2020-09-26 15:02:35 -04:00
vexorian
c2a8bdc4c9 Fix xmltv writer crashing when a rating in the channel json is null for some reason (usually because of the python library) 2020-09-26 15:02:35 -04:00
vexorian
c114cab269 #110 expose url-tvg and x-tvg-url fields in m3u 2020-09-26 15:02:35 -04:00
vexorian
04107ab652
Merge pull request #140 from vexorian/20200926_dev
Fix anamorphic video (again), when audio is transcoded by plex but video is direct played. #127
2020-09-26 12:11:20 -04:00
vexorian
18bd87dcaf Fix anamorphic video (again), when audio is transcoded by plex but video is direct played. #127 2020-09-26 12:09:03 -04:00
vexorian
8ab7a29e02
Merge pull request #139 from vexorian/20200926_dev
Align dev/1.0.x into dev/1.1.x
2020-09-26 08:08:52 -04:00
vexorian
2557b78c6b Add limit to plex-library, it is now mandatory after the 1.0.x merge. 2020-09-26 08:05:52 -04:00
vexorian
c6454aa227 Merge branch 'dev/1.0.x' into dev/1.1.x 2020-09-26 08:05:17 -04:00
vexorian
c498223642
Merge pull request #138 from vexorian/20200926_dev
Fix #123. Deal with channel limits. Channel numbers 0-9999 allowed now
2020-09-26 07:53:13 -04:00
vexorian
a0693c934a Fix #123. Deal with channel limits. Tools and library will be limited to a maximum channel size 2020-09-26 07:46:47 -04:00
vexorian
df7200b20e
Merge pull request #136 from vexorian/20200924_dev
20200924 dev
2020-09-24 19:02:00 -04:00
vexorian
cd3012a042 Merge branch 'dev/1.0.x' into dev/1.1.x 2020-09-24 19:00:17 -04:00
vexorian
a9e0eb2014 Merge branch 'main' into dev/1.0.x 2020-09-24 18:57:39 -04:00
vexorian
0df7622a32 dizqueTV license 2020-09-24 18:49:41 -04:00
vexorian
837780fde9 Merge remote-tracking branch 'origin/master' into main 2020-09-24 18:12:39 -04:00
Dan Ferguson
665e71e24e
Create LICENSE 2020-09-24 17:02:41 -04:00
vexorian
c6fab8def4
Merge pull request #134 from vexorian/20200923_dev
20200923 dev
2020-09-23 19:17:38 -04:00
vexorian
8a6fb782ad Attempts to make error screen more likely to appear instead of stream just dying 2020-09-23 19:15:41 -04:00
vexorian
43bf85db20 Fix #69 (nice?) Hopefully for good this time. It was already very difficult to create an infinite cache loop. Now even if the worst happens, it might repeat the last few seconds of a video once but nothing more. 2020-09-23 19:15:41 -04:00
vexorian
c4514b3255
Merge pull request #133 from vexorian/20200922_dev
Fix #132 Save Program not working at all.
2020-09-22 12:19:30 -04:00
vexorian
e9fe6001e1 Fix #132 Save Program not working at all. 2020-09-22 12:17:08 -04:00
vexorian
9a26b1f0ea
Merge pull request #131 from vexorian/20200921_dev
20200921 dev
2020-09-21 23:17:37 -04:00
vexorian
a9341f12c8 Fix up fillers before saving them to the file. 2020-09-21 23:15:45 -04:00
vexorian
01f8557ba6 Merge remote-tracking branch 'dev/1.0.x' into dev/1.1.x 2020-09-21 22:43:02 -04:00
vexorian
3e1d207e1b Prepare 1.1.1 development 2020-09-21 22:42:09 -04:00
vexorian
885204a1bd
Merge pull request #130 from vexorian/20200921_dev
20200921 dev
2020-09-21 21:21:31 -04:00
vexorian
a2bdb5c1ea #126 fix tool help 2020-09-21 21:18:31 -04:00
vexorian
51c978ce37 Prepare 1.0.2 development 2020-09-21 21:18:31 -04:00
vexorian
760f13cecc 1.1.0 2020-09-20 22:52:39 -04:00
vexorian
0bc6c4a7a2
Merge pull request #128 from vexorian/20200920_dev
Filler Lists
2020-09-20 22:51:24 -04:00
vexorian
d1a9be6058 Filler Lists 2020-09-20 22:42:17 -04:00
vexorian
4c385d0442
Merge pull request #122 from vexorian/20200918_dev
20200918 dev
2020-09-18 00:30:41 -04:00
vexorian
9dd82c7d30 Merge branch 'dev/1.0.x' into dev/1.1.x 2020-09-18 00:28:39 -04:00
vexorian
621f261a59 1.0.1 2020-09-17 23:51:33 -04:00
vexorian
098df2eba2
Merge pull request #121 from vexorian/20200917_dev
20200917 dev
2020-09-17 23:48:53 -04:00
vexorian
10c615a828 Fix guide sending tons and tons of status requests to backend, even after leaving the guide 2020-09-17 23:43:46 -04:00
vexorian
44d90df24b Fix long blocks of consecutive small flex time periods, getting melded together with the previous program. 2020-09-17 23:43:46 -04:00
vexorian
2a72744809 Loading screen duration to 40 milliseconds instead of 1 second. 2020-09-17 23:43:46 -04:00
vexorian
4bfef9fd38 Fix slowness when adding filler when there are a lot of fillers in the list. 2020-09-17 23:43:46 -04:00
vexorian
e53d7f7dcd Prepare 1.1.0 development 2020-09-15 21:24:49 -04:00
vexorian
a81cf78f5c Prepare 1.0.1 development 2020-09-15 21:22:59 -04:00
vexorian
7f342b6ebc PR template 2020-09-15 21:20:33 -04:00
vexorian
87de403f00 Merge branch 'dev/1.0.x' into main 2020-09-15 20:57:32 -04:00
vexorian
eb52242609 1.0.0 2020-09-15 20:01:46 -04:00
vexorian
baf236102e contribute 2020-09-12 01:04:29 -04:00
vexorian
adbc6ede51 0.0.69 2020-09-11 23:14:36 -04:00
vexorian
4667ccc050 Merge branch 'dev/0.0.x' into main 2020-09-11 23:11:20 -04:00
vexorian
d4cb8c0429
Merge pull request #112 from vexorian/20200911_dev
20200911 dev
2020-09-11 23:05:22 -04:00
vexorian
36939ccf74 favicon , reddit icon. 2020-09-11 23:01:32 -04:00
vexorian
fa58d59c82 New channel tools: Replicate and Replicate + Shuffle 2020-09-11 23:01:04 -04:00
vexorian
2bcb14083e README.md improvements, make it link to the wiki 2020-09-11 23:01:04 -04:00
vexorian
a08bc351d5
Merge pull request #111 from vexorian/20200911_dev
Prepare 0.0.69 development
2020-09-11 00:38:59 -04:00
vexorian
8fc9430aaa Prepare 0.0.69 development 2020-09-11 00:37:09 -04:00
vexorian
2b6d148b47
Merge pull request #108 from vexorian/20200910_68
0.0.68
2020-09-10 20:00:44 -04:00
vexorian
910b56a5c0 0.0.68 2020-09-10 19:58:37 -04:00
vexorian
29ea556b23 Merge branch 'dev/0.0.x' into main 2020-09-10 19:57:43 -04:00
vexorian
2e0ffb27fc
Merge pull request #107 from vexorian/20200910_dev
20200910 dev
2020-09-10 19:54:52 -04:00
vexorian
1c03df78d3 Horizontal Duration Indicator 2020-09-10 19:51:28 -04:00
vexorian
1a874f62d8 New Channel Took : Save/Recover tv show positions. 2020-09-10 19:51:28 -04:00
vexorian
904444ebc5 Do not allow to enable subtitles if direct play is forced. 2020-09-10 19:51:28 -04:00
vexorian
61b6d67f21 Improved resilience to errors in streams. Error stream shouldn't die abruptly anymore. 2020-09-10 19:51:28 -04:00
vexorian
93c9ad710b
Merge pull request #104 from vexorian/20200909_dev
20200909 dev
2020-09-09 16:03:07 -04:00
vexorian
5a0080c6b7 Discord icon and font awesome 5 (which in my opinion is noticieably worse than 4, but it has the discord icon) 2020-09-09 16:01:11 -04:00
vexorian
e506ac15b0 Fix Memory usage peaking during TV Guide Generation 2020-09-09 16:01:11 -04:00
vexorian
87fb2baa73 Icons for tool buttons 2020-09-09 16:01:10 -04:00
vexorian
0158832fe0 Channel at night / restrict hours: Differentiate between padding time and restricted time. Fixes #96 2020-09-09 16:01:10 -04:00
vexorian
9e89b572bb
Merge pull request #100 from vexorian/20200907_night
20200907 night
2020-09-07 22:06:12 -04:00
vexorian
307507a223 Fix #95 Channel at night channels not rendering in TV guide when used as redirect. 2020-09-07 22:03:59 -04:00
vexorian
7dcaf70608 Fix unraid repo in readme 2020-09-07 22:03:59 -04:00
vexorian
9b636ae2d4
Merge pull request #99 from vexorian/20200907_vs
Virtual Scroll for channel editor. Big thanks to @TimeBomb for the help.
2020-09-07 21:55:41 -04:00
vexorian
1395c681ca Virtual Scroll for channel editor. Big thanks to @TimeBomb for the help. 2020-09-07 21:47:57 -04:00
vexorian
07b96e258d
Merge pull request #93 from vexorian/20200906_68
Prepare 0.0.68 development
2020-09-06 07:51:48 -04:00
vexorian
4dffb666d3 Prepare 0.0.68 development 2020-09-06 07:50:10 -04:00
vexorian
0dc116ada2 0.0.67 2020-09-05 21:10:27 -04:00
vexorian
7bad6ffcae Merge branch 'dev/0.0.x' into main 2020-09-05 21:09:30 -04:00
vexorian
15d889c8bb
Merge pull request #92 from vexorian/20200905_67
20200905 67
2020-09-05 21:07:18 -04:00
vexorian
e2b94c62cb Hotfix for 0.0.66 issues: xmltv path not being configurable and xmltv cache option being ignored. 2020-09-05 20:29:43 -04:00
vexorian
d5661201ed
Merge pull request #91 from vexorian/20200905_67dev
Prepare 0.0.67 development
2020-09-05 19:12:48 -04:00
vexorian
d4607a86a6 Prepare 0.0.67 development 2020-09-05 19:10:50 -04:00
vexorian
27823ac665 0.0.66 2020-09-05 18:37:01 -04:00
vexorian
5bf8bab6f2 Merge branch 'dev/0.0.x' into main 2020-09-05 18:36:19 -04:00
vexorian
569d24d3a1
Merge pull request #90 from vexorian/20200905_dev
20200905 dev
2020-09-05 18:34:22 -04:00
vexorian
47d9b235b6 Fix some last minute bugs with new setups 2020-09-05 18:31:00 -04:00
vexorian
62a58a5a46 Stealth mode for channels 2020-09-05 17:57:41 -04:00
vexorian
89132d8ac8 Play button in TV guide. 2020-09-05 17:57:41 -04:00
vexorian
461dfea071 - "Smart" resize instead of relying on ffmpeg for the logic. This fixes the bug with anamorphic video getting messed up aspect ratios.
- Normalize resolution will always normalize anamorphic videos, even if they have the same dimensions.
- When transcoding and normalize resolution being off, padding will not be added. But there's a special case for odd dimensions, where it needs to add 1 pixel.
- Changed scaling algorithm to 'billinear_fast'. It has 'fast' in the name, so hopefully it's faster?
2020-09-05 17:57:41 -04:00
vexorian
ee08b3a601 TV Guide > Fix bug with missing redirects. Now keeps retryiung in case of error. 2020-09-05 17:57:41 -04:00
vexorian
98c5f5c118
Merge pull request #87 from vexorian/20200904_guide
20200904 guide
2020-09-04 21:53:25 -04:00
vexorian
40079d385d Minor Web UI tip fixes. 2020-09-04 21:51:42 -04:00
vexorian
9bd3e9063b - Channel redirects will now render in the TV Guide.
- Programs shorter than 5 minutes will not appear in the TV Guide and will be treated the same way Flex time is treated normally.
- The Web UI now has its own TV Guide viewer so that you don't have to rely on clients to preview your channels' line ups.
- API: New endpoint to get what shows are being scheduled for a given time frame.
- Fix bug where some bad luck could cause two different XMLTVs to get written at once, generating a corrupt xmltv file.
2020-09-04 21:48:30 -04:00
vexorian
a24ce52f41 Nexe cache = Improve build times. 2020-08-30 10:58:04 -04:00
vexorian
80dfcc17a0 Prepare 0.0.66 development 2020-08-26 22:45:07 -04:00
vexorian
d6a15c39aa
Merge pull request #72 from vexorian/20200826_main
0.0.65
2020-08-26 21:22:43 -04:00
vexorian
0499d434a8 0.0.65 2020-08-26 21:21:39 -04:00
vexorian
665487e812 Merge branch 'dev/0.0.x' into main 2020-08-26 21:20:59 -04:00
vexorian
5411f6c17b
Merge pull request #71 from vexorian/20200826_dev
20200826 dev
2020-08-26 21:18:50 -04:00
vexorian
05833c3d14 Log message to explain when ffmpeg is transcoding or copying streams. Removed minimum bitrates... 2020-08-26 21:15:04 -04:00
vexorian
c342d42db9 xmltv delivery is not synchronous anymore. It is no longer possible to change xmltv file location through UI or the api. Basic exception handling for the API but the api will need further cleanup. 2020-08-26 20:50:23 -04:00
vexorian
c6787f9f2a Reruns 2020-08-26 20:50:23 -04:00
vexorian
1c0b3c1620
Merge pull request #67 from vexorian/20200824_dev
20200824 dev
2020-08-24 22:49:10 -04:00
vexorian
2138176689 Channel redirects + 'Channel At night' 2020-08-24 22:47:27 -04:00
vexorian
c4a0b7af96
Merge pull request #62 from TimeBomb/remove-show-button
Implement Remove Show(s) Channel Config UI
2020-08-22 19:07:28 -04:00
Jason Baker
2859c90b2c Implement Remove Show(s) Channel Config UI 2020-08-22 16:05:08 -07:00
vexorian
00e0e8b5ba
Merge pull request #63 from vexorian/20200822_65
Prepare 0.0.65 development
2020-08-22 18:55:26 -04:00
vexorian
78e7b8d1a9 Prepare 0.0.65 development 2020-08-22 18:53:57 -04:00
vexorian
3e1e637fd4 0.0.64 2020-08-22 17:42:31 -04:00
vexorian
6a13a513a6
Merge pull request #61 from vexorian/20200822_64
20200822 64
2020-08-22 17:38:29 -04:00
vexorian
8422bae2ed Fix channels tab permanently showing a loading wheel when there are no channels around. 2020-08-22 17:33:36 -04:00
vexorian
b1f3090dcb Fix fresh install issue with it generating a ghost plex server. 2020-08-22 17:33:36 -04:00
vexorian
973a2f9721 Fix /m3u8 endpoint being broken 2020-08-22 16:38:25 -04:00
vexorian
5fcb5d9571
Merge pull request #58 from vexorian/20200822_64
20200822 64
2020-08-22 11:29:03 -04:00
vexorian
971697405b Prepare 0.0.64 development 2020-08-22 11:25:59 -04:00
vexorian
3ab7de3769 0.0.63 2020-08-22 10:46:01 -04:00
vexorian
4b800090e2 Merge branch 'dev/0.0.x' into main 2020-08-22 10:45:15 -04:00
vexorian
1f5bd3e5a0
Merge pull request #57 from vexorian/20200822_dev
20200822 dev
2020-08-22 10:40:08 -04:00
vexorian
9cc5bd4a88 Also cleanup streams and opts in migration 2020-08-22 10:37:40 -04:00
vexorian
3022dfe375 1 json per channel. Plex server editing and status. Max resolution for transcoding. 640x360 fix. 2020-08-22 10:37:40 -04:00
vexorian
55c22846bf Use stream.pipe instead of transporting the bytes manually. Hopefully this improves playback. 2020-08-22 10:37:40 -04:00
vexorian
e87e1c13a1
PR template
PR template
2020-08-18 00:25:41 -04:00
vexorian
f80d763e3c
Merge pull request #41 from cbarbara/genre-support
genre support for movies
2020-08-17 23:54:58 -04:00
Chris Barbara
4f0451c285 fix bug to block adding movies from genres multiple times. 2020-08-17 19:50:01 -07:00
vexorian
5138ced433 Dockerfile for executables builder. 2020-08-17 21:34:03 -04:00
Chris Barbara
7355b52dcd genre support for movies 2020-08-17 06:42:34 -07:00
vexorian
389b54cc96
Merge pull request #49 from vexorian/480x270
480x270 resolution in ffmpeg
2020-08-17 08:52:27 -04:00
vexorian
19e9eb7f58 480x270 resolution 2020-08-17 08:50:18 -04:00
vexorian
2efe1c0f48 Improve Dockerfiles, but the build is slower regardless of caching, because nexe is hideously limited, we really need to find a better way to package these things 2020-08-16 21:47:26 -04:00
vexorian
ba4c6cd9d0 Testing nvidia dockerfile 2020-08-16 10:27:10 -04:00
vexorian
42f6ba54ea
Merge pull request #39 from vexorian/tvg-chno
tvg-chno in M3U
2020-08-16 09:03:47 -04:00
vexorian
ab0da60228 tvg-chno in M3U 2020-08-16 09:01:30 -04:00
vexorian
e057b5eb9d 0.0.63-development 2020-08-15 21:48:11 -04:00
vexorian
b43eb4703b
Merge pull request #35 from vexorian/pushmain20200815
Release 0.0.62
2020-08-15 18:35:45 -04:00
vexorian
968e567b92 Version 0.0.62 2020-08-15 18:33:27 -04:00
vexorian
cc299bb0bc Merge 'dev/0.0.x' into main 2020-08-15 18:32:41 -04:00
vexorian
77b8da63c4
Merge pull request #34 from vexorian/push20200815
Changes for 0.0.62
2020-08-15 18:30:46 -04:00
vexorian
52f9b20764 Made the library browser more reslient, hopefully errors won't cut the loading of programs but they will be reported in the page. 2020-08-15 18:23:31 -04:00
vexorian
01494250e9 New channel tool to remove specials. Add Plex will insert the plex at the current view position. 2020-08-15 18:22:26 -04:00
vexorian
709b8e1605 Fix bug with loading screen causing streams to break when normalization was disabled. /m3u8 endpoint doesn't require normalization. 2020-08-15 18:20:53 -04:00
vexorian
f4c2ac4940 Now really fix the bug that made playing videos shorter than 10 seconds loop permanently. 2020-08-15 18:17:37 -04:00
vexorian
05a8b2c4af Improve version tab, include UI and ffmpeg versions. Channels in m3u get sorted by channel number because apparently that's important. 2020-08-15 18:16:23 -04:00
vexorian
f11cab29d6 Fix issues (specially UNRAID) that arose from changing the main branch name to main 2020-08-15 18:07:15 -04:00
vexorian
5efc5c6afc Revert "Test with higher probesize"
This reverts commit c5cadb74bdabba2fd55fcccf2a44db7690cc1741.
2020-08-15 16:09:26 -04:00
vexorian
c5cadb74bd Test with higher probesize 2020-08-15 15:35:27 -04:00
vexorian
f054d8dfcb
Prepare development on 0.0.62
Prepare development on 0.0.62
2020-08-15 00:32:44 -04:00
vexorian
ac60c32d2c
Merge pull request #30 from vexorian/main20200814
Merge version 0.0.61 into main
2020-08-14 23:48:18 -04:00
vexorian
f30a14b204 Version 0.0.61 2020-08-14 23:45:57 -04:00
vexorian
54349f757b
Merge pull request #29 from vexorian/friday20200814
Changes in 61
2020-08-14 23:43:16 -04:00
vexorian
b1163e2d00 * Improve the performance of the media importer, so that it doesn't become much slower after adding many programs.
* Progress Wheel during program import.
* Collections support
* Ability to import whole libraries (get ready to experience the joys of having huge channels)
2020-08-14 23:36:06 -04:00
vexorian
65fa8a22dd Improve ffmpeg settings tooltips 2020-08-14 23:35:49 -04:00
vexorian
b911f29b5f After adding programs, scroll channel to the added programs 2020-08-14 23:35:27 -04:00
vexorian
b54b5d9112 * Fix Plex mobile apps spamming a new notification every time a video plays.
* Loading Screen.
* Minor log improvements.
* Minor defaults improvements.
* FFMPEG concat process to use 1 thread.
* Fix av1 bug when plex has to transcode the audio.
* /m3u8 endpoint
2020-08-14 23:31:28 -04:00
vexorian
db70e56129
Merge pull request #28 from vexorian/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2020-08-14 22:49:50 -04:00
vexorian
f715e8f160
Create CODE_OF_CONDUCT.md 2020-08-14 22:49:22 -04:00
vexorian
053c6d53fc Merge branch 'main' into dev/0.0.x 2020-08-14 22:43:48 -04:00
vexorian
23f42e42eb
Merge pull request #12 from McCloudS/patch-2
Update ffmpeg.js
2020-08-13 13:43:28 -04:00
McCloudS
3431f06655
Update ffmpeg.js
missing quote after show name for FFMPEG
2020-08-13 12:39:18 -05:00
vexorian
e72d0bf160 Bump version 0.0.60 2020-08-11 21:07:03 -04:00
vexorian
a06989bf26 Merge branch 'dev/0.0.x' 2020-08-11 21:06:00 -04:00
vexorian
027f9a1a74 bump dev version 2020-08-11 21:05:43 -04:00
vexorian
5a377655a6 Rename some files, README fixes 2020-08-11 21:04:50 -04:00
vexorian
005a514660 Merge branch 'dev/0.0.x' 2020-08-11 18:43:42 -04:00
vexorian
d8892a4dc6 Update readme screenshots 2020-08-11 18:43:14 -04:00
vexorian
f77a619540 Merge remote-tracking branch 'vexorian/dev/0.0.x' 2020-08-10 22:56:22 -04:00
vexorian
0bc810ccec Introducing dizqueTV 2020-08-10 22:53:40 -04:00
vexorian
4f9bbba5e5 0.0.60-vx . Dependabot is a bad bot. 2020-08-10 00:34:26 -04:00
vexorian
7d9fc9f3cf Merge remote-tracking branch 'vexorian/development' 2020-08-10 00:33:12 -04:00
vexorian
71fc2e278a Revert "Bump angular from 1.7.9 to 1.8.0"
This reverts commit e068b4bb3c4d6932ba508837e70a2075688a025d.
2020-08-10 00:32:36 -04:00
vexorian
000928024c Fork Version 0.0.59 2020-08-09 23:39:33 -04:00
vexorian
b0f310adb0 Merge remote-tracking branch 'vexorian/development' 2020-08-09 19:16:04 -04:00
vexorian
21afdec8ba Merge remote-tracking branch 'flying-sausages/patch-1' into development 2020-08-09 19:14:39 -04:00
vexorian
20934d05d3 Fork version 0.0.58 2020-08-09 15:18:23 -04:00
vexorian
9fb1498225 Merge with branch 'vexorian/development' 2020-08-09 15:14:25 -04:00
vexorian
82bfd50535 Remove old 'commercials' feature. DB Migration in place. Backwards compatibility with pseudotv instances made between versions 0.0.51 and 0.0.53. Old commercials get replaced with Flex time 2020-08-09 15:09:47 -04:00
vexorian
06c11a1d16 Fix issue with concurrent streams playing when they both require Plex transcoding. One would be forcefully stopped by Plex. 2020-08-09 15:01:52 -04:00
vexorian
3184306e86 Major channel UI changes. Remove commercials. Tools for filler editor. Visible Color and Length signatures for programs and flex. 2020-08-09 14:58:59 -04:00
vexorian
451b4ede14 Fix unexpected when attempting to play error/offline streams. Fix streams ending abruptly after some minutes unless ffmpeg logs are enabled. 2020-08-09 14:52:16 -04:00
flying-sausages
11bf5021d4
Added some small shields
Some shields for what users might want to find at the top of the readme. Using https://shields.io/
2020-08-05 13:31:43 +02:00
vexorian
97b8fd73e1 Fork 0.0.57 2020-08-04 22:47:28 -04:00
vexorian
370e68a189 Merge remote-tracking branch 'vexorian/development' 2020-08-04 22:46:38 -04:00
vexorian
e1d79490a5 Fix Pad Time bug. To improve on channel loading times in the UI, channel editor will only show 100 programs at once, but there's a slider to see more. 2020-08-04 22:45:59 -04:00
vexorian
0e1d2e4bb1 Tweaked Flex random algorithm AGAIN 2020-08-04 22:45:48 -04:00
vexorian
3fef59c4da Fix Pad Time bug. To improve on channel loading times in the UI, channel editor will only show 100 programs at once, but there's a slider to see more. 2020-08-04 22:43:15 -04:00
vexorian
3391e9173f Tweaked Flex random algorithm AGAIN 2020-08-04 22:38:44 -04:00
vexorian
3c3b3544f1 Fork version 0.0.56 2020-08-01 07:13:47 -04:00
vexorian
9414f0b971 New channel programming tool: Tweak Weigths. Also more padding options. 2020-08-01 07:01:20 -04:00
vexorian
0450dcec37 TV guide will no longer have holes in it due to Flex times, instead the previous program will be extended OR the channel's name will be used if the Flex time is too long. 2020-08-01 07:00:23 -04:00
vexorian
861c1d9bda Tweak random filler selection, give a bit of bias to larger videos, but just a bit. 2020-08-01 06:58:34 -04:00
vexorian
664d3b30c9 Fix large filesizes when building with nexe 2020-07-28 19:42:49 -04:00
vexorian
8e9d9e9a11 nexe instead of pkg 2020-07-28 15:40:57 -04:00
Austin Tinius
becb8bc176
Merge pull request #58 from DEFENDORe/dependabot/npm_and_yarn/angular-1.8.0
Bump angular from 1.7.9 to 1.8.0
2020-06-23 10:39:32 -07:00
dependabot[bot]
e068b4bb3c
Bump angular from 1.7.9 to 1.8.0
Bumps [angular](https://github.com/angular/angular.js) from 1.7.9 to 1.8.0.
- [Release notes](https://github.com/angular/angular.js/releases)
- [Changelog](https://github.com/angular/angular.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/angular/angular.js/compare/v1.7.9...v1.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-18 15:32:55 +00:00
Austin Tinius
47b0bf54b8
Merge pull request #30 from benmcmath/patch-1
Adding support for Raspberry Pi
2020-05-21 12:20:41 -07:00
benmcmath
003602f92f
Adding support for Raspberry Pi
I wanted to run this on my raspberry pi that controls my plex server.
2020-05-20 21:40:11 -04:00
148 changed files with 16556 additions and 10307 deletions

View File

@ -1,9 +1,10 @@
node_modules
npm-debug.log
Dockerfile
*Dockerfile*
.dockerignore
.git
.gitignore
bin
dist
.pseudotv
.pseudotv
.dizquetv

13
.github/workflows/README.md vendored Normal file
View File

@ -0,0 +1,13 @@
# The workflow
edge main
^ \ ^
| \------2--\ |
1 \ 3
| \ |
development <-4-- patch
1. Releasing a new 'edge' version.
2. Moving an 'edge' version to stable.
3. Releasing a stable version
4. Aligning bug fixes with the new features.

46
.github/workflows/binaries-build.yaml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Build Executables and Update Release
on:
workflow_call:
inputs:
release:
required: true
type: string
jobs:
release-files:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build dist image
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile-builder
load: true
tags: builder
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run dist docker
run: |
docker run -v ./dist:/home/node/app/dist builder sh make_dist.sh
- name: Upload Files
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ inputs.release }}
files: |
./dist/dizquetv-win-x64.exe
./dist/dizquetv-win-x86.exe
./dist/dizquetv-linux-x64
./dist/dizquetv-macos-x64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -0,0 +1,13 @@
name: Development Binaries
on:
push:
branches:
- development
jobs:
binaries:
uses: ./.github/workflows/binaries-build.yaml
with:
release: development-binaries
secrets: inherit

13
.github/workflows/development-tag.yaml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Development Tag
on:
push:
branches:
- development
jobs:
docker:
uses: ./.github/workflows/docker-build.yaml
with:
tag: development
secrets: inherit

44
.github/workflows/docker-build.yaml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Docker Build
on:
workflow_call:
inputs:
tag:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- name: Default
Dockerfile: Dockerfile
suffix: ""
- name: nvidia
Dockerfile: Dockerfile-nvidia
suffix: "-nvidia"
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
file: ${{ matrix.Dockerfile }}
tags: vexorian/dizquetv:${{ inputs.tag }}${{ matrix.suffix }}
cache-from: type=gha
cache-to: type=gha,mode=max

13
.github/workflows/edge-tag.yaml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Edge Tag
on:
push:
branches:
- edge
jobs:
docker:
uses: ./.github/workflows/docker-build.yaml
with:
tag: edge
secrets: inherit

13
.github/workflows/latest-tag.yaml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Latest Tag
on:
push:
branches:
- main
jobs:
docker:
uses: ./.github/workflows/docker-build.yaml
with:
tag: latest
secrets: inherit

13
.github/workflows/named-tag.yaml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Named Tag
on:
push:
tags:
- '*'
jobs:
docker:
uses: ./.github/workflows/docker-build.yaml
with:
tag: ${{ github.ref_name }}
secrets: inherit

13
.github/workflows/tag-binaries.yaml vendored Normal file
View File

@ -0,0 +1,13 @@
name: Release Binaries
on:
push:
tags:
- "*"
jobs:
binaries:
uses: ./.github/workflows/binaries-build.yaml
with:
release: ${{ github.ref_name }}
secrets: inherit

5
.gitignore vendored
View File

@ -2,4 +2,7 @@ node_modules/
dist/
bin/
.pseudotv/
web/public/bundle.js
.dizquetv/
web/public/bundle.js
*.orig
package-lock.json

67
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,67 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We pledge to make our community welcoming, safe, and equitable for all.
We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant.
## Encouraged Behaviors
While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language.
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
2. Engaging **kindly and honestly** with others.
3. Respecting **different viewpoints** and experiences.
4. **Taking responsibility** for our actions and contributions.
5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of our community**.
## Restricted Behaviors
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyones personality or behavior on the basis of immutable identities or traits.
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our community.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
## 'AI' policy
There are ways in which a LLM-based tool can be helpful such as when searching the web or for learning. More so, tools like github and google themselves are being modified to railroad users into using LLM-based tools. It'd be really impractical and neigh-impossible to ban 'AI' altogether from being used during the development of a contribution and that's not really the purpose of this policy.
HOWEVER, from a legal standpoint, it is too difficult to know the origin of LLM-generated code so there are risks of it infringing on copyright. And from a pragmatic stand point, we want to be able to trust the quality of the code. That is to say, we do not want contributions where the bulk of the code was AI-generated or or where the contributors themselves do not understand the code being pushed. This is also in relation to items 4 and 5 of the encouraged behaviors. We want contributors that can vouch for the code they contribute and can receive and act on constructive feedback to it.
Note that this is only a policy affecting Pull Requests to this project. This project is released under a permissive FOSS licence, so there's nothing really that can stop forks from having a different policy on this and any individual is allowed to create such a fork should they want to.
## Reporting an Issue
Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm.
When an incident does occur, it is important to report it promptly. To report a possible violation, may be
reported by contacting the project team at vexorian@gmail.com
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 3.0,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html . The AI policy is a modification.
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,50 +1,14 @@
FROM node:12.18-alpine3.12
# Should be ffmpeg v4.2.3
ARG LIBDAV1D_VERSION=0.7.1
ARG LIBDAV1D_URL="https://code.videolan.org/videolan/dav1d/-/archive/$LIBDAV1D_VERSION/dav1d-$LIBDAV1D_VERSION.tar.gz"
RUN apk add --update \
curl nasm yasm build-base gcc zlib-dev libc-dev openssl-dev yasm-dev lame-dev libogg-dev x264-dev libvpx-dev libvorbis-dev x265-dev freetype-dev libass-dev libwebp-dev rtmpdump-dev libtheora-dev opus-dev meson ninja && \
wget -O dav1d.tar.gz "$LIBDAV1D_URL" && \
tar xfz dav1d.tar.gz && \
cd dav1d-* && meson build --buildtype release -Ddefault_library=static && ninja -C build install && \
DIR=$(mktemp -d) && cd ${DIR} && \
curl -s http://ffmpeg.org/releases/ffmpeg-4.2.3.tar.gz | tar zxvf - -C . && \
cd ffmpeg-4.2.3 && \
./configure \
--enable-version3 \
--enable-gpl \
--enable-nonfree \
--enable-small \
--enable-libmp3lame \
--enable-libx264 \
--enable-libdav1d \
--enable-libx265 \
--enable-libvpx \
--enable-libtheora \
--enable-libvorbis \
--enable-libopus \
--enable-libass \
--enable-libwebp \
--enable-librtmp \
--enable-postproc \
--enable-avresample \
--enable-libfreetype \
--enable-openssl \
--enable-filter=drawtext \
--disable-debug && \
make && \
make install && \
make distclean && \
rm -rf ${DIR} && \
mv /usr/local/bin/ffmpeg /usr/bin/ffmpeg && \
apk del build-base curl tar bzip2 x264 openssl nasm openssl xz gnupg && rm -rf /v
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install
RUN npm install -g browserify
EXPOSE 8000
CMD [ "npm", "start"]
COPY package.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
COPY . .
RUN npm run build
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
FROM jrottenberg/ffmpeg:4.3-ubuntu1804
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg

6
Dockerfile-builder Normal file
View File

@ -0,0 +1,6 @@
FROM node:14-alpine3.14
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
COPY --from=vexorian/dizquetv:nexecache /var/nexe/* /var/nexe/
COPY . .

14
Dockerfile-nvidia Normal file
View File

@ -0,0 +1,14 @@
FROM node:14-alpine3.14
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install && npm install -g browserify nexe@3.3.7
COPY --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
COPY . .
RUN npm run build && LINUXBUILD=dizquetv sh make_dist.sh linuxonly
FROM jrottenberg/ffmpeg:4.3-nvidia1804
EXPOSE 8000
WORKDIR /home/node/app
ENTRYPOINT [ "./dizquetv" ]
COPY --from=0 /home/node/app/dist/dizquetv /home/node/app/
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
zlib License
Copyright (c) 2020 Dan Ferguson, Victor Hugo Soliz Kuncar
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.

164
README.md
View File

@ -1,142 +1,57 @@
# pseudotv-plex
# dizqueTV 1.5.5
![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square)
Create live TV channel streams from media on your Plex servers.
Project recently migrated from [gitlab](https://gitlab.com/DEFENDORe/pseudotv-plex) to github to improve development flow (docker builds and binary releases).
**dizqueTV** ( *dis·keˈtiːˈvi* ) is a fork of the project previously-known as [pseudotv-plex](https://gitlab.com/DEFENDORe/pseudotv-plex) or [pseudotv](https://github.com/DEFENDORe/pseudotv). New repository because of lack of activity from the main repository and the name change is because projects with the old name already existed and were created long before this approach and it was causing confusion. You can migrate from pseudoTV 0.0.51 to dizqueTV by renaming the .pseudotv folder to .dizquetv and running the new executable (or doing a similar trick with the volumes used by the docker containers).
<img src="./resources/pseudotv.png" width="200">
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png" width="200">
Configure your channels, programs, commercials and settings using the PseudoTV web UI.
Configure your channels, programs, commercials and settings using the dizqueTV web UI.
Access your channels by adding the spoofed PseudoTV HDHomerun tuner to Plex, or utilize the M3U Url with any 3rd party app.
Access your channels by adding the spoofed dizqueTV HDHomerun tuner to Plex, Jellyfin or emby or utilize the M3U Url with any 3rd party IPTV player app.
EPG (Guide Information) data is stored to `.pseudotv/xmltv.xml`
EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml`
## Features
- Docker image and prepackage binaries for Windows, Linux and Mac
- Web UI for channel configuration and app settings
- A wide variety of options for the clients where you can play the TV channels, since it both spoofs a HDHR tuner and a IPTV channel list.
- Ease of setup for xteve and Plex playback by mocking a HDHR server.
- Configure your channels once, and play them just the same in any of the other devices.
- Customize your channels and what they play. Make them display their logo while they play. Play filler content (&quot;commercials&quot;, music videos, prerolls, channel branding videos) at specific times to pad time.
- Docker image and prepackage binaries for Windows, Linux and Mac.
- Supports nvidia for hardware encoding, including in docker.
- Select media (desired programs and commercials) across multiple Plex servers
- Sign into your Plex servers using any sign in method (Username, Email, Google, Facebook, etc.)
- Ability to auto update Plex DVR guide data and channel mappings
- Auto update the xmltv.xml file at a set interval (in hours). You can also set the amount EPG cache (in hours).
- Continuous playback support
- Ability to add breaks or padding between episodes and use Commercials, Trailers, Bumpers or other filler.
- Commercial support. 5 commercial slots for a program (BEFORE, 1/4, 1/2, 3/4, AFTER). Place as many commercials as desired per slot to chain commercials.
- Media track selection (video, audio, subtitle). (subtitles disabled by default)
- Includes a WEB TV Guide where you can even play channels in your desktop by using your local media player.
- Subtitle support.
- Ability to overlay channel icon over stream
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
- Can be configured to completely force Direct play.
- Can normalize video formats to prevent stream breaking.
## Useful Tips/Info
- Internal and External SRT/ASS subtitles may cause a delay when starting stream (only when subtitles are activated). For internal SRT/ASS subtitles, FFMPEG needs to perform a subtitle track extraction from the original media before the requested stream can be started. External SRT/ASS subtitle files still need to be sliced to the correct start time and duration so even they may cause a delay when starting a stream. Image based subs (PGS) should have little to no impact.
- Utilize your hardware accelerated encoders, or use mpeg2 instead of h264 by changing the default video encoder in FFMPEG settings. *Note that some encoders may not be capable of handling every transcoding scenario, libx264 and mpeg2video seem to be the most stable.*
- Intel Quick Sync: `h264_qsv`, `mpeg2_qsv`
- NVIDIA GPU: `h264_nvenc`
- MPEG2 `mpeg2video`
- H264 `libx264` (default)
- MacOS `h264_videotoolbox`
- **Enable the option to log ffmpeg's stderr output directly to the pseudotv app console, for detecting issues**
- Host your own images for channel icons, program icons, etc.. Simply add your image to `.pseudotv/images` and reference them via `http://pseudotv-ip:8000/images/myImage.png`
- Use the Block Shuffle feature to play a specified number of TV episodes before advancing to the next available TV show in the channel. You can also specify to randomize the TV Show order. Any movies added to the channel will be pushed to the end of the program lineup, this is also applicable the "Sort TV Shows" option.
- Plex is smart enough not to open another stream if it currently is being viewed by another user. This allows only one transcode session for mulitple viewers if they are watching the same channel.
- Even if your Plex server is running on the same machine as the PseudoTV app, use your network address (not a loopback) when configuring your Plex Server(s) in the web UI.
- Can be configured to completely force Direct play, if you are ready for the caveats.
- It's up to you if the channels have a life of their own and act as if they continued playing when you weren't watching them or if you want "on-demand" channels that stop their schedules while not being watched.
## Limitations
- Plex Pass is required to unlock Plex Live TV/DVR feature
- Only one EPG source can be used with Plex server. This may cause an issue if you are adding the pseudotv tuner to a Plex server with Live TV/DVR already enabled/configured.
- If you want to play the TV channels in Plex using the spoofed HDHR, Plex pass is required.
- dizqueTV does not currently watch your Plex server for media updates/changes. You must manually remove and re-add your programs for any changes to take effect. Same goes for Plex server changes (changing IP, port, etc).. You&apos;ll have to update the server settings manually in that case.
- Most players (including Plex) will break after switching episodes if video / audio format is too different. dizqueTV can be configured to use ffmpeg transcoding to prevent this, but that costs resources.
- If you configure Plex DVR, it will always be recording and transcoding the channel&apos;s contents.
- In its current state, dizquetv is intended for private use only and you should be discouraged from running dizqueTV in any capacity where other users can have access to dizqueTV's ports. You can use Plex's iptv player feature to share dizqueTV streams or you'll have to come up with some work arounds to make sure that streams can be played without ever actually exposing the dizquetv port to the outside world. Please use it with care, consider exposing dizqueTV's ports as something only advanced users who know what they are doing should try.
* There are projects like xteve that allow you to unify multiple EPG sources into a single list which Plex can use.
## Releases
- PseudoTV does not watch your Plex server for media updates/changes. You must manually remove and readd your programs for any changes to take effect. Same goes for Plex server changes (changing IP, port, etc).. all media will fail..
- Many IPTV players (including Plex) will break after switching episodes if video / audio format is too different between. PseudoTV can be configured to use ffmpeg transcoding to prevent htis, but that costs resources.
- https://github.com/vexorian/dizquetv/releases
## Installation
## Wiki
*Please delete your old `.pseudotv` directory before using the new build. I'm sorry but it'd take more effort than its worth to convert the old databases..*
- For setup instructions, check [the wiki](https://github.com/vexorian/dizquetv/wiki)
Unless your are using Docker/Unraid, you must download and install **ffmpeg** to your system and set the correct path in the PseudoTV Web UI.
By default, pseudotv will create the directory `.pseudotv` wherever pseudotv is launched from. Your `xmltv.xml` file and config databases are stored here.
#### Binary Release
[Download](https://github.com/DEFENDORe/pseudotv/releases) and run the PseudoTV executable (argument defaults below)
```
./pseudotv-win-x64.exe --port 8000 --database ./pseudotv
```
#### Docker
The Docker repository can be viewed [here](https://hub.docker.com/r/defendore/pseudotv).
Use Docker to fetch PseudoTV, then run the container.. (replace `C:\.pseudotv` with your desired config directory location)
```
docker pull defendore/pseudotv
docker run --name pseudotv -p 8000:8000 -v C:\.pseudotv:/home/node/app/.pseudotv defendore/pseudotv
```
#### Building Docker image from source
Build docker image from source and run the container. (replace `C:\.pseudotv` with your desired config directory location)
```
git clone https://github.com/DEFENDORe/pseudotv
cd pseudotv-plex
docker build -t pseudotv .
docker run --name pseudotv -p 8000:8000 -v C:\.pseudotv:/home/node/app/.pseudotv pseudotv
```
#### Unraid Install
Add
```
https://github.com/DEFENDORe/pseudotv
```
to your "Template repositories" in the Docker tab.
Click the "Add Container" button
Select either the pseudotv template or the pseudotv-nvidia template if you want nvidia hardware accelerated transcoding.
Make sure you have the Unraid Nvidia plugin installed and change your video encoder to h264_nvenc in the pseudotv ffmpeg settings.
#### From Source
Install NodeJS and FFMPEG
```
git clone https://github.com/DEFENDORe/pseudotv
cd pseudotv-plex
npm install
npm run build
npm run start
```
## Plex Setup
Add the PseudoTV spoofed HDHomerun tuner to Plex via Plex Settings.
If the tuner isn't automatically listed, manually enter the network address of pseudotv. Example:
```
127.0.0.1:8000
```
When prompted for a Postal/Zip code, click the `"Have an XMLTV guide on your server? Click here to use that instead."` link.
Enter the location of the `.pseudotv/xmltv.xml` file. Example (Windows):
```
C:\.pseudotv\xmltv.xml
```
**Do not use the Web UI XMLTV URL when feeding Plex the xmltv.xml file. Plex fails to update it's EPG from a URL for some reason (at least on Windows). Use the local file path to `.pseudotv/xmltv.xml`**
## App Preview
<img src="./docs/channels.png" width="500">
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/channels.png" width="500">
<br/>
<img src="./docs/channel-config.png" width="500">
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/channel-config.png" width="500">
<br/>
<img src="./docs/plex-guide.png" width="500">
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/plex-guide.png" width="500">
<br/>
<img src="./docs/plex-stream.png" width="500">
<img src="https://raw.githubusercontent.com/vexorian/dizquetv/main/docs/plex-stream.png" width="500">
## Development
Building/Packaging Binaries: (uses `browserify`, `babel` and `pkg`)
@ -151,3 +66,24 @@ Live Development: (using `nodemon` and `watchify`)
npm run dev-client
npm run dev-server
```
## Contribute
* Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first.
* Tip Jar: https://buymeacoffee.com/vexorian
## License
* Original pseudotv-Plex code was released under [MIT license (c) 2020 Dan Ferguson](https://github.com/DEFENDORe/pseudotv/blob/665e71e24ee5e93d9c9c90545addb53fdc235ff6/LICENSE)
* dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar
* FontAwesome: [https://fontawesome.com/license/free](https://archive.fo/PRqis)
* Bootstrap: https://github.com/twbs/bootstrap/blob/v4.4.1/LICENSE
## Thanks
* DEFENDORe , George and everyone that worked on PseudoTV
* Ahmed Said Al-Busaidi , for reporting exploits in advance before publishing in exploit-db.
* Nathan for working on automation addons.
* Timebomb, Rafael for the contributions during dizqueTV's active days.

1
commitlint.config.js Normal file
View File

@ -0,0 +1 @@
module.exports = {extends: ['@commitlint/config-conventional']}

View File

@ -1,30 +1,30 @@
<?xml version="1.0"?>
<Container version="2">
<Name>pseudotv</Name>
<Repository>defendore/pseudotv:latest</Repository>
<Registry>https://hub.docker.com/r/defendore/pseudotv</Registry>
<Name>dizquetv</Name>
<Repository>vexorian/dizquetv:latest-nvidia</Repository>
<Registry>https://hub.docker.com/r/vexorian/dizquetv</Registry>
<Network>host</Network>
<MyIP/>
<Shell>bash</Shell>
<Privileged>false</Privileged>
<Support/>
<Project/>
<Overview>PseudoTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all manged throught the PseudoTV Web UI.&#xD;
<Overview>dizqueTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all managed through the dizqueTV Web UI.&#xD;
&#xD;
PseudoTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.pseudotv/xmltv.xml file for EPG data. PseudoTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Overview>
dizqueTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.dizquetv/xmltv.xml file for EPG data. dizqueTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Overview>
<Category/>
<WebUI>http://[IP]:[PORT:8000]</WebUI>
<TemplateURL/>
<Icon>https://raw.githubusercontent.com/DEFENDORe/pseudotv/master/resources/pseudotv.png</Icon>
<Icon>https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png</Icon>
<ExtraParams>--runtime=nvidia</ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled>1589436589</DateInstalled>
<DonateText/>
<DonateLink/>
<Description>PseudoTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all manged throught the PseudoTV Web UI.&#xD;
<Description>dizqueTV is a Plex DVR plugin. It allows you to host your own fake live tv service by dynamically streaming media from your Plex servers(s). Your channels and settings are all manged throught the dizqueTV Web UI.&#xD;
&#xD;
PseudoTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.pseudotv/xmltv.xml file for EPG data. PseudoTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Description>
dizqueTV will show up as a HDHomeRun device within Plex. When configuring your Plex Tuner, simply use the generatered ./.dizque/xmltv.xml file for EPG data. dizqueTV will automatically refresh your Plex server's EPG data and channel mappings (if specified to do so in settings) when configuring channels via the Web UI. Ensure your FFMPEG path is set correctly via the Web UI, and enjoy!</Description>
<Networking>
<Mode>host</Mode>
<Publish>
@ -37,8 +37,8 @@ PseudoTV will show up as a HDHomeRun device within Plex. When configuring your P
</Networking>
<Data>
<Volume>
<HostDir>/mnt/user/appdata/pseudotv/</HostDir>
<ContainerDir>/home/node/app/.pseudotv</ContainerDir>
<HostDir>/mnt/user/appdata/dizquetv/</HostDir>
<ContainerDir>/home/node/app/.dizquetv</ContainerDir>
<Mode>rw</Mode>
</Volume>
</Data>
@ -58,5 +58,5 @@ PseudoTV will show up as a HDHomeRun device within Plex. When configuring your P
<Config Name="Webui &amp;amp; HDHR" Target="8000" Default="" Mode="tcp" Description="Container Port: 8000" Type="Port" Display="always" Required="false" Mask="false">8000</Config>
<Config Name="NVIDIA_VISIBLE_DEVICES" Target="NVIDIA_VISIBLE_DEVICES" Default="" Mode="" Description="Container Variable: NVIDIA_VISIBLE_DEVICES" Type="Variable" Display="always" Required="false" Mask="false">all</Config>
<Config Name="NVIDIA_DRIVER_CAPABILITIES" Target="NVIDIA_DRIVER_CAPABILITIES" Default="" Mode="" Description="Container Variable: NVIDIA_DRIVER_CAPABILITIES" Type="Variable" Display="always" Required="false" Mask="false">all</Config>
<Config Name="Appdata" Target="/home/node/app/.pseudotv" Default="" Mode="rw" Description="Container Path: /home/node/app/.pseudotv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/pseudotv/</Config>
<Config Name="Appdata" Target="/home/node/app/.dizquetv" Default="" Mode="rw" Description="Container Path: /home/node/app/.dizquetv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/dizquetv/</Config>
</Container>

View File

@ -1,8 +1,8 @@
<?xml version="1.0"?>
<Container version="2">
<Name>pseudotv</Name>
<Repository>defendore/pseudotv:latest</Repository>
<Registry>https://hub.docker.com/r/defendore/pseudotv</Registry>
<Name>dizquetv</Name>
<Repository>vexorian/dizquetv:latest</Repository>
<Registry>https://hub.docker.com/r/vexorian/dizquetv</Registry>
<Network>host</Network>
<MyIP/>
<Shell>bash</Shell>
@ -13,7 +13,7 @@
<Category/>
<WebUI>http://[IP]:[PORT:8000]</WebUI>
<TemplateURL/>
<Icon>https://raw.githubusercontent.com/DEFENDORe/pseudotv/master/resources/pseudotv.png</Icon>
<Icon>https://raw.githubusercontent.com/vexorian/dizquetv/main/resources/dizquetv.png</Icon>
<ExtraParams></ExtraParams>
<PostArgs/>
<CPUset/>
@ -33,8 +33,8 @@
</Networking>
<Data>
<Volume>
<HostDir>/mnt/user/appdata/pseudotv/</HostDir>
<ContainerDir>/home/node/app/.pseudotv</ContainerDir>
<HostDir>/mnt/user/appdata/dizquetv/</HostDir>
<ContainerDir>/home/node/app/.dizquetv</ContainerDir>
<Mode>rw</Mode>
</Volume>
</Data>
@ -42,5 +42,5 @@
</Environment>
<Labels/>
<Config Name="Webui &amp;amp; HDHR" Target="8000" Default="" Mode="tcp" Description="Container Port: 8000" Type="Port" Display="always" Required="false" Mask="false">8000</Config>
<Config Name="Appdata" Target="/home/node/app/.pseudotv" Default="" Mode="rw" Description="Container Path: /home/node/app/.pseudotv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/pseudotv/</Config>
<Config Name="Appdata" Target="/home/node/app/.dizquetv" Default="" Mode="rw" Description="Container Path: /home/node/app/.dizquetv" Type="Path" Display="always" Required="false" Mask="false">/mnt/user/appdata/dizquetv/</Config>
</Container>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

After

Width:  |  Height:  |  Size: 665 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 869 KiB

After

Width:  |  Height:  |  Size: 735 KiB

433
index.js

File diff suppressed because it is too large Load Diff

15
locales/server/en.json Normal file
View File

@ -0,0 +1,15 @@
{
"event":{
"server_started": "Server Started",
"server_shutdown": "Initiated Server Shutdown"
},
"api": {
"plex_server_not_found": "Plex server not found.",
"missing_name": "Missing name"
},
"tvGuide": {
"no_channels": "No channels configured",
"no_channels_summary": "Use the dizqueTV web UI to configure channels.",
"xmltv_updated": "XMLTV updated at server time {{t}}"
}
}

34
make_dist.sh Normal file
View File

@ -0,0 +1,34 @@
#!/bin/sh
MODE=${1:-all}
WIN64=dizquetv-win-x64.exe
WIN32=dizquetv-win-x86.exe
MACOSX=dizquetv-macos-x64
LINUX64=${LINUXBUILD:-dizquetv-linux-x64}
rm -R ./dist/*
npm run build || exit 1
npm run compile || exit 1
cp -R ./web ./dist/web
cp -R ./resources ./dist/
cp -R ./locales/ ./dist/locales/
cd dist
if [ "$MODE" == "all" ]; then
nexe --temp /var/nexe -r "./**/*" -t windows-x64-12.18.2 --output $WIN64
mv $WIN64 ../
nexe --temp /var/nexe -r "./**/*" -t mac-x64-12.18.2 --output $MACOSX
mv $MACOSX ../
nexe --temp /var/nexe -r "./**/*" -t windows-x86-12.18.2 --output $WIN32
mv $WIN32 ../
fi
nexe --temp /var/nexe -r "./**/*" -t linux-x64-12.16.2 --output $LINUX64 || exit 1
echo dist/$LINUX64
if [ "$MODE" == "all" ]; then
mv ../$WIN64 ./
mv ../$WIN32 ./
mv ../$MACOSX ./
echo dist/$WIN64
echo dist/$MACOSX
echo dist/$WIN32
fi

7351
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "pseudotv",
"name": "dizquetv",
"version": "1.0.0",
"description": "Create LiveTV channels from your Plex media",
"main": "index.js",
@ -10,46 +10,50 @@
"dev-client": "watchify ./web/app.js -o ./web/public/bundle.js",
"dev-server": "nodemon index.js --ignore ./web/ --ignore ./db/ --ignore ./xmltv.xml",
"compile": "babel index.js -d dist && babel src -d dist/src",
"package": "copyfiles ./web/public/**/* ./dist && copyfiles ./resources/**/* ./dist && pkg . --out-path bin",
"clean": "del-cli --force ./bin ./dist ./.pseudotv ./web/public/bundle.js"
"package": "sh ./make_dist.sh",
"clean": "del-cli --force ./bin ./dist ./.dizquetv ./web/public/bundle.js"
},
"author": "Dan Ferguson",
"license": "ISC",
"author": "vexorian",
"license": "Zlib",
"dependencies": {
"angular": "^1.7.9",
"angular": "^1.8.0",
"angular-router-browserify": "0.0.2",
"axios": "^0.19.2",
"angular-sanitize": "^1.8.2",
"angular-vs-repeat": "2.0.13",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"diskdb": "^0.1.17",
"diskdb": "0.1.17",
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"i18next": "^20.3.2",
"i18next-fs-backend": "^1.1.1",
"i18next-http-backend": "^1.2.6",
"i18next-http-middleware": "^3.1.4",
"JSONStream": "1.0.5",
"merge": "2.1.1",
"ng-i18next": "^1.0.7",
"node-graceful-shutdown": "1.1.0",
"node-ssdp": "^4.0.0",
"quickselect": "2.0.0",
"random-js": "2.1.0",
"request": "^2.88.2",
"uuid": "^8.0.0",
"uuid": "9.0.1",
"unzipper": "0.10.14",
"xml-writer": "^1.7.0"
},
"bin": "dist/index.js",
"pkg": {
"assets": [
"dist/web/public/**/*",
"dist/resources/**/*"
],
"targets": [
"x64",
"linux",
"macos",
"windows"
]
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.0",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/preset-env": "^7.9.5",
"@commitlint/cli": "^12.1.4",
"@commitlint/config-conventional": "^12.1.4",
"browserify": "^16.5.1",
"copyfiles": "^2.2.0",
"del-cli": "^3.0.0",
"nexe": "^3.3.7",
"nodemon": "^2.0.3",
"pkg": "^4.4.7",
"watchify": "^3.11.1"
},
"babel": {

25
pull_request_template.md Normal file
View File

@ -0,0 +1,25 @@
### Explanation of the changes, problem that they are intended to fix.
...
### All Submissions:
* [ ] I have read the code of conduct.
* [ ] I am submitting to the correct base branch
<!--
* Bug fixes for 'stable' versions must go to `patch`.
* New features and fixes for 'edge' version must go to `development`.
-->
### Changes that modify the db structure
* [ ] Backwards compatibility: Users running the new code using an existing .disquetv folder will not lose their channels / settings.
* [ ] I've implemented the necessary db migration steps if any.
### New features
* [ ] I understand that the feature may not be accepted if it doesn't fit the upstream app's planned design direction. But that in this case I am encouraged to share this as an available modification other users can use if they want.
### Code Standards
* [ ] I understand the code being contributed and it's purpose. <!-- Please read CODE_OF_CONDUCT for more info. -->

BIN
resources/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

View File

@ -0,0 +1,14 @@
/** For example : */
:root {
--guide-text : #F0F0f0;
--guide-header-even: #423cd4ff;
--guide-header-odd: #262198ff;
--guide-color-a: #212121;
--guide-color-b: #515151;
--guide-color-c: #313131;
--guide-color-d: #414141;
}

BIN
resources/dizquetv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

136
resources/favicon.svg Normal file
View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="200"
height="200"
viewBox="0 0 52.9168 52.916668"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="/home/vx/dev/pseudotv/resources/favicon-16.png"
inkscape:export-xdpi="7.6799998"
inkscape:export-ydpi="7.6799998">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.0547013"
inkscape:cx="55.816079"
inkscape:cy="84.726326"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-244.08278)">
<rect
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86666667;stroke:none;stroke-width:1.46508551;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4712"
width="52.211964"
height="51.512306"
x="-0.85796964"
y="245.32475"
transform="matrix(0.99980416,-0.01978974,0.00448328,0.99998995,0,0)" />
<g
id="g4581"
style="fill:#080808;fill-opacity:1;stroke-width:0.68901283"
transform="matrix(1.2119871,0,0,1.7379906,-82.577875,-167.18505)">
<rect
transform="rotate(-0.94645665)"
y="239.28041"
x="65.156158"
height="27.75024"
width="41.471352"
id="rect4524"
style="opacity:1;fill:#080808;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<rect
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4518"
width="10.338528"
height="23.042738"
x="8.726779"
y="258.22861"
transform="matrix(0.99995865,0.00909414,-0.00926779,0.99995705,0,0)" />
<ellipse
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4568"
cx="44.118061"
cy="261.20392"
rx="2.4216392"
ry="2.3988426" />
<ellipse
cy="272.90894"
cx="44.765343"
id="circle4570"
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
rx="2.4216392"
ry="2.3988426" />
<g
id="g1705"
transform="translate(0,-2.116672)">
<rect
transform="matrix(0.99967585,0.02545985,-0.02594573,0.99966335,0,0)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="10.338468"
height="23.042864"
x="23.424755"
y="259.99872" />
</g>
<rect
transform="matrix(0.99837418,-0.05699994,0.05808481,0.99831165,0,0)"
y="259.69229"
x="10.517879"
height="23.043449"
width="10.33821"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.03992438px;line-height:125%;font-family:'Liberation Serif';-inkscape-font-specification:'Liberation Serif';letter-spacing:0px;word-spacing:0px;fill:#e6e6e6;fill-opacity:1;stroke:none;stroke-width:0.264584px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="16.979799"
y="286.34747"
id="text1730"
transform="rotate(-1.2296789)"><tspan
sodipodi:role="line"
id="tspan1728"
x="16.979799"
y="286.34747"
style="fill:#e6e6e6;fill-opacity:1;stroke-width:0.264584px">dizqueTV</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,70 @@
const SLACK = require('./constants').SLACK;
let cache = {};
let programPlayTimeCache = {};
let configCache = {};
function getChannelConfig(db, channelId) {
let fillerPlayTimeCache = {};
let configCache = {};
let numbers = null;
async function getChannelConfig(channelDB, channelId) {
//with lazy-loading
if ( typeof(configCache[channelId]) === 'undefined') {
let channel = db['channels'].find( { number: channelId } )
configCache[channelId] = channel;
return channel;
} else {
return configCache[channelId];
let channel = await channelDB.getChannel(channelId)
if (channel == null) {
configCache[channelId] = [];
} else {
configCache[channelId] = [channel];
}
}
return configCache[channelId];
}
async function getAllNumbers(channelDB) {
if (numbers === null) {
let n = await channelDB.getAllChannelNumbers();
numbers = n;
}
return numbers;
}
async function getAllChannels(channelDB) {
let channelNumbers = await getAllNumbers(channelDB);
return (await Promise.all( channelNumbers.map( async (x) => {
return (await getChannelConfig(channelDB, x))[0];
}) )).filter( (channel) => {
if (channel == null) {
console.error("Found a null channel " + JSON.stringify(channelNumbers) );
return false;
}
if ( typeof(channel) === "undefined") {
console.error("Found a undefined channel " + JSON.stringify(channelNumbers) );
return false;
}
if ( typeof(channel.number) === "undefined") {
console.error("Found a channel without number " + JSON.stringify(channelNumbers) );
return false;
}
return true;
} );
}
function saveChannelConfig(number, channel ) {
configCache[number] = [channel];
// flush the item played cache for the channel and any channel in its
// redirect chain
if (typeof(cache[number]) !== 'undefined') {
let lineupItem = cache[number].lineupItem;
for (let i = 0; i < lineupItem.redirectChannels.length; i++) {
delete cache[ lineupItem.redirectChannels[i].number ];
}
delete cache[number];
}
numbers = null;
}
function getCurrentLineupItem(channelId, t1) {
@ -27,10 +74,21 @@ function getCurrentLineupItem(channelId, t1) {
let recorded = cache[channelId];
let lineupItem = JSON.parse( JSON.stringify(recorded.lineupItem) );
let diff = t1 - recorded.t0;
if (diff <= SLACK) {
let rem = lineupItem.duration - lineupItem.start;
if (typeof(lineupItem.streamDuration) !== 'undefined') {
rem = Math.min(rem, lineupItem.streamDuration);
}
if ( (diff <= SLACK) && (diff + SLACK < rem) ) {
//closed the stream and opened it again let's not lose seconds for
//no reason
return lineupItem;
let originalT0 = recorded.lineupItem.originalT0;
if (typeof(originalT0) === 'undefined') {
originalT0 = recorded.t0;
}
if (t1 - originalT0 <= SLACK) {
lineupItem.originalT0 = originalT0;
return lineupItem;
}
}
lineupItem.start += diff;
@ -40,40 +98,58 @@ function getCurrentLineupItem(channelId, t1) {
return null;
}
}
if(lineupItem.start + SLACK > lineupItem.actualDuration) {
if(lineupItem.start + SLACK > lineupItem.duration) {
return null;
}
return lineupItem;
}
function getKey(channelId, program) {
function getProgramKey(program) {
let serverKey = "!unknown!";
if (typeof(program.server) !== 'undefined') {
if (typeof(program.server.name) !== 'undefined') {
serverKey = "plex|" + program.server.name;
if (typeof(program.serverKey) !== 'undefined') {
if (typeof(program.serverKey) !== 'undefined') {
serverKey = "plex|" + program.serverKey;
}
}
let programKey = "!unknownProgram!";
if (typeof(program.key) !== 'undefined') {
programKey = program.key;
}
return channelId + "|" + serverKey + "|" + programKey;
return serverKey + "|" + programKey;
}
function recordProgramPlayTime(channelId, lineupItem, t0) {
function getFillerKey(channelId, fillerId) {
return channelId + "|" + fillerId;
}
function recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0) {
let remaining;
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
remaining = lineupItem.streamDuration;
} else {
remaining = lineupItem.actualDuration - lineupItem.start;
remaining = lineupItem.duration - lineupItem.start;
}
setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t0 + remaining);
if (typeof(lineupItem.fillerId) !== 'undefined') {
fillerPlayTimeCache[ getFillerKey(channelId, lineupItem.fillerId) ] = t0 + remaining;
}
programPlayTimeCache[ getKey(channelId, lineupItem) ] = t0 + remaining;
}
function getProgramLastPlayTime(channelId, program) {
let v = programPlayTimeCache[ getKey(channelId, program) ];
function setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t) {
let programKey = getProgramKey(lineupItem);
programPlayTime.update(channelId, programKey, t);
}
function getProgramLastPlayTime(programPlayTime, channelId, program) {
let programKey = getProgramKey(program);
return programPlayTime.getProgramLastPlayTime(channelId, programKey);
}
function getFillerLastPlayTime(channelId, fillerId) {
let v = fillerPlayTimeCache[ getFillerKey(channelId, fillerId) ];
if (typeof(v) === 'undefined') {
return 0;
} else {
@ -81,8 +157,8 @@ function getProgramLastPlayTime(channelId, program) {
}
}
function recordPlayback(channelId, t0, lineupItem) {
recordProgramPlayTime(channelId, lineupItem, t0);
function recordPlayback(programPlayTime, channelId, t0, lineupItem) {
recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0);
cache[channelId] = {
t0: t0,
@ -90,10 +166,15 @@ function recordPlayback(channelId, t0, lineupItem) {
}
}
function clearPlayback(channelId) {
delete cache[channelId];
}
function clear() {
//it's not necessary to clear the playback cache and it may be undesirable
configCache = {};
cache = {};
numbers = null;
}
module.exports = {
@ -101,6 +182,10 @@ module.exports = {
recordPlayback: recordPlayback,
clear: clear,
getProgramLastPlayTime: getProgramLastPlayTime,
getAllChannels: getAllChannels,
getAllNumbers: getAllNumbers,
getChannelConfig: getChannelConfig,
saveChannelConfig: saveChannelConfig,
}
getFillerLastPlayTime: getFillerLastPlayTime,
clearPlayback: clearPlayback,
}

View File

@ -1,4 +1,39 @@
module.exports = {
SLACK: 9999,
VERSION_NAME: "0.0.54-testing"
}
TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000,
DEFAULT_GUIDE_STEALTH_DURATION: 5 * 60* 1000,
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
TOO_FREQUENT: 1000,
// Duration of things like the loading screen and the interlude (the black
// frame that appears between videos). The goal of these things is to
// prevent the video from getting stuck on the last second, which looks bad
// for some reason ~750 works well. I raised the fps to 60 and now 420 works
// but I wish it was lower.
GAP_DURATION: 10*42,
//when a channel is forcibly stopped due to an update, let's mark it as active
// for a while during the transaction just in case.
CHANNEL_STOP_SHIELD : 5000,
START_CHANNEL_GRACE_PERIOD: 15 * 1000,
// if a channel is stopped while something is playing, subtract
// this amount of milliseconds from the last-played timestamp, because
// video playback has latency and also because maybe the user wants
// the last 30 seconds to remember what was going on...
FORGETFULNESS_BUFFER: 30 * 1000,
// When a channel stops playing, this is a grace period before the channel is
// considered offline. It could be that the client halted the playback for some
// reason and is about to start playing again. Or maybe the user switched
// devices or something. Otherwise we would have on-demand channels constantly
// reseting on their own.
MAX_CHANNEL_IDLE: 60*1000,
// there's a timer that checks all active channels to see if they really are
// staying active, it checks every 5 seconds
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
VERSION_NAME: "1.5.5"
}

124
src/dao/channel-db.js Normal file
View File

@ -0,0 +1,124 @@
const path = require('path');
var fs = require('fs');
class ChannelDB {
constructor(folder) {
this.folder = folder;
}
async getChannel(number) {
let f = path.join(this.folder, `${number}.json` );
try {
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
resolve( JSON.parse(data) )
} catch (err) {
reject(err);
}
})
});
} catch (err) {
console.error(err);
return null;
}
}
async saveChannel(number, json) {
await this.validateChannelJson(number, json);
let f = path.join(this.folder, `${json.number}.json` );
return await new Promise( (resolve, reject) => {
let data = undefined;
try {
data = JSON.stringify(json);
} catch (err) {
return reject(err);
}
fs.writeFile(f, data, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
saveChannelSync(number, json) {
this.validateChannelJson(number, json);
let data = JSON.stringify(json);
let f = path.join(this.folder, `${json.number}.json` );
fs.writeFileSync( f, data );
}
validateChannelJson(number, json) {
json.number = number;
if (typeof(json.number) === 'undefined') {
throw Error("Expected a channel.number");
}
if (typeof(json.number) === 'string') {
try {
json.number = parseInt(json.number);
} catch (err) {
console.error("Error parsing channel number.", err);
}
}
if ( isNaN(json.number)) {
throw Error("channel.number must be a integer");
}
}
async deleteChannel(number) {
let f = path.join(this.folder, `${number}.json` );
await new Promise( (resolve, reject) => {
fs.unlink(f, function (err) {
if (err) {
return reject(err);
}
resolve();
});
});
}
async getAllChannelNumbers() {
return await new Promise( (resolve, reject) => {
fs.readdir(this.folder, function(err, items) {
if (err) {
return reject(err);
}
let channelNumbers = [];
for (let i = 0; i < items.length; i++) {
let name = path.basename( items[i] );
if (path.extname(name) === '.json') {
let numberStr = name.slice(0, -5);
if (!isNaN(numberStr)) {
channelNumbers.push( parseInt(numberStr) );
}
}
}
resolve (channelNumbers);
});
});
}
async getAllChannels() {
let numbers = await this.getAllChannelNumbers();
return await Promise.all( numbers.map( async (c) => this.getChannel(c) ) );
}
}
module.exports = ChannelDB;

131
src/dao/custom-show-db.js Normal file
View File

@ -0,0 +1,131 @@
const path = require('path');
const { v4: uuidv4 } = require('uuid');
let fs = require('fs');
class CustomShowDB {
constructor(folder) {
this.folder = folder;
}
async $loadShow(id) {
let f = path.join(this.folder, `${id}.json` );
try {
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
let j = JSON.parse(data);
j.id = id;
resolve(j);
} catch (err) {
reject(err);
}
})
});
} catch (err) {
console.error(err);
return null;
}
}
async getShow(id) {
return await this.$loadShow(id);
}
async saveShow(id, json) {
if (typeof(id) === 'undefined') {
throw Error("Mising custom show id");
}
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
let data = undefined;
try {
//id is determined by the file name, not the contents
fixup(json);
delete json.id;
data = JSON.stringify(json);
} catch (err) {
return reject(err);
}
fs.writeFile(f, data, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
async createShow(json) {
let id = uuidv4();
fixup(json);
await this.saveShow(id, json);
return id;
}
async deleteShow(id) {
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
fs.unlink(f, function (err) {
if (err) {
return reject(err);
}
resolve();
});
});
}
async getAllShowIds() {
return await new Promise( (resolve, reject) => {
fs.readdir(this.folder, function(err, items) {
if (err) {
return reject(err);
}
let fillerIds = [];
for (let i = 0; i < items.length; i++) {
let name = path.basename( items[i] );
if (path.extname(name) === '.json') {
let id = name.slice(0, -5);
fillerIds.push(id);
}
}
resolve (fillerIds);
});
});
}
async getAllShows() {
let ids = await this.getAllShowIds();
return await Promise.all( ids.map( async (c) => this.getShow(c) ) );
}
async getAllShowsInfo() {
//returns just name and id
let shows = await this.getAllShows();
return shows.map( (f) => {
return {
'id' : f.id,
'name': f.name,
'count': f.content.length,
}
} );
}
}
function fixup(json) {
if (typeof(json.content) === 'undefined') {
json.content = [];
}
if (typeof(json.name) === 'undefined') {
json.name = "Unnamed Show";
}
}
module.exports = CustomShowDB;

202
src/dao/filler-db.js Normal file
View File

@ -0,0 +1,202 @@
const path = require('path');
const { v4: uuidv4 } = require('uuid');
let fs = require('fs');
class FillerDB {
constructor(folder, channelService) {
this.folder = folder;
this.cache = {};
this.channelService = channelService;
}
async $loadFiller(id) {
let f = path.join(this.folder, `${id}.json` );
try {
return await new Promise( (resolve, reject) => {
fs.readFile(f, (err, data) => {
if (err) {
return reject(err);
}
try {
let j = JSON.parse(data);
j.id = id;
resolve(j);
} catch (err) {
reject(err);
}
})
});
} catch (err) {
console.error(err);
return null;
}
}
async getFiller(id) {
if (typeof(this.cache[id]) === 'undefined') {
this.cache[id] = await this.$loadFiller(id);
}
return this.cache[id];
}
async saveFiller(id, json) {
if (typeof(id) === 'undefined') {
throw Error("Mising filler id");
}
let f = path.join(this.folder, `${id}.json` );
try {
await new Promise( (resolve, reject) => {
let data = undefined;
try {
//id is determined by the file name, not the contents
fixup(json);
delete json.id;
data = JSON.stringify(json);
} catch (err) {
return reject(err);
}
fs.writeFile(f, data, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
} finally {
delete this.cache[id];
}
}
async createFiller(json) {
let id = uuidv4();
fixup(json);
await this.saveFiller(id, json);
return id;
}
async getFillerChannels(id) {
let numbers = await this.channelService.getAllChannelNumbers();
let channels = [];
await Promise.all( numbers.map( async(number) => {
let ch = await this.channelService.getChannel(number);
let name = ch.name;
let fillerCollections = ch.fillerCollections;
for (let i = 0 ; i < fillerCollections.length; i++) {
if (fillerCollections[i].id === id) {
channels.push( {
number: number,
name : name,
} );
break;
}
}
ch = null;
} ) );
return channels;
}
async deleteFiller(id) {
try {
let channels = await this.getFillerChannels(id);
await Promise.all( channels.map( async(channel) => {
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
let json = await channelService.getChannel(channel.number);
json.fillerCollections = json.fillerCollections.filter( (col) => {
return col.id != id;
} );
await this.channelService.saveChannel( channel.number, json );
} ) );
let f = path.join(this.folder, `${id}.json` );
await new Promise( (resolve, reject) => {
fs.unlink(f, function (err) {
if (err) {
return reject(err);
}
resolve();
});
});
} finally {
delete this.cache[id];
}
}
async getAllFillerIds() {
return await new Promise( (resolve, reject) => {
fs.readdir(this.folder, function(err, items) {
if (err) {
return reject(err);
}
let fillerIds = [];
for (let i = 0; i < items.length; i++) {
let name = path.basename( items[i] );
if (path.extname(name) === '.json') {
let id = name.slice(0, -5);
fillerIds.push(id);
}
}
resolve (fillerIds);
});
});
}
async getAllFillers() {
let ids = await this.getAllFillerIds();
return await Promise.all( ids.map( async (c) => this.getFiller(c) ) );
}
async getAllFillersInfo() {
//returns just name and id
let fillers = await this.getAllFillers();
return fillers.map( (f) => {
return {
'id' : f.id,
'name': f.name,
'count': f.content.length,
}
} );
}
async getFillersFromChannel(channel) {
let f = [];
if (typeof(channel.fillerCollections) !== 'undefined') {
f = channel.fillerContent;
}
let loadChannelFiller = async(fillerEntry) => {
let content = [];
try {
let filler = await this.getFiller(fillerEntry.id);
content = filler.content;
} catch(e) {
console.error(`Channel #${channel.number} - ${channel.name} references an unattainable filler id: ${fillerEntry.id}`);
}
return {
id: fillerEntry.id,
content: content,
weight: fillerEntry.weight,
cooldown: fillerEntry.cooldown,
}
};
return await Promise.all(
channel.fillerCollections.map(loadChannelFiller)
);
}
}
function fixup(json) {
if (typeof(json.content) === 'undefined') {
json.content = [];
}
if (typeof(json.name) === 'undefined') {
json.name = "Unnamed Filler";
}
}
module.exports = FillerDB;

248
src/dao/plex-server-db.js Normal file
View File

@ -0,0 +1,248 @@
//hmnn this is more of a "PlexServerService"...
const ICON_REGEX = /https?:\/\/.*(\/library\/metadata\/\d+\/thumb\/\d+).X-Plex-Token=.*/;
const ICON_FIELDS = ["icon", "showIcon", "seasonIcon", "episodeIcon"];
// DB is a misnomer here, this is closer to a service
class PlexServerDB
{
constructor(channelService, fillerDB, showDB, db) {
this.channelService = channelService;
this.db = db;
this.fillerDB = fillerDB;
this.showDB = showDB;
}
async fixupAllChannels(name, newServer) {
let channelNumbers = await this.channelService.getAllChannelNumbers();
let report = await Promise.all( channelNumbers.map( async (i) => {
let channel = await this.channelService.getChannel(i);
let channelReport = {
channelNumber : channel.number,
channelName : channel.name,
destroyedPrograms: 0,
modifiedPrograms: 0,
};
this.fixupProgramArray(channel.programs, name,newServer, channelReport);
//if fallback became offline, remove it
if (
(typeof(channel.fallback) !=='undefined')
&& (channel.fallback.length > 0)
&& (channel.fallback[0].isOffline)
) {
channel.fallback = [];
if (channel.offlineMode != "pic") {
channel.offlineMode = "pic";
channel.offlinePicture = `http://localhost:${process.env.PORT}/images/generic-offline-screen.png`;
}
}
this.fixupProgramArray(channel.fallback, name,newServer, channelReport);
await this.channelService.saveChannel(i, channel);
return channelReport;
}) );
return report;
}
async fixupAllFillers(name, newServer) {
let fillers = await this.fillerDB.getAllFillers();
let report = await Promise.all( fillers.map( async (filler) => {
let fillerReport = {
channelNumber : "--",
channelName : filler.name + " (filler)",
destroyedPrograms: 0,
modifiedPrograms: 0,
};
this.fixupProgramArray( filler.content, name,newServer, fillerReport );
filler.content = this.removeOffline(filler.content);
await this.fillerDB.saveFiller( filler.id, filler );
return fillerReport;
} ) );
return report;
}
async fixupAllShows(name, newServer) {
let shows = await this.showDB.getAllShows();
let report = await Promise.all( shows.map( async (show) => {
let showReport = {
channelNumber : "--",
channelName : show.name + " (custom show)",
destroyedPrograms: 0,
modifiedPrograms: 0,
};
this.fixupProgramArray( show.content, name,newServer, showReport );
show.content = this.removeOffline(show.content);
await this.showDB.saveShow( show.id, show );
return showReport;
} ) );
return report;
}
removeOffline( progs ) {
if (typeof(progs) === 'undefined') {
return progs;
}
return progs.filter(
(p) => {
return (true !== p.isOffline);
}
);
}
async fixupEveryProgramHolders(serverName, newServer) {
let reports = await Promise.all( [
this.fixupAllChannels( serverName, newServer ),
this.fixupAllFillers(serverName, newServer),
this.fixupAllShows(serverName, newServer),
] );
let report = [];
reports.forEach(
(r) => r.forEach( (r2) => {
report.push(r2)
} )
);
return report;
}
async deleteServer(name) {
let report = await this.fixupEveryProgramHolders(name, null);
this.db['plex-servers'].remove( { name: name } );
return report;
}
doesNameExist(name) {
return this.db['plex-servers'].find( { name: name} ).length > 0;
}
async updateServer(server) {
let name = server.name;
if (typeof(name) === 'undefined') {
throw Error("Missing server name from request");
}
let s = this.db['plex-servers'].find( { name: name} );
if (s.length != 1) {
throw Error("Server doesn't exist.");
}
s = s[0];
let arGuide = server.arGuide;
if (typeof(arGuide) === 'undefined') {
arGuide = false;
}
let arChannels = server.arChannels;
if (typeof(arChannels) === 'undefined') {
arChannels = false;
}
let newServer = {
name: s.name,
uri: server.uri,
accessToken: server.accessToken,
arGuide: arGuide,
arChannels: arChannels,
index: s.index,
}
this.normalizeServer(newServer);
let report = await this.fixupEveryProgramHolders(name, newServer);
this.db['plex-servers'].update(
{ _id: s._id },
newServer
);
return report;
}
async addServer(server) {
let name = server.name;
if (typeof(name) === 'undefined') {
name = "plex";
}
let i = 2;
let prefix = name;
let resultName = name;
while (this.doesNameExist(resultName)) {
resultName = `${prefix}${i}` ;
i += 1;
}
name = resultName;
let arGuide = server.arGuide;
if (typeof(arGuide) === 'undefined') {
arGuide = false;
}
let arChannels = server.arGuide;
if (typeof(arChannels) === 'undefined') {
arChannels = false;
}
let index = this.db['plex-servers'].find({}).length;
let newServer = {
name: name,
uri: server.uri,
accessToken: server.accessToken,
arGuide: arGuide,
arChannels: arChannels,
index: index,
};
this.normalizeServer(newServer);
this.db['plex-servers'].save(newServer);
}
fixupProgramArray(arr, serverName,newServer, channelReport) {
if (typeof(arr) !== 'undefined') {
for(let i = 0; i < arr.length; i++) {
arr[i] = this.fixupProgram( arr[i], serverName,newServer, channelReport );
}
}
}
fixupProgram(program, serverName,newServer, channelReport) {
if ( (program.serverKey === serverName) && (newServer == null) ) {
channelReport.destroyedPrograms += 1;
return {
isOffline: true,
duration: program.duration,
}
} else if (program.serverKey === serverName) {
let modified = false;
ICON_FIELDS.forEach( (field) => {
if (
(typeof(program[field] ) === 'string')
&&
program[field].includes("/library/metadata")
&&
program[field].includes("X-Plex-Token")
) {
let m = program[field].match(ICON_REGEX);
if (m.length == 2) {
let lib = m[1];
let newUri = `${newServer.uri}${lib}?X-Plex-Token=${newServer.accessToken}`
program[field] = newUri;
modified = true;
}
}
} );
if (modified) {
channelReport.modifiedPrograms += 1;
}
}
return program;
}
normalizeServer(server) {
while (server.uri.endsWith("/")) {
server.uri = server.uri.slice(0,-1);
}
}
}
module.exports = PlexServerDB

View File

@ -0,0 +1,90 @@
const path = require('path');
var fs = require('fs');
class ProgramPlayTimeDB {
constructor(dir) {
this.dir = dir;
this.programPlayTimeCache = {};
}
async load() {
try {
if (! (await fs.promises.stat(this.dir)).isDirectory()) {
return;
}
} catch (err) {
return;
}
let files = await fs.promises.readdir(this.dir);
let processSubFileName = async (fileName, subDir, subFileName) => {
try {
if (subFileName.endsWith(".json")) {
let programKey64 = subFileName.substring(
0,
subFileName.length - 4
);
let programKey = Buffer.from(programKey64, 'base64')
.toString('utf-8');
let filePath = path.join(subDir, subFileName);
let fileContent = await fs.promises.readFile(
filePath, 'utf-8');
let jsonData = JSON.parse(fileContent);
let key = getKey(fileName, programKey);
this.programPlayTimeCache[ key ] = jsonData["t"]
}
} catch (err) {
console.log(`When processing ${subDir}/${subFileName}`, err);
}
}
let processFileName = async(fileName) => {
try {
const subDir = path.join(this.dir, fileName);
let subFiles = await fs.promises.readdir( subDir );
await Promise.all( subFiles.map( async subFileName => {
return processSubFileName(fileName, subDir, subFileName);
}) );
} catch (err) {
console.log(`When processing ${subDir}`, err);
}
}
await Promise.all( files.map(processFileName) );
}
getProgramLastPlayTime(channelId, programKey) {
let v = this.programPlayTimeCache[ getKey(channelId, programKey) ];
if (typeof(v) === 'undefined') {
v = 0;
}
return v;
}
async update(channelId, programKey, t) {
let key = getKey(channelId, programKey);
this.programPlayTimeCache[ key ] = t;
const channelDir = path.join(this.dir, `${channelId}`);
await fs.promises.mkdir( channelDir, { recursive: true } );
let key64 = Buffer.from(programKey, 'utf-8').toString('base64');
let filepath = path.join(channelDir, `${key64}.json`);
let data = {t:t};
await fs.promises.writeFile(filepath, JSON.stringify(data), 'utf-8');
}
}
function getKey(channelId, programKey) {
return channelId + "|" + programKey;
}
module.exports = ProgramPlayTimeDB;

903
src/database-migration.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
function ffmpeg() {
return {
//a record of the config version will help migrating between versions
// in the future. Always increase the version when new ffmpeg configs
// are added.
//
// configVersion 3: First versioned config.
//
configVersion: 4,
ffmpegPath: "/usr/bin/ffmpeg",
threads: 4,
concatMuxDelay: "0",
logFfmpeg: false,
enableFFMPEGTranscoding: false,
audioVolumePercent: 100,
videoEncoder: "mpeg2video",
audioEncoder: "ac3",
targetResolution: "1920x1080",
videoBitrate: 10000,
videoBufSize: 2000,
audioBitrate: 192,
audioBufSize: 50,
audioSampleRate: 48,
audioChannels: 2,
errorScreen: "pic",
errorAudio: "silent",
normalizeVideoCodec: false,
normalizeAudioCodec: false,
normalizeResolution: false,
normalizeAudio: false,
}
}
function repairFFmpeg(existingConfigs) {
var hasBeenRepaired = false;
var currentConfig = {};
var _id = null;
if (existingConfigs.length === 0) {
currentConfig = {};
} else {
currentConfig = existingConfigs[0];
_id = currentConfig._id;
}
if (
(typeof(currentConfig.configVersion) === 'undefined')
|| (currentConfig.configVersion < 3)
) {
hasBeenRepaired = true;
currentConfig = ffmpeg();
currentConfig._id = _id;
}
if (currentConfig.configVersion == 3) {
//migrate from version 3 to 4
hasBeenRepaired = true;
//new settings:
currentConfig.audioBitrate = 192;
currentConfig.audioBufSize = 50;
currentConfig.audioChannels = 2;
currentConfig.audioSampleRate = 48;
//this one has been renamed:
currentConfig.normalizeAudio = currentConfig.alignAudio;
currentConfig.configVersion = 4;
}
return {
hasBeenRepaired: hasBeenRepaired,
fixedConfig : currentConfig,
};
}
module.exports = {
ffmpeg: ffmpeg,
repairFFmpeg: repairFFmpeg,
}

31
src/ffmpeg-info.js Normal file
View File

@ -0,0 +1,31 @@
const exec = require('child_process').exec;
class FFMPEGInfo {
constructor(opts) {
this.ffmpegPath = opts.ffmpegPath
}
async getVersion() {
try {
let s = await new Promise( (resolve, reject) => {
exec( `"${this.ffmpegPath}" -version`, function(error, stdout, stderr){
if (error !== null) {
reject(error);
} else {
resolve(stdout);
}
});
});
var m = s.match( /version\s+([^\s]+)\s+.*Copyright/ )
if (m == null) {
console.error("ffmpeg -version command output not in the expected format: " + s);
return "Unknown";
}
return m[1];
} catch (err) {
console.error("Error getting ffmpeg version", err);
return "Error";
}
}
}
module.exports = FFMPEGInfo

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ const SSDP = require('node-ssdp').Server
module.exports = hdhr
function hdhr(db) {
function hdhr(db, channelDB) {
const server = new SSDP({
location: {
@ -43,14 +43,17 @@ function hdhr(db) {
}
res.send(JSON.stringify(data))
})
router.get('/lineup.json', (req, res) => {
router.get('/lineup.json', async (req, res) => {
res.header("Content-Type", "application/json")
var lineup = []
var channels = db['channels'].find()
for (let i = 0, l = channels.length; i < l; i++)
var channels = await channelDB.getAllChannels();
for (let i = 0, l = channels.length; i < l; i++) {
if (channels[i].stealth !== true) {
lineup.push({ GuideNumber: channels[i].number.toString(), GuideName: channels[i].name, URL: `${req.protocol}://${req.get('host')}/video?channel=${channels[i].number}` })
}
}
if (lineup.length === 0)
lineup.push({ GuideNumber: '1', GuideName: 'PseudoTV', URL: `${req.protocol}://${req.get('host')}/setup` })
lineup.push({ GuideNumber: '1', GuideName: 'dizqueTV', URL: `${req.protocol}://${req.get('host')}/setup` })
res.send(JSON.stringify(lineup))
})
@ -60,14 +63,14 @@ function hdhr(db) {
function getDevice(db, host) {
let hdhrSettings = db['hdhr-settings'].find()[0]
var device = {
FriendlyName: "PseudoTV",
Manufacturer: "PseudoTV - Silicondust",
ManufacturerURL: "https://gitlab.org/DEFENDORe/pseudotv-plex",
FriendlyName: "dizqueTV",
Manufacturer: "dizqueTV - Silicondust",
ManufacturerURL: "https://github.com/vexorian/dizquetv",
ModelNumber: "HDTC-2US",
FirmwareName: "hdhomeruntc_atsc",
TunerCount: hdhrSettings.tunerCount,
FirmwareVersion: "20170930",
DeviceID: 'PseudoTV',
DeviceID: 'dizqueTV',
DeviceAuth: "",
BaseURL: `${host}`,
LineupURL: `${host}/lineup.json`
@ -82,7 +85,7 @@ function getDevice(db, host) {
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>
<friendlyName>PseudoTV</friendlyName>
<friendlyName>dizqueTV</friendlyName>
<manufacturer>Silicondust</manufacturer>
<modelName>HDTC-2US</modelName>
<modelNumber>HDTC-2US</modelNumber>

View File

@ -1,17 +1,47 @@
module.exports = {
getCurrentProgramAndTimeElapsed: getCurrentProgramAndTimeElapsed,
createLineup: createLineup,
isChannelIconEnabled: isChannelIconEnabled,
getWatermark: getWatermark,
generateChannelContext: generateChannelContext,
}
let channelCache = require('./channel-cache');
const INFINITE_TIME = new Date().getTime() + 10*365*24*60*60*1000; //10 years from the initialization of the server. I dunno, I just wanted it to be a high time without it stopping being human readable if converted to date.
const SLACK = require('./constants').SLACK;
const randomJS = require("random-js");
const quickselect = require("quickselect");
const Random = randomJS.Random;
const random = new Random( randomJS.MersenneTwister19937.autoSeed() );
const CHANNEL_CONTEXT_KEYS = [
"disableFillerOverlay",
"watermark",
"icon",
"offlinePicture",
"offlineSoundtrack",
"name",
"transcoding",
"number",
];
module.exports.random = random;
function getCurrentProgramAndTimeElapsed(date, channel) {
let channelStartTime = new Date(channel.startTime)
if (channelStartTime > date)
throw new Error("startTime cannot be set in the future. something fucked up..")
let timeElapsed = (date.valueOf() - channelStartTime.valueOf()) % channel.duration
let channelStartTime = (new Date(channel.startTime)).getTime();
if (channelStartTime > date) {
let t0 = date;
let t1 = channelStartTime;
console.log("Channel start time is above the given date. Flex time is picked till that.");
return {
program: {
isOffline: true,
duration : t1 - t0,
},
timeElapsed: 0,
programIndex: -1,
}
}
let timeElapsed = (date - channelStartTime) % channel.duration
let currentProgramIndex = -1
for (let y = 0, l2 = channel.programs.length; y < l2; y++) {
let program = channel.programs[y]
@ -33,31 +63,47 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
}
function createLineup(obj, channel, isFirst) {
function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
let timeElapsed = obj.timeElapsed
// Start time of a file is never consistent unless 0. Run time of an episode can vary.
// When within 30 seconds of start time, just make the time 0 to smooth things out
// Helps prevents loosing first few seconds of an episode upon lineup change
let activeProgram = obj.program
let beginningOffset = 0;
let lineup = []
if ( typeof(activeProgram.err) !== 'undefined') {
let remaining = activeProgram.duration - timeElapsed;
lineup.push( {
type: 'offline',
title: 'Error',
err: activeProgram.err,
streamDuration: remaining,
duration: remaining,
start: 0,
beginningOffset: beginningOffset,
})
return lineup;
}
if (activeProgram.isOffline === true) {
//offline case
let remaining = activeProgram.actualDuration - timeElapsed;
let remaining = activeProgram.duration - timeElapsed;
//look for a random filler to play
let filler = null;
let special = null;
if (typeof(channel.fillerContent) !== 'undefined') {
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
special = JSON.parse(JSON.stringify(channel.fallback[0]));
}
let randomResult = pickRandomWithMaxDuration(channel, channel.fillerContent, remaining + (isFirst? (24*60*60*1000) : 0) );
let randomResult = pickRandomWithMaxDuration(programPlayTime, channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
filler = randomResult.filler;
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
remaining = randomResult.minimumWait;
}
}
let isSpecial = false;
if (filler == null) {
filler = special;
@ -66,17 +112,17 @@ function createLineup(obj, channel, isFirst) {
if (filler != null) {
let fillerstart = 0;
if (isSpecial) {
if (filler.actualDuration > remaining) {
fillerstart = filler.actualDuration - remaining;
if (filler.duration > remaining) {
fillerstart = filler.duration - remaining;
} else {
ffillerstart = 0;
}
} else if(isFirst) {
fillerstart = Math.max(0, filler.actualDuration - remaining);
fillerstart = Math.max(0, filler.duration - remaining);
//it's boring and odd to tune into a channel and it's always
//the start of a commercial.
let more = Math.max(0, filler.actualDuration - fillerstart - 15000 - SLACK);
fillerstart += Math.floor(more * Math.random() );
let more = Math.max(0, filler.duration - fillerstart - 15000 - SLACK);
fillerstart += random.integer(0, more);
}
lineup.push({ // just add the video, starting at 0, playing the entire duration
type: 'commercial',
@ -86,9 +132,11 @@ function createLineup(obj, channel, isFirst) {
file: filler.file,
ratingKey: filler.ratingKey,
start: fillerstart,
streamDuration: Math.max(1, Math.min(filler.actualDuration - fillerstart, remaining) ),
duration: filler.actualDuration,
server: filler.server
streamDuration: Math.max(1, Math.min(filler.duration - fillerstart, remaining) ),
duration: filler.duration,
fillerId: filler.fillerId,
beginningOffset: beginningOffset,
serverKey: filler.serverKey
});
return lineup;
}
@ -101,146 +149,137 @@ function createLineup(obj, channel, isFirst) {
type: 'offline',
title: 'Channel Offline',
streamDuration: remaining,
beginningOffset: beginningOffset,
duration: remaining,
start: 0
})
return lineup;
}
let originalTimeElapsed = timeElapsed;
if (timeElapsed < 30000) {
timeElapsed = 0
}
beginningOffset = Math.max(0, originalTimeElapsed - timeElapsed);
let programStartTimes = [0, activeProgram.actualDuration * .25, activeProgram.actualDuration * .50, activeProgram.actualDuration * .75, activeProgram.actualDuration]
let commercials = [[], [], [], [], []]
for (let i = 0, l = activeProgram.commercials.length; i < l; i++) // Sort the commercials into their own commerical "slot" array
commercials[activeProgram.commercials[i].commercialPosition].push(activeProgram.commercials[i])
let foundFirstVideo = false
let progTimeElapsed = 0
for (let i = 0, l = commercials.length; i < l; i++) { // Foreach commercial slot
for (let y = 0, l2 = commercials[i].length; y < l2; y++) { // Foreach commercial in that slot
if (!foundFirstVideo && timeElapsed - commercials[i][y].duration < 0) { // If havent already found the starting video AND the this is a the starting video
foundFirstVideo = true // We found the fucker
lineup.push({
type: 'commercial',
title: commercials[i][y].title,
key: commercials[i][y].key,
plexFile: commercials[i][y].plexFile,
file: commercials[i][y].file,
ratingKey: commercials[i][y].ratingKey,
start: timeElapsed, // start time will be the time elapsed, cause this is the first video
streamDuration: commercials[i][y].duration - timeElapsed, // stream duration set accordingly
duration: commercials[i][y].duration,
server: commercials[i][y].server
})
} else if (foundFirstVideo) { // Otherwise, if weve already found the starting video
lineup.push({ // just add the video, starting at 0, playing the entire duration
type: 'commercial',
title: commercials[i][y].title,
key: commercials[i][y].key,
plexFile: commercials[i][y].plexFile,
file: commercials[i][y].file,
ratingKey: commercials[i][y].ratingKey,
start: 0,
streamDuration: commercials[i][y].actualDuration,
duration: commercials[i][y].actualDuration,
server: commercials[i][y].server
})
} else { // Otherwise, this bitch has already been played.. Reduce the time elapsed by its duration
timeElapsed -= commercials[i][y].actualDuration
}
}
if (i < l - 1) { // The last commercial slot is END, so dont write a program..
if (!foundFirstVideo && timeElapsed - (programStartTimes[i + 1] - programStartTimes[i]) < 0) { // same shit as above..
foundFirstVideo = true
lineup.push({
type: 'program',
title: activeProgram.title,
key: activeProgram.key,
plexFile: activeProgram.plexFile,
file: activeProgram.file,
ratingKey: activeProgram.ratingKey,
start: progTimeElapsed + timeElapsed, // add the duration of already played program chunks to the timeElapsed
streamDuration: (programStartTimes[i + 1] - programStartTimes[i]) - timeElapsed,
duration: activeProgram.actualDuration,
server: activeProgram.server
})
} else if (foundFirstVideo) {
if (lineup[lineup.length - 1].type === 'program') { // merge consecutive programs..
lineup[lineup.length - 1].streamDuration += (programStartTimes[i + 1] - programStartTimes[i])
} else {
lineup.push({
return [ {
type: 'program',
title: activeProgram.title,
key: activeProgram.key,
plexFile: activeProgram.plexFile,
file: activeProgram.file,
ratingKey: activeProgram.ratingKey,
start: programStartTimes[i],
streamDuration: (programStartTimes[i + 1] - programStartTimes[i]),
duration: activeProgram.actualDuration,
server: activeProgram.server
})
}
} else {
timeElapsed -= (programStartTimes[i + 1] - programStartTimes[i])
progTimeElapsed += (programStartTimes[i + 1] - programStartTimes[i]) // add the duration of already played program chunks together
}
}
}
return lineup
start: timeElapsed,
streamDuration: activeProgram.duration - timeElapsed,
beginningOffset: beginningOffset,
duration: activeProgram.duration,
serverKey: activeProgram.serverKey
} ];
}
function pickRandomWithMaxDuration(channel, list, maxDuration) {
function weighedPick(a, total) {
return random.bool(a, total);
}
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
let list = [];
for (let i = 0; i < fillers.length; i++) {
list = list.concat(fillers[i].content);
}
let pick1 = null;
let pick2 = null;
let n = 0;
let m = 0;
let t0 = (new Date()).getTime();
let minimumWait = 1000000000;
const D = 24*60*60*1000;
const D = 7*24*60*60*1000;
const E = 5*60*60*1000;
if (typeof(channel.fillerRepeatCooldown) === 'undefined') {
channel.fillerRepeatCooldown = 30*60*1000;
}
for (let i = 0; i < list.length; i++) {
let listM = 0;
let fillerId = undefined;
for (let medianCheck = 1; medianCheck >= 0; medianCheck--) {
for (let j = 0; j < fillers.length; j++) {
list = fillers[j].content;
let pickedList = false;
let n = 0;
let maximumPlayTimeAllowed = INFINITE_TIME;
if (medianCheck==1) {
//calculate the median
let median = getFillerMedian(programPlayTime, channel, fillers[j]);
if (median > 0) {
maximumPlayTimeAllowed = median - 1;
// allow any clip with a play time that's less than the median.
} else {
// initially all times are 0, so if the median is 0, all of those
// are allowed.
maximumPlayTimeAllowed = 0;
}
}
for (let i = 0; i < list.length; i++) {
let clip = list[i];
// a few extra milliseconds won't hurt anyone, would it? dun dun dun
if (clip.actualDuration <= maxDuration + SLACK ) {
let t1 = channelCache.getProgramLastPlayTime( channel.number, clip );
if (clip.duration <= maxDuration + SLACK ) {
let t1 = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip );
if (t1 > maximumPlayTimeAllowed) {
continue;
}
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
let w = channel.fillerRepeatCooldown - timeSince;
if (clip.actualDuration + w <= maxDuration + SLACK) {
if (clip.duration + w <= maxDuration + SLACK) {
minimumWait = Math.min(minimumWait, w);
}
timeSince = 0;
//30 minutes is too little, don't repeat it at all
}
if (timeSince >= D) {
n += 1;
if ( Math.floor(n*Math.random()) == 0) {
pick1 = clip;
}
} else {
let adjust = Math.floor(timeSince / (60*1000));
if (adjust > 0) {
adjust = adjust * adjust;
//weighed
m += adjust;
if ( Math.floor(m*Math.random()) < adjust) {
pick2 = clip;
} else if (!pickedList) {
let t1 = channelCache.getFillerLastPlayTime( channel.number, fillers[j].id );
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
if (timeSince + SLACK >= fillers[j].cooldown) {
//should we pick this list?
listM += fillers[j].weight;
if ( weighedPick(fillers[j].weight, listM) ) {
pickedList = true;
fillerId = fillers[j].id;
n = 0;
} else {
break;
}
} else {
let w = fillers[j].cooldown - timeSince;
if (clip.duration + w <= maxDuration + SLACK) {
minimumWait = Math.min(minimumWait, w);
}
break;
}
}
if (timeSince <= 0) {
continue;
}
let s = norm_s( (timeSince >= E) ? E : timeSince );
let d = norm_d( clip.duration);
let w = s + d;
n += w;
if (weighedPick(w,n)) {
pick1 = clip;
}
}
}
}
if (pick1 != null) {
break;
}
}
let pick = (pick1 == null) ? pick2: pick1;
let pickTitle = "null";
let pick = pick1;
if (pick != null) {
pickTitle = pick.title;
pick = JSON.parse( JSON.stringify(pick) );
pick.fillerId = fillerId;
}
return {
filler: pick,
@ -248,19 +287,93 @@ function pickRandomWithMaxDuration(channel, list, maxDuration) {
}
}
function isChannelIconEnabled( ffmpegSettings, channel, type) {
function norm_d(x) {
x /= 60 * 1000;
if (x >= 3.0) {
x = 3.0 + Math.log(x);
}
let y = 10000 * ( Math.ceil(x * 1000) + 1 );
return Math.ceil(y / 1000000) + 1;
}
function norm_s(x) {
let y = Math.ceil(x / 600) + 1;
y = y*y;
return Math.ceil(y / 1000000) + 1;
}
// any channel thing used here should be added to channel context
function getWatermark( ffmpegSettings, channel, type) {
if (! ffmpegSettings.enableFFMPEGTranscoding || ffmpegSettings.disableChannelOverlay ) {
return false;
return null;
}
let d = channel.disableFillerOverlay;
if (typeof(d) === 'undefined') {
d = true;
}
if ( (typeof type !== `undefined`) && (type == 'commercial') && d ) {
return false;
return null;
}
if (channel.icon === '' || !channel.overlayIcon) {
return false;
let e = false;
let icon = undefined;
let watermark = {};
if (typeof(channel.watermark) !== 'undefined') {
watermark = channel.watermark;
e = (watermark.enabled === true);
icon = watermark.url;
}
return true;
if (! e) {
return null;
}
if ( (typeof(icon) === 'undefined') || (icon === '') ) {
icon = channel.icon;
if ( (typeof(icon) === 'undefined') || (icon === '') ) {
return null;
}
}
let result = {
url: icon,
width: watermark.width,
verticalMargin: watermark.verticalMargin,
horizontalMargin: watermark.horizontalMargin,
duration: watermark.duration,
position: watermark.position,
fixedSize: (watermark.fixedSize === true),
animated: (watermark.animated === true),
}
return result;
}
function getFillerMedian(programPlayTime, channel, filler) {
let times = [];
list = filler.content;
for (let i = 0; i < list.length; i++) {
let clip = list[i];
let t = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip);
times.push(t);
}
if (times.length <= 1) {
//if there are too few elements, the protection is not helpful.
return INFINITE_TIME;
}
let m = Math.floor(times.length / 2);
quickselect(times, m)
return times[m];
}
function generateChannelContext(channel) {
let channelContext = {};
for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) {
let key = CHANNEL_CONTEXT_KEYS[i];
if (typeof(channel[key]) !== 'undefined') {
channelContext[key] = JSON.parse( JSON.stringify(channel[key] ) );
}
}
return channelContext;
}

View File

@ -13,36 +13,71 @@ class OfflinePlayer {
constructor(error, context) {
this.context = context;
this.error = error;
if (context.isLoading === true) {
context.channel = JSON.parse( JSON.stringify(context.channel) );
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`;
context.channel.offlineSoundtrack = undefined;
}
if (context.isInterlude === true) {
context.channel = JSON.parse( JSON.stringify(context.channel) );
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/black.png`;
context.channel.offlineSoundtrack = undefined;
}
this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel);
this.ffmpeg.setAudioOnly(this.context.audioOnly);
}
cleanUp() {
this.ffmpeg.kill();
}
async play() {
async play(outStream) {
try {
let emitter = new EventEmitter();
let ffmpeg = this.ffmpeg;
let lineupItem = this.context.lineupItem;
let duration = lineupItem.streamDuration - lineupItem.start;
let ff;
if (this.error) {
ffmpeg.spawnError(duration);
ff = await ffmpeg.spawnError(duration);
} else {
ffmpeg.spawnOffline(duration);
ff = await ffmpeg.spawnOffline(duration);
}
ff.pipe(outStream, {'end':false} );
ffmpeg.on('data', (data) => {
emitter.emit('data', data);
});
ffmpeg.on('end', () => {
emitter.emit('end');
});
ffmpeg.on('close', () => {
emitter.emit('close');
});
ffmpeg.on('error', (err) => {
emitter.emit('error', err);
ffmpeg.on('error', async (err) => {
//wish this code wasn't repeated.
if (! this.error ) {
console.log("Replacing failed stream with error stream");
ff.unpipe(outStream);
ffmpeg.removeAllListeners('data');
ffmpeg.removeAllListeners('end');
ffmpeg.removeAllListeners('error');
ffmpeg.removeAllListeners('close');
ffmpeg = new FFMPEG(this.context.ffmpegSettings, this.context.channel); // Set the transcoder options
ffmpeg.setAudioOnly(this.context.audioOnly);
ffmpeg.on('close', () => {
emitter.emit('close');
});
ffmpeg.on('end', () => {
emitter.emit('end');
});
ffmpeg.on('error', (err) => {
emitter.emit('error', err );
});
ff = await ffmpeg.spawnError('oops', 'oops', Math.min(duration, 60000) );
ff.pipe(outStream);
} else {
emitter.emit('error', err);
}
});
return emitter;
} catch(err) {

View File

@ -9,6 +9,9 @@ const PlexTranscoder = require('./plexTranscoder')
const EventEmitter = require('events');
const helperFuncs = require('./helperFuncs')
const FFMPEG = require('./ffmpeg')
const constants = require('./constants');
let USED_CLIENTS = {};
class PlexPlayer {
@ -17,9 +20,17 @@ class PlexPlayer {
this.ffmpeg = null;
this.plexTranscoder = null;
this.killed = false;
let coreClientId = this.context.db['client-id'].find()[0].clientId;
let i = 0;
while ( USED_CLIENTS[coreClientId+"-"+i]===true) {
i++;
}
this.clientId = coreClientId+"-"+i;
USED_CLIENTS[this.clientId] = true;
}
cleanUp() {
USED_CLIENTS[this.clientId] = false;
this.killed = true;
if (this.plexTranscoder != null) {
this.plexTranscoder.stopUpdatingPlex();
@ -31,22 +42,33 @@ class PlexPlayer {
}
}
async play() {
async play(outStream) {
let lineupItem = this.context.lineupItem;
let ffmpegSettings = this.context.ffmpegSettings;
let db = this.context.db;
let channel = this.context.channel;
let server = db['plex-servers'].find( { 'name': lineupItem.serverKey } );
if (server.length == 0) {
throw Error(`Unable to find server "${lineupItem.serverKey}" specified by program.`);
}
server = server[0];
if (server.uri.endsWith("/")) {
server.uri = server.uri.slice(0, server.uri.length - 1);
}
try {
let plexSettings = db['plex-settings'].find()[0];
let plexTranscoder = new PlexTranscoder(plexSettings, lineupItem);
let plexTranscoder = new PlexTranscoder(this.clientId, server, plexSettings, channel, lineupItem);
this.plexTranscoder = plexTranscoder;
let enableChannelIcon = this.context.enableChannelIcon;
let watermark = this.context.watermark;
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
ffmpeg.setAudioOnly( this.context.audioOnly );
this.ffmpeg = ffmpeg;
let streamDuration;
if (typeof(streamDuration)!=='undefined') {
streamDuration = lineupItem.streamDuration / 1000;
if (typeof(lineupItem.streamDuration)!=='undefined') {
if (lineupItem.start + lineupItem.streamDuration + constants.SLACK < lineupItem.duration) {
streamDuration = lineupItem.streamDuration / 1000;
}
}
let deinterlace = ffmpegSettings.enableFFMPEGTranscoding; //for now it will always deinterlace when transcoding is enabled but this is sub-optimal
@ -63,30 +85,46 @@ class PlexPlayer {
let emitter = new EventEmitter();
//setTimeout( () => {
ffmpeg.spawnStream(stream.streamUrl, stream.streamStats, streamStart, streamDuration, enableChannelIcon, lineupItem.type); // Spawn the ffmpeg process
let ff = await ffmpeg.spawnStream(stream.streamUrl, stream.streamStats, streamStart, streamDuration, watermark, lineupItem.type); // Spawn the ffmpeg process
ff.pipe(outStream, {'end':false} );
//}, 100);
plexTranscoder.startUpdatingPlex();
ffmpeg.on('data', (data) => {
emitter.emit('data', data);
});
ffmpeg.on('end', () => {
emitter.emit('end');
});
ffmpeg.on('close', () => {
emitter.emit('close');
});
ffmpeg.on('error', (err) => {
ffmpeg.on('error', async (err) => {
console.log("Replacing failed stream with error stream");
ff.unpipe(outStream);
ffmpeg.removeAllListeners('data');
ffmpeg.removeAllListeners('end');
ffmpeg.removeAllListeners('error');
ffmpeg.removeAllListeners('close');
ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
ffmpeg.setAudioOnly(this.context.audioOnly);
ffmpeg.on('close', () => {
emitter.emit('close');
});
ffmpeg.on('end', () => {
emitter.emit('end');
});
ffmpeg.on('error', (err) => {
emitter.emit('error', err );
});
ff = await ffmpeg.spawnError('oops', 'oops', Math.min(streamStats.duration, 60000) );
ff.pipe(outStream);
emitter.emit('error', err);
});
return emitter;
} catch(err) {
if (err instanceof Error) {
throw err;
} else {
return Error("Error when playing plex program: " + JSON.stringify(err) );
}
return Error("Error when playing plex program: " + JSON.stringify(err) );
}
}
}

View File

@ -2,17 +2,24 @@ const request = require('request')
class Plex {
constructor(opts) {
this._accessToken = typeof opts.accessToken !== 'undefined' ? opts.accessToken : ''
let uri = "http://127.0.0.1:32400";
if ( (typeof opts.uri) !== 'undefined' ) {
uri = opts.uri;
if (uri.endsWith("/")) {
uri = uri.slice(0, uri.length - 1);
}
}
this._server = {
uri: typeof opts.uri !== 'undefined' ? opts.uri : 'http://127.0.0.1:32400',
uri: uri,
host: typeof opts.host !== 'undefined' ? opts.host : '127.0.0.1',
port: typeof opts.port !== 'undefined' ? opts.port : '32400',
protocol: typeof opts.protocol !== 'undefined' ? opts.protocol : 'http'
}
this._headers = {
'Accept': 'application/json',
'X-Plex-Device': 'PseudoTV',
'X-Plex-Device-Name': 'PseudoTV',
'X-Plex-Product': 'PseudoTV',
'X-Plex-Device': 'dizqueTV',
'X-Plex-Device-Name': 'dizqueTV',
'X-Plex-Product': 'dizqueTV',
'X-Plex-Version': '0.1',
'X-Plex-Client-Identifier': 'rg14zekk3pa5zp4safjwaa8z',
'X-Plex-Platform': 'Chrome',
@ -48,8 +55,23 @@ class Plex {
})
})
}
Get(path, optionalHeaders = {}) {
var req = {
doRequest(req) {
return new Promise( (resolve, reject) => {
request( req, (err, res) => {
if (err) {
reject(err);
} else if ((res.statusCode < 200) || (res.statusCode >= 300) ) {
reject( Error(`Request returned status code ${res.statusCode}`) );
} else {
resolve(res);
}
});
});
}
async Get(path, optionalHeaders = {}) {
let req = {
method: 'get',
url: `${this.URL}${path}`,
headers: this._headers,
@ -57,19 +79,14 @@ class Plex {
}
Object.assign(req, optionalHeaders)
req.headers['X-Plex-Token'] = this._accessToken
return new Promise((resolve, reject) => {
if (this._accessToken === '')
reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.")
else
request(req, (err, res) => {
if (err || res.statusCode !== 200)
reject(`Plex 'Get' request failed. URL: ${this.URL}${path}`)
else
resolve(JSON.parse(res.body).MediaContainer)
})
})
if (this._accessToken === '') {
throw Error("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.");
} else {
let res = await this.doRequest(req);
return JSON.parse(res.body).MediaContainer;
}
}
Put(path, query = {}, optionalHeaders = {}) {
async Put(path, query = {}, optionalHeaders = {}) {
var req = {
method: 'put',
url: `${this.URL}${path}`,
@ -79,7 +96,7 @@ class Plex {
}
Object.assign(req, optionalHeaders)
req.headers['X-Plex-Token'] = this._accessToken
return new Promise((resolve, reject) => {
await new Promise((resolve, reject) => {
if (this._accessToken === '')
reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.")
else
@ -113,31 +130,52 @@ class Plex {
})
})
}
async checkServerStatus() {
try {
await this.Get('/');
return 1;
} catch (err) {
console.error("Error getting Plex server status", err);
return -1;
}
}
async GetDVRS() {
var result = await this.Get('/livetv/dvrs')
var dvrs = result.Dvr
dvrs = typeof dvrs === 'undefined' ? [] : dvrs
return dvrs
try {
var result = await this.Get('/livetv/dvrs')
var dvrs = result.Dvr
dvrs = typeof dvrs === 'undefined' ? [] : dvrs
return dvrs
} catch (err) {
throw Error( "GET /livetv/drs failed: " + err.message);
}
}
async RefreshGuide(_dvrs) {
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
for (var i = 0; i < dvrs.length; i++)
this.Post(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`).then(() => { }, (err) => { console.log(err) })
try {
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
for (var i = 0; i < dvrs.length; i++) {
await this.Post(`/livetv/dvrs/${dvrs[i].key}/reloadGuide`);
}
} catch (err) {
throw Error("Zort", err);
}
}
async RefreshChannels(channels, _dvrs) {
var dvrs = typeof _dvrs !== 'undefined' ? _dvrs : await this.GetDVRS()
var _channels = []
let qs = {}
for (var i = 0; i < channels.length; i++)
for (var i = 0; i < channels.length; i++) {
_channels.push(channels[i].number)
}
qs.channelsEnabled = _channels.join(',')
for (var i = 0; i < _channels.length; i++) {
qs[`channelMapping[${_channels[i]}]`] = _channels[i]
qs[`channelMappingByKey[${_channels[i]}]`] = _channels[i]
}
for (var i = 0; i < dvrs.length; i++)
for (var y = 0; y < dvrs[i].Device.length; y++)
this.Put(`/media/grabbers/devices/${dvrs[i].Device[y].key}/channelmap`, qs).then(() => { }, (err) => { console.log(err) })
for (var i = 0; i < dvrs.length; i++) {
for (var y = 0; y < dvrs[i].Device.length; y++) {
await this.Put(`/media/grabbers/devices/${dvrs[i].Device[y].key}/channelmap`, qs);
}
}
}
}

View File

@ -1,24 +1,33 @@
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
const fs = require('fs');
class PlexTranscoder {
constructor(settings, lineupItem) {
constructor(clientId, server, settings, channel, lineupItem) {
this.session = uuidv4()
this.device = "channel-" + channel.number;
this.deviceName = this.device;
this.clientIdentifier = clientId;
this.product = "dizqueTV";
this.settings = settings
this.log("Plex transcoder initiated")
this.log("Debug logging enabled")
this.key = lineupItem.key
this.plexFile = `${lineupItem.server.uri}${lineupItem.plexFile}?X-Plex-Token=${lineupItem.server.accessToken}`
this.file = lineupItem.file.replace(settings.pathReplace, settings.pathReplaceWith)
this.transcodeUrlBase = `${lineupItem.server.uri}/video/:/transcode/universal/start.m3u8?`
this.metadataPath = `${server.uri}${lineupItem.key}?X-Plex-Token=${server.accessToken}`
this.plexFile = `${server.uri}${lineupItem.plexFile}?X-Plex-Token=${server.accessToken}`
if (typeof(lineupItem.file)!=='undefined') {
this.file = lineupItem.file.replace(settings.pathReplace, settings.pathReplaceWith)
}
this.transcodeUrlBase = `${server.uri}/video/:/transcode/universal/start.m3u8?`
this.ratingKey = lineupItem.ratingKey
this.currTimeMs = lineupItem.start
this.currTimeS = this.currTimeMs / 1000
this.duration = lineupItem.duration
this.server = lineupItem.server
this.server = server
this.transcodingArgs = undefined
this.decisionJson = undefined
@ -26,6 +35,11 @@ class PlexTranscoder {
this.updateInterval = 30000
this.updatingPlex = undefined
this.playState = "stopped"
this.mediaHasNoVideo = false;
this.albumArt = {
attempted : false,
path: null,
}
}
async getStream(deinterlace) {
@ -35,60 +49,83 @@ class PlexTranscoder {
this.log(` deinterlace: ${deinterlace}`)
this.log(` streamPath: ${this.settings.streamPath}`)
this.setTranscodingArgs(stream.directPlay, true, false, false);
await this.tryToDetectAudioOnly();
if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) {
if (this.settings.enableSubtitles) {
this.log("Direct play is forced, so subtitles are forcibly disabled.");
this.settings.enableSubtitles = false;
}
stream = {directPlay: true}
} else {
try {
this.log("Setting transcoding parameters")
this.setTranscodingArgs(stream.directPlay, true, deinterlace)
this.setTranscodingArgs(stream.directPlay, true, deinterlace, this.mediaHasNoVideo)
await this.getDecision(stream.directPlay);
if (this.isDirectPlay()) {
stream.directPlay = true;
stream.streamUrl = this.plexFile;
}
} catch (err) {
this.log("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.")
console.error("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.", err)
stream.directPlay = true;
}
}
if (stream.directPlay) {
if (stream.directPlay || this.isAV1() ) {
if (! stream.directPlay) {
this.log("Plex doesn't support av1, so we are forcing direct play, including for audio because otherwise plex breaks the stream.")
}
this.log("Direct play forced or native paths enabled")
stream.directPlay = true
this.setTranscodingArgs(stream.directPlay, true, false)
this.setTranscodingArgs(stream.directPlay, true, false, this.mediaHasNoVideo )
// Update transcode decision for session
await this.getDecision(stream.directPlay);
stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
if(this.settings.streamPath === 'direct') {
fs.access(this.file, fs.F_OK, (err) => {
if (err) {
throw Error("Can't access this file", err);
return
}
})
}
if (typeof(stream.streamUrl) == 'undefined') {
throw Error("Direct path playback is not possible for this program because it was registered at a time when the direct path settings were not set. To fix this, you must either revert the direct path setting or rebuild this channel.");
}
} else if (this.isVideoDirectStream() === false) {
this.log("Decision: File can direct play")
this.log("Decision: Should transcode")
// Change transcoding arguments to be the user chosen transcode parameters
this.setTranscodingArgs(stream.directPlay, false, deinterlace)
this.setTranscodingArgs(stream.directPlay, false, deinterlace, this.mediaHasNoVideo)
// Update transcode decision for session
await this.getDecision(stream.directPlay);
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
} else {
//This case sounds complex. Apparently plex is sending us just the audio, so we would need to get the video in a separate stream.
this.log("Decision: Direct stream. Audio is being transcoded")
stream.separateVideoStream = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
this.directInfo = await this.getDirectInfo();
this.videoIsDirect = true;
}
stream.streamStats = this.getVideoStats();
// use correct audio stream if direct play
let audioIndex = await this.getAudioIndex();
stream.streamStats.audioIndex = (stream.directPlay) ? audioIndex : 'a'
stream.streamStats.audioIndex = (stream.directPlay) ? ( await this.getAudioIndex() ) : 'a'
this.log(stream)
return stream
}
setTranscodingArgs(directPlay, directStream, deinterlace) {
setTranscodingArgs(directPlay, directStream, deinterlace, audioOnly) {
let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution
let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate
let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize
let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar
let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing
let isDirectPlay = (directPlay) ? '1' : '0'
let isDirectPlay = (directPlay) ? '1' : '0';
let hasMDE = '1';
let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set
let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra
@ -104,12 +141,17 @@ class PlexTranscoder {
vc = "av1";
}
let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\
let clientProfile ="";
if (! audioOnly ) {
clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=true)+\
add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&BreakNonKeyframes=true)+\
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.width&value=${resolutionArr[0]})+\
add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&value=${resolutionArr[1]})`
} else {
clientProfile=`add-transcode-target(type=musicProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)`
}
// Set transcode settings per audio codec
this.settings.audioCodecs.split(",").forEach(function (codec) {
clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})`
@ -127,14 +169,18 @@ add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&va
let clientProfile_enc=encodeURIComponent(clientProfile)
this.transcodingArgs=`X-Plex-Platform=${profileName}&\
X-Plex-Product=${this.product}&\
X-Plex-Client-Platform=${profileName}&\
X-Plex-Client-Profile-Name=${profileName}&\
X-Plex-Device-Name=${this.deviceName}&\
X-Plex-Device=${this.device}&\
X-Plex-Client-Identifier=${this.clientIdentifier}&\
X-Plex-Platform=${profileName}&\
X-Plex-Token=${this.server.accessToken}&\
X-Plex-Client-Profile-Extra=${clientProfile_enc}&\
protocol=${this.settings.streamProtocol}&\
Connection=keep-alive&\
hasMDE=1&\
hasMDE=${hasMDE}&\
path=${this.key}&\
mediaIndex=0&\
partIndex=0&\
@ -159,16 +205,27 @@ lang=en`
try {
return this.getVideoStats().videoDecision === "copy";
} catch (e) {
console.log("Error at decision:" + e);
console.error("Error at decision:", e);
return false;
}
}
isAV1() {
try {
return this.getVideoStats().videoCodec === 'av1';
} catch (e) {
return false;
}
}
isDirectPlay() {
try {
if (this.getVideoStats().audioOnly) {
return this.getVideoStats().audioDecision === "copy";
}
return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy";
} catch (e) {
console.log("Error at decision:" + e);
console.error("Error at decision:" , e);
return false;
}
}
@ -177,11 +234,26 @@ lang=en`
let ret = {}
try {
let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream
ret.duration = parseFloat( this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].duration );
streams.forEach(function (stream) {
streams.forEach(function (_stream, $index) {
// Video
let stream = _stream;
if (stream["streamType"] == "1") {
if ( this.videoIsDirect === true && typeof(this.directInfo) !== 'undefined') {
stream = this.directInfo.MediaContainer.Metadata[0].Media[0].Part[0].Stream[$index];
}
ret.anamorphic = ( (stream.anamorphic === "1") || (stream.anamorphic === true) );
if (ret.anamorphic) {
let parsed = parsePixelAspectRatio(stream.pixelAspectRatio);
if (isNaN(parsed.p) || isNaN(parsed.q) ) {
throw Error("isNaN");
}
ret.pixelP = parsed.p;
ret.pixelQ = parsed.q;
} else {
ret.pixelP= 1;
ret.pixelQ = 1;
}
ret.videoCodec = stream.codec;
ret.videoWidth = stream.width;
ret.videoHeight = stream.height;
@ -189,6 +261,7 @@ lang=en`
// Rounding framerate avoids scenarios where
// 29.9999999 & 30 don't match.
ret.videoDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision;
ret.videoScanType = stream.scanType;
}
// Audio. Only look at stream being used
if (stream["streamType"] == "2" && stream["selected"] == "1") {
@ -196,9 +269,17 @@ lang=en`
ret.audioCodec = stream["codec"];
ret.audioDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision;
}
})
}.bind(this) )
} catch (e) {
console.log("Error at decision:" + e);
console.error("Error at decision:" , e);
}
if (typeof(ret.videoCodec) === 'undefined') {
ret.audioOnly = true;
ret.placeholderImage = (this.albumArt.path != null) ?
ret.placeholderImage = this.albumArt.path
:
ret.placeholderImage = `http://localhost:${process.env.PORT}/images/generic-music-screen.png`
;
}
this.log("Current video stats:")
@ -225,11 +306,11 @@ lang=en`
}
})
} catch (e) {
console.log("Error at get media info:" + e);
console.error("Error at get media info:" + e);
}
})
.catch((err) => {
console.log(err);
console.error("Error getting audio index",err);
});
this.log(`Found audio index: ${index}`)
@ -237,24 +318,74 @@ lang=en`
return index
}
async getDecision(directPlay) {
await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, {
async getDirectInfo() {
return (await axios.get(this.metadataPath) ).data;
}
async getDecisionUnmanaged(directPlay) {
let url = `${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`;
let res = await axios.get(url, {
headers: { Accept: 'application/json' }
})
.then((res) => {
this.decisionJson = res.data;
this.log("Recieved transcode decision:")
this.log("Received transcode decision:");
this.log(res.data)
// Print error message if transcode not possible
// TODO: handle failure better
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode
if (!(directPlay || transcodeDecisionCode == "1001")) {
console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
if (res.data.MediaContainer.mdeDecisionCode === 1000) {
this.log("mde decision code 1000, so it's all right?");
return;
}
})
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode;
if (
( typeof(transcodeDecisionCode) === 'undefined' )
) {
this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo';
this.log("Strange case, attempt direct play");
} else if (!(directPlay || transcodeDecisionCode == "1001")) {
this.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
this.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
}
}
async tryToDetectAudioOnly() {
try {
this.log("Try to detect audio only:");
let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`;
let res = await axios.get(url, {
headers: { Accept: 'application/json' }
});
let mediaContainer = res.data.MediaContainer;
let metadata = getOneOrUndefined( mediaContainer, "Metadata");
if (typeof(metadata) !== 'undefined') {
this.albumArt.path = `${this.server.uri}${metadata.thumb}?X-Plex-Token=${this.server.accessToken}`;
let media = getOneOrUndefined( metadata, "Media");
if (typeof(media) !== 'undefined') {
if (typeof(media.videoCodec)==='undefined') {
this.log("Audio-only file detected");
this.mediaHasNoVideo = true;
}
}
}
} catch (err) {
console.error("Error when getting album art", err);
}
}
async getDecision(directPlay) {
try {
await this.getDecisionUnmanaged(directPlay);
} catch (err) {
console.error(err);
}
}
getStatusUrl() {
@ -270,12 +401,13 @@ state=${this.playState}&\
key=${this.key}&\
time=${this.currTimeMs}&\
duration=${this.duration}&\
X-Plex-Product=${this.product}&\
X-Plex-Platform=${profileName}&\
X-Plex-Client-Platform=${profileName}&\
X-Plex-Client-Profile-Name=${profileName}&\
X-Plex-Device-Name=PseudoTV-Plex&\
X-Plex-Device=PseudoTV-Plex&\
X-Plex-Client-Identifier=${this.session}&\
X-Plex-Device-Name=${this.deviceName}&\
X-Plex-Device=${this.device}&\
X-Plex-Client-Identifier=${this.clientIdentifier}&\
X-Plex-Platform=${profileName}&\
X-Plex-Token=${this.server.accessToken}`;
@ -299,8 +431,15 @@ X-Plex-Token=${this.server.accessToken}`;
}
updatePlex() {
this.log("Updating plex status")
axios.post(this.getStatusUrl());
this.log("Updating plex status");
const statusUrl = this.getStatusUrl();
try {
axios.post(statusUrl);
} catch (error) {
this.log(`Problem updating Plex status using status URL ${statusUrl}:`);
this.log(error);
return false;
}
this.currTimeMs += this.updateInterval;
if (this.currTimeMs > this.duration) {
this.currTimeMs = this.duration;
@ -315,4 +454,27 @@ X-Plex-Token=${this.server.accessToken}`;
}
}
function parsePixelAspectRatio(s) {
let x = s.split(":");
return {
p: parseInt(x[0], 10),
q: parseInt(x[1], 10),
}
}
function getOneOrUndefined(object, field) {
if (typeof(object) === 'undefined') {
return undefined;
}
if ( typeof(object[field]) === "undefined") {
return undefined;
}
let x = object[field];
if (x.length < 1) {
return undefined;
}
return x[0];
}
module.exports = PlexTranscoder

View File

@ -29,9 +29,25 @@ class ProgramPlayer {
constructor( context ) {
this.context = context;
let program = context.lineupItem;
if (program.err instanceof Error) {
if (context.m3u8) {
context.ffmpegSettings.normalizeAudio = false;
// people might want the codec normalization to stay because of player support
context.ffmpegSettings.normalizeResolution = false;
}
context.ffmpegSettings.noRealTime = program.noRealTime;
if ( typeof(program.err) !== 'undefined') {
console.log("About to play error stream");
this.delegate = new OfflinePlayer(true, context);
} else if (program.type === 'loading') {
console.log("About to play loading stream");
/* loading */
context.isLoading = true;
this.delegate = new OfflinePlayer(false, context);
} else if (program.type === 'interlude') {
console.log("About to play interlude stream");
/* interlude */
context.isInterlude = true;
this.delegate = new OfflinePlayer(false, context);
} else if (program.type === 'offline') {
console.log("About to play offline stream");
/* offline */
@ -41,31 +57,20 @@ class ProgramPlayer {
/* plex */
this.delegate = new PlexPlayer(context);
}
this.context.enableChannelIcon = helperFuncs.isChannelIconEnabled( context.ffmpegSettings, context.channel, context.lineupItem.type);
this.context.watermark = helperFuncs.getWatermark( context.ffmpegSettings, context.channel, context.lineupItem.type);
}
cleanUp() {
this.delegate.cleanUp();
}
async playDelegate() {
async playDelegate(outStream) {
return await new Promise( async (accept, reject) => {
setTimeout( () => {
reject( Error("program player timed out before receiving any data.") );
}, 30000);
try {
let stream = await this.delegate.play();
let first = true;
let stream = await this.delegate.play(outStream);
accept(stream);
let emitter = new EventEmitter();
stream.on("data", (data) => {
if (first) {
accept( {stream: emitter, data: data} );
first = false;
} else {
emitter.emit("data", data);
}
});
function end() {
reject( Error("Stream ended with no data") );
stream.removeAllListeners("data");
@ -85,9 +90,9 @@ class ProgramPlayer {
}
})
}
async play() {
async play(outStream) {
try {
return await this.playDelegate();
return await this.playDelegate(outStream);
} catch(err) {
if (! (err instanceof Error) ) {
err= Error("Program player had an error before receiving any data. " + JSON.stringify(err) );
@ -105,7 +110,7 @@ class ProgramPlayer {
}
this.delegate.cleanUp();
this.delegate = new OfflinePlayer(true, this.context);
return await this.play();
return await this.play(outStream);
}
}
}

View File

@ -0,0 +1,150 @@
const constants = require("../constants");
/* Keeps track of which channels are being played, calls on-demand service
when they stop playing.
*/
class ActiveChannelService
{
/****
*
**/
constructor(onDemandService, channelService ) {
this.cache = {};
this.onDemandService = onDemandService;
this.onDemandService.setActiveChannelService(this);
this.channelService = channelService;
this.timeNoDelta = new Date().getTime();
this.loadChannelsForFirstTry();
this.setupTimer();
}
loadChannelsForFirstTry() {
let fun = async() => {
try {
let numbers = await this.channelService.getAllChannelNumbers();
numbers.forEach( (number) => {
this.ensure(this.timeNoDelta, number);
} );
this.checkChannels();
} catch (err) {
console.error("Unexpected error when checking channels for the first time.", err);
}
}
fun();
}
async shutdown() {
try {
let t = new Date().getTime() - constants.FORGETFULNESS_BUFFER;
for (const [channelNumber, value] of Object.entries(this.cache)) {
console.log("Forcefully registering channel " + channelNumber + " as stopped...");
delete this.cache[ channelNumber ];
await this.onDemandService.registerChannelStopped( channelNumber, t , true);
}
} catch (err) {
console.error("Unexpected error when shutting down active channels service.", err);
}
}
setupTimer() {
this.handle = setTimeout( () => this.timerLoop(), constants.PLAYED_MONITOR_CHECK_FREQUENCY );
}
checkChannel(t, channelNumber, value) {
if (value.active === 0) {
let delta = t - value.lastUpdate;
if ( (delta >= constants.MAX_CHANNEL_IDLE) || (value.lastUpdate <= this.timeNoDelta) ) {
console.log("Channel : " + channelNumber + " is not playing...");
onDemandService.registerChannelStopped(channelNumber, value.stopTime);
delete this.cache[channelNumber];
}
}
}
checkChannels() {
let t = new Date().getTime();
for (const [channelNumber, value] of Object.entries(this.cache)) {
this.checkChannel(t, channelNumber, value);
}
}
timerLoop() {
try {
this.checkChannels();
} catch (err) {
console.error("There was an error in active channel timer loop", err);
} finally {
this.setupTimer();
}
}
registerChannelActive(t, channelNumber) {
this.ensure(t, channelNumber);
if (this.cache[channelNumber].active === 0) {
console.log("Channel is being played: " + channelNumber );
}
this.cache[channelNumber].active++;
//console.log(channelNumber + " ++active=" + this.cache[channelNumber].active );
this.cache[channelNumber].stopTime = 0;
this.cache[channelNumber].lastUpdate = new Date().getTime();
}
registerChannelStopped(t, channelNumber) {
this.ensure(t, channelNumber);
if (this.cache[channelNumber].active === 1) {
console.log("Register that channel is no longer being played: " + channelNumber );
}
if (this.cache[channelNumber].active === 0) {
console.error("Serious issue with channel active service, double delete");
} else {
this.cache[channelNumber].active--;
//console.log(channelNumber + " --active=" + this.cache[channelNumber].active );
let s = this.cache[channelNumber].stopTime;
if ( (typeof(s) === 'undefined') || (s < t) ) {
this.cache[channelNumber].stopTime = t;
}
this.cache[channelNumber].lastUpdate = new Date().getTime();
}
}
ensure(t, channelNumber) {
if (typeof(this.cache[channelNumber]) === 'undefined') {
this.cache[channelNumber] = {
active: 0,
stopTime: t,
lastUpdate: t,
}
}
}
peekChannel(t, channelNumber) {
this.ensure(t, channelNumber);
}
isActiveWrapped(channelNumber) {
if (typeof(this.cache[channelNumber]) === 'undefined') {
return false;
}
if (typeof(this.cache[channelNumber].active) !== 'number') {
return false;
}
return (this.cache[channelNumber].active !== 0);
}
isActive(channelNumber) {
let bol = this.isActiveWrapped(channelNumber);
return bol;
}
}
module.exports = ActiveChannelService

View File

@ -0,0 +1,156 @@
const fs = require('fs');
const express = require('express');
const request = require('request');
/**
* Manager a cache in disk for external images.
*
* @class CacheImageService
*/
class CacheImageService {
constructor( db, fileCacheService ) {
this.cacheService = fileCacheService;
this.imageCacheFolder = 'images';
this.db = db['cache-images'];
}
/**
* Router interceptor to download image and update cache before pass to express.static return this cached image.
*
* GET /:hash - Hash is a full external URL encoded in base64.
* eg.: http://{host}/cache/images/aHR0cHM6Ly8xO...cXVUbmFVNDZQWS1LWQ==
*
* @returns
* @memberof CacheImageService
*/
routerInterceptor() {
const router = express.Router();
router.get('/:hash', async (req, res, next) => {
try {
const hash = req.params.hash;
const imgItem = this.db.find({url: hash})[0];
if(imgItem) {
const file = await this.getImageFromCache(imgItem.url);
if(!file.length) {
const fileMimeType = await this.requestImageAndStore(Buffer.from(imgItem.url, 'base64').toString('ascii'), imgItem);
res.set('content-type', fileMimeType);
next();
} else {
res.set('content-type', imgItem.mimeType);
next();
}
}
} catch(err) {
console.error(err);
res.status(500).send("error");
}
});
return router;
}
/**
* Routers exported to use on express.use() function.
* Use on api routers, like `{host}/api/cache/images`
*
* `DELETE /` - Clear all files on .dizquetv/cache/images
*
* @returns {Router}
* @memberof CacheImageService
*/
apiRouters() {
const router = express.Router();
router.delete('/', async (req, res, next) => {
try {
await this.clearCache();
res.status(200).send({msg: 'Cache Image are Cleared'});
} catch (error) {
console.error(error);
res.status(500).send("error");
}
});
return router;
}
/**
*
*
* @param {*} url External URL to get file/image
* @param {*} dbFile register of file from db
* @returns {promise} `Resolve` when can download imagem and store on cache folder, `Reject` when file are inaccessible over network or can't write on directory
* @memberof CacheImageService
*/
async requestImageAndStore(url, dbFile) {
return new Promise( async(resolve, reject) => {
const requestConfiguration = {
method: 'get',
url
};
request(requestConfiguration, (err, res) => {
if (err) {
reject(err);
} else {
const mimeType = res.headers['content-type'];
this.db.update({_id: dbFile._id}, {url: dbFile.url, mimeType});
request(requestConfiguration)
.pipe(fs.createWriteStream(`${this.cacheService.cachePath}/${this.imageCacheFolder}/${dbFile.url}`))
.on('close', () =>{
resolve(mimeType);
});
}
});
});
}
/**
* Get image from cache using an filename
*
* @param {*} fileName
* @returns {promise} `Resolve` with file content
* @memberof CacheImageService
*/
getImageFromCache(fileName) {
return new Promise(async(resolve, reject) => {
try {
const file = await this.cacheService.getCache(`${this.imageCacheFolder}/${fileName}`);
resolve(file);
} catch (error) {
reject(error);
}
});
}
/**
* Clear all files on .dizquetv/cache/images
*
* @returns {promise}
* @memberof CacheImageService
*/
async clearCache() {
return new Promise( async(resolve, reject) => {
const cachePath = `${this.cacheService.cachePath}/${this.imageCacheFolder}`;
fs.rmdir(cachePath, { recursive: true }, (err) => {
if(err) {
reject();
}
fs.mkdirSync(cachePath);
resolve();
});
});
}
registerImageOnDatabase(imageUrl) {
const url = Buffer.from(imageUrl).toString('base64');
const dbQuery = {url};
if(!this.db.find(dbQuery)[0]) {
this.db.save(dbQuery);
}
return url;
}
}
module.exports = CacheImageService;

View File

@ -0,0 +1,103 @@
const events = require('events')
const channelCache = require("../channel-cache");
class ChannelService extends events.EventEmitter {
constructor(channelDB) {
super();
this.channelDB = channelDB;
this.onDemandService = null;
}
setOnDemandService(onDemandService) {
this.onDemandService = onDemandService;
}
async saveChannel(number, channelJson, options) {
let channel = cleanUpChannel(channelJson);
let ignoreOnDemand = true;
if (
(this.onDemandService != null)
&&
( (typeof(options) === 'undefined') || (options.ignoreOnDemand !== true) )
) {
ignoreOnDemand = false;
this.onDemandService.fixupChannelBeforeSave( channel );
}
channelCache.saveChannelConfig( number, channel);
await channelDB.saveChannel( number, channel );
this.emit('channel-update', { channelNumber: number, channel: channel, ignoreOnDemand: ignoreOnDemand} );
}
async deleteChannel(number) {
await channelDB.deleteChannel( number );
this.emit('channel-update', { channelNumber: number, channel: null} );
channelCache.clear();
}
async getChannel(number) {
let lis = await channelCache.getChannelConfig(this.channelDB, number)
if ( lis == null || lis.length !== 1) {
return null;
}
return lis[0];
}
async getAllChannelNumbers() {
return await channelCache.getAllNumbers(this.channelDB);
}
async getAllChannels() {
return await channelCache.getAllChannels(this.channelDB);
}
}
function cleanUpProgram(program) {
delete program.start
delete program.stop
delete program.streams;
delete program.durationStr;
delete program.commercials;
if (
(typeof(program.duration) === 'undefined')
||
(program.duration <= 0)
) {
console.error(`Input contained a program with invalid duration: ${program.duration}. This program has been deleted`);
return [];
}
if (! Number.isInteger(program.duration) ) {
console.error(`Input contained a program with invalid duration: ${program.duration}. Duration got fixed to be integer.`);
program.duration = Math.ceil(program.duration);
}
return [ program ];
}
function cleanUpChannel(channel) {
if (
(typeof(channel.groupTitle) === 'undefined')
||
(channel.groupTitle === '')
) {
channel.groupTitle = "dizqueTV";
}
channel.programs = channel.programs.flatMap( cleanUpProgram );
delete channel.fillerContent;
delete channel.filler;
channel.fallback = channel.fallback.flatMap( cleanUpProgram );
channel.duration = 0;
for (let i = 0; i < channel.programs.length; i++) {
channel.duration += channel.programs[i].duration;
}
return channel;
}
module.exports = ChannelService

View File

@ -0,0 +1,47 @@
const EventEmitter = require("events");
class EventsService {
constructor() {
this.stream = new EventEmitter();
let that = this;
let fun = () => {
that.push( "heartbeat", "{}");
setTimeout(fun, 5000)
};
fun();
}
setup(app) {
app.get("/api/events", (request, response) => {
console.log("Open event channel.");
response.writeHead(200, {
"Content-Type" : "text/event-stream",
"Cache-Control" : "no-cache",
"connection" : "keep-alive",
} );
let listener = (event,data) => {
//console.log( String(event) + " " + JSON.stringify(data) );
response.write("event: " + String(event) + "\ndata: "
+ JSON.stringify(data) + "\nretry: 5000\n\n" );
};
this.stream.on("push", listener );
response.on( "close", () => {
console.log("Remove event channel.");
this.stream.removeListener("push", listener);
} );
} );
}
push(event, data) {
if (typeof(data.message) !== 'undefined') {
console.log("Push event: " + data.message );
}
this.stream.emit("push", event, data );
}
}
module.exports = EventsService;

View File

@ -0,0 +1,123 @@
const databaseMigration = require('../database-migration');
const DAY_MS = 1000 * 60 * 60 * 24;
const path = require('path');
const fs = require('fs');
class FfmpegSettingsService {
constructor(db, unlock) {
this.db = db;
if (unlock) {
this.unlock();
}
}
get() {
let ffmpeg = this.getCurrentState();
if (isLocked(ffmpeg)) {
ffmpeg.lock = true;
}
// Hid this info from the API
delete ffmpeg.ffmpegPathLockDate;
return ffmpeg;
}
unlock() {
let ffmpeg = this.getCurrentState();
console.log("ffmpeg path UI unlocked for another day...");
ffmpeg.ffmpegPathLockDate = new Date().getTime() + DAY_MS;
this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, ffmpeg)
}
update(attempt) {
let ffmpeg = this.getCurrentState();
attempt.ffmpegPathLockDate = ffmpeg.ffmpegPathLockDate;
if (isLocked(ffmpeg)) {
console.log("Note: ffmpeg path is not being updated since it's been locked for your security.");
attempt.ffmpegPath = ffmpeg.ffmpegPath;
if (typeof(ffmpeg.ffmpegPathLockDate) === 'undefined') {
// make sure to lock it even if it was undefined
attempt.ffmpegPathLockDate = new Date().getTime() - DAY_MS;
}
} else if (attempt.addLock === true) {
// lock it right now
attempt.ffmpegPathLockDate = new Date().getTime() - DAY_MS;
} else {
attempt.ffmpegPathLockDate = new Date().getTime() + DAY_MS;
}
delete attempt.addLock;
delete attempt.lock;
let err = fixupFFMPEGSettings(attempt);
if ( typeof(err) !== "undefined" ) {
return {
error: err
}
}
this.db['ffmpeg-settings'].update({ _id: ffmpeg._id }, attempt)
return {
ffmpeg: this.get()
}
}
reset() {
// Even if reseting, it's impossible to unlock the ffmpeg path
let ffmpeg = databaseMigration.defaultFFMPEG() ;
this.update(ffmpeg);
return this.get();
}
getCurrentState() {
return this.db['ffmpeg-settings'].find()[0]
}
}
function fixupFFMPEGSettings(ffmpeg) {
if (typeof(ffmpeg.ffmpegPath) !== 'string') {
return "ffmpeg path is required."
}
if (! isValidFilePath(ffmpeg.ffmpegPath)) {
return "ffmpeg path must be a valid file path."
}
if (typeof(ffmpeg.maxFPS) === 'undefined') {
ffmpeg.maxFPS = 60;
return null;
} else if ( isNaN(ffmpeg.maxFPS) ) {
return "maxFPS should be a number";
}
}
//These checks are good but might not be enough, as long as we are letting the
//user choose any path and we are making dizqueTV execute, it is too risky,
//hence why we are also adding the lock feature on top of these checks.
function isValidFilePath(filePath) {
const normalizedPath = path.normalize(filePath);
if (!path.isAbsolute(normalizedPath)) {
return false;
}
try {
const stats = fs.statSync(normalizedPath);
return stats.isFile();
} catch (err) {
// Handle potential errors (e.g., file not found, permission issues)
if (err.code === 'ENOENT') {
return false; // File does not exist
} else {
throw err; // Re-throw other errors for debugging
}
}
}
function isLocked(ffmpeg) {
return isNaN(ffmpeg.ffmpegPathLockDate) || ffmpeg.ffmpegPathLockDate < new Date().getTime();
}
module.exports =FfmpegSettingsService;

View File

@ -0,0 +1,97 @@
const path = require('path');
const fs = require('fs');
/**
* Store files in cache
*
* @class FileCacheService
*/
class FileCacheService {
constructor(cachePath) {
this.cachePath = cachePath;
this.cache = {};
}
/**
* `save` a file on cache folder
*
* @param {string} fullFilePath
* @param {*} data
* @returns {promise}
* @memberof CacheService
*/
setCache(fullFilePath, data) {
return new Promise((resolve, reject) => {
try {
const file = fs.createWriteStream(path.join(this.cachePath, fullFilePath));
file.write(data, (err) => {
if(err) {
throw Error("Can't save file: ", err);
} else {
this.cache[fullFilePath] = data;
resolve(true);
}
});
} catch (err) {
reject(err);
}
});
}
/**
* `get` a File from cache folder
*
* @param {string} fullFilePath
* @returns {promise} `Resolve` with file content, `Reject` with false
* @memberof CacheService
*/
getCache(fullFilePath) {
return new Promise((resolve, reject) => {
try {
if(fullFilePath in this.cache) {
resolve(this.cache[fullFilePath]);
} else {
fs.readFile(path.join(this.cachePath, fullFilePath), 'utf8', function (err,data) {
if (err) {
resolve(false);
}
resolve(data);
});
}
} catch (error) {
resolve(false);
throw Error("Can't get file", error)
}
});
}
/**
* `delete` a File from cache folder
*
* @param {string} fullFilePath
* @returns {promise}
* @memberof CacheService
*/
deleteCache(fullFilePath) {
return new Promise((resolve, reject) => {
try {
let thePath = path.join(this.cachePath, fullFilePath);
if (! fs.existsSync(thePath)) {
return resolve(true);
}
fs.unlinkSync(thePath, (err) => {
if(err) {
throw Error("Can't save file: ", err);
} else {
delete this.cache[fullFilePath];
resolve(true);
}
});
} catch (err) {
reject(err);
}
});
}
}
module.exports = FileCacheService;

View File

@ -0,0 +1,68 @@
//This is an exact copy of the file with the same now in the web project
//one of these days, we'll figure out how to share the code.
module.exports = function () {
let movieTitleOrder = {};
let movieTitleOrderNumber = 0;
return (program) => {
if ( typeof(program.customShowId) !== 'undefined' ) {
return {
hasShow : true,
showId : "custom." + program.customShowId,
showDisplayName : program.customShowName,
order : program.customOrder,
shuffleOrder : program.shuffleOrder,
}
} else if (program.isOffline && program.type === 'redirect') {
return {
hasShow : true,
showId : "redirect." + program.channel,
order : program.duration,
showDisplayName : `Redirect to channel ${program.channel}`,
channel: program.channel,
}
} else if (program.isOffline) {
return {
hasShow : false
}
} else if (program.type === 'movie') {
let key = program.serverKey + "|" + program.key;
if (typeof(movieTitleOrder[key]) === 'undefined') {
movieTitleOrder[key] = movieTitleOrderNumber++;
}
return {
hasShow : true,
showId : "movie.",
showDisplayName : "Movies",
order : movieTitleOrder[key],
shuffleOrder : program.shuffleOrder,
}
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
let s = 0;
let e = 0;
if ( typeof(program.season) !== 'undefined') {
s = program.season;
}
if ( typeof(program.episode) !== 'undefined') {
e = program.episode;
}
let prefix = "tv.";
if (program.type === 'track') {
prefix = "audio.";
}
return {
hasShow: true,
showId : prefix + program.showTitle,
showDisplayName : program.showTitle,
order : s * 1000000 + e,
shuffleOrder : program.shuffleOrder,
}
} else {
return {
hasShow : false,
}
}
}
}

View File

@ -0,0 +1,97 @@
/**
* Manager and Generate M3U content
*
* @class M3uService
*/
class M3uService {
constructor(fileCacheService, channelService) {
this.channelService = channelService;
this.cacheService = fileCacheService;
this.cacheReady = false;
this.channelService.on("channel-update", (data) => {
this.clearCache();
} );
}
/**
* Get the channel list in HLS or M3U
*
* @param {string} [type='m3u'] List type
* @returns {promise} Return a Promise with HLS or M3U file content
* @memberof M3uService
*/
getChannelList(host) {
return this.buildM3uList(host);
}
/**
* Build M3U with cache
*
* @param {string} host
* @returns {promise} M3U file content
* @memberof M3uService
*/
async buildM3uList(host) {
if (this.cacheReady) {
const cachedM3U = await this.cacheService.getCache('channels.m3u');
if (cachedM3U) {
return this.replaceHostOnM3u(host, cachedM3U);
}
}
let channels = await this.channelService.getAllChannels();
channels.sort((a, b) => {
return parseInt(a.number) < parseInt(b.number) ? -1 : 1
});
const tvg = `{{host}}/api/xmltv.xml`;
let data = `#EXTM3U url-tvg="${tvg}" x-tvg-url="${tvg}"\n`;
for (var i = 0; i < channels.length; i++) {
if (channels[i].stealth !== true) {
data += `#EXTINF:0 tvg-id="${channels[i].number}" CUID="${channels[i].number}" tvg-chno="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="${channels[i].groupTitle}",${channels[i].name}\n`
data += `{{host}}/video?channel=${channels[i].number}\n`
}
}
if (channels.length === 0) {
data += `#EXTINF:0 tvg-id="1" tvg-chno="1" tvg-name="dizqueTV" tvg-logo="{{host}}/resources/dizquetv.png" group-title="dizqueTV",dizqueTV\n`
data += `{{host}}/setup\n`
}
let saveCacheThread = async() => {
try {
await this.cacheService.setCache('channels.m3u', data);
this.cacheReady = true;
} catch(err) {
console.error(err);
}
};
saveCacheThread();
return this.replaceHostOnM3u(host, data);
}
/**
* Replace {{host}} string with a URL on file contents.
*
* @param {*} host
* @param {*} data
* @returns
* @memberof M3uService
*/
replaceHostOnM3u(host, data) {
return data.replace(/\{\{host\}\}/g, host);
}
/**
* Clear channels.m3u file from cache folder.
*
* @memberof M3uService
*/
async clearCache() {
this.cacheReady = false;
}
}
module.exports = M3uService;

View File

@ -0,0 +1,226 @@
const constants = require("../constants");
const SLACK = constants.SLACK;
class OnDemandService
{
/****
*
**/
constructor(channelService) {
this.channelService = channelService;
this.channelService.setOnDemandService(this);
this.activeChannelService = null;
}
setActiveChannelService(activeChannelService) {
this.activeChannelService = activeChannelService;
}
activateChannelIfNeeded(moment, channel) {
if ( this.isOnDemandChannelPaused(channel) ) {
channel = this.resumeOnDemandChannel(moment, channel);
this.updateChannelAsync(channel);
}
return channel;
}
async registerChannelStopped(channelNumber, stopTime, waitForSave) {
try {
let channel = await this.channelService.getChannel(channelNumber);
if (channel == null) {
console.error("Could not stop channel " + channelNumber + " because it apparently no longer exists"); // I guess if someone deletes the channel just in the grace period?
return
}
if ( (typeof(channel.onDemand) !== 'undefined') && channel.onDemand.isOnDemand && ! channel.onDemand.paused) {
//pause the channel
channel = this.pauseOnDemandChannel( channel , stopTime );
if (waitForSave) {
await this.updateChannelSync(channel);
} else {
this.updateChannelAsync(channel);
}
}
} catch (err) {
console.error("Error stopping channel", err);
}
}
pauseOnDemandChannel(originalChannel, stopTime) {
console.log("Pause on-demand channel : " + originalChannel.number);
let channel = clone(originalChannel);
// first find what the heck is playing
let t = stopTime;
let s = new Date(channel.startTime).getTime();
let onDemand = channel.onDemand;
onDemand.paused = true;
if ( channel.programs.length == 0) {
console.log("On-demand channel has no programs. That doesn't really make a lot of sense...");
onDemand.firstProgramModulo = s % onDemand.modulo;
onDemand.playedOffset = 0;
} else if (t < s) {
// the first program didn't even play.
onDemand.firstProgramModulo = s % onDemand.modulo;
onDemand.playedOffset = 0;
} else {
let i = 0;
let total = 0;
while (true) {
let d = channel.programs[i].duration;
if ( (s + total <= t) && (t < s + total + d) ) {
break;
}
total += d;
i = (i + 1) % channel.programs.length;
}
// rotate
let programs = [];
for (let j = i; j < channel.programs.length; j++) {
programs.push( channel.programs[j] );
}
for (let j = 0; j <i; j++) {
programs.push( channel.programs[j] );
}
onDemand.firstProgramModulo = (s + total) % onDemand.modulo;
onDemand.playedOffset = t - (s + total);
channel.programs = programs;
channel.startTime = new Date(s + total).toISOString();
}
return channel;
}
async updateChannelSync(channel) {
try {
await this.channelService.saveChannel(
channel.number,
channel,
{ignoreOnDemand: true}
);
console.log("Channel " + channel.number + " saved by on-demand service...");
} catch (err) {
console.error("Error saving resumed channel: " + channel.number, err);
}
}
updateChannelAsync(channel) {
this.updateChannelSync(channel);
}
fixupChannelBeforeSave(channel) {
let isActive = false;
if (this.activeChannelService != null && this.activeChannelService.isActive(channel.number) ) {
isActive = true;
}
if (typeof(channel.onDemand) === 'undefined') {
channel.onDemand = {};
}
if (typeof(channel.onDemand.isOnDemand) !== 'boolean') {
channel.onDemand.isOnDemand = false;
}
if ( channel.onDemand.isOnDemand !== true ) {
channel.onDemand.modulo = 1;
channel.onDemand.firstProgramModulo = 1;
channel.onDemand.playedOffset = 0;
channel.onDemand.paused = false;
} else {
if ( typeof(channel.onDemand.modulo) !== 'number') {
channel.onDemand.modulo = 1;
}
if (isActive) {
// if it is active, the channel isn't paused
channel.onDemand.paused = false;
} else {
let s = new Date(channel.startTime).getTime();
channel.onDemand.paused = true;
channel.onDemand.firstProgramModulo = s % channel.onDemand.modulo;
channel.onDemand.playedOffset = 0;
}
}
}
resumeOnDemandChannel(t, originalChannel) {
let channel = clone(originalChannel);
console.log("Resume on-demand channel: " + channel.name);
let programs = channel.programs;
let onDemand = channel.onDemand;
onDemand.paused = false; //should be the invariant
if (programs.length == 0) {
console.log("On-demand channel is empty. This doesn't make a lot of sense...");
return channel;
}
let i = 0;
let backupFo = onDemand.firstProgramModulo;
while (i < programs.length) {
let program = programs[i];
if ( program.isOffline && (program.type !== 'redirect') ) {
//skip flex
i++;
onDemand.playedOffset = 0;
onDemand.firstProgramModulo = ( onDemand.firstProgramModulo + program.duration ) % onDemand.modulo;
} else {
break;
}
}
if (i == programs.length) {
console.log("Everything in the channel is flex... This doesn't really make a lot of sense for an onDemand channel, you know...");
i = 0;
onDemand.playedOffset = 0;
onDemand.firstProgramModulo = backupFo;
}
// Last we've seen this channel, it was playing program #i , played the first playedOffset milliseconds.
// move i to the beginning of the program list
let newPrograms = []
for (let j = i; j < programs.length; j++) {
newPrograms.push( programs[j] );
}
for (let j = 0; j < i; j++) {
newPrograms.push( programs[j] );
}
// now the start program is 0, and the "only" thing to do now is change the start time
let startTime = t - onDemand.playedOffset;
// with this startTime, it would work perfectly if modulo is 1. But what about other cases?
let tm = t % onDemand.modulo;
let pm = (onDemand.firstProgramModulo + onDemand.playedOffset) % onDemand.modulo;
if (tm < pm) {
startTime += (pm - tm);
} else {
let o = (tm - pm);
startTime = startTime - o;
//It looks like it is convenient to make the on-demand a bit more lenient SLACK-wise tha
//other parts of the schedule process. So SLACK*2 instead of just SLACK
if (o >= SLACK*2) {
startTime += onDemand.modulo;
}
}
channel.startTime = (new Date(startTime)).toISOString();
channel.programs = newPrograms;
return channel;
}
isOnDemandChannelPaused(channel) {
return (
(typeof(channel.onDemand) !== 'undefined')
&&
(channel.onDemand.isOnDemand === true)
&&
(channel.onDemand.paused === true)
);
}
}
function clone(channel) {
return JSON.parse( JSON.stringify(channel) );
}
module.exports = OnDemandService

View File

@ -0,0 +1,35 @@
const helperFuncs = require("../helperFuncs");
/* Tells us what is or should be playing in some channel
If the channel is a an on-demand channel and is paused, resume the channel.
Before running the logic.
This hub for the programming logic used to be helperFuncs.getCurrentProgramAndTimeElapsed.
This class will still call that function, but this should be the entry point
for that logic.
Eventually it looks like a good idea to move that logic here.
*/
class ProgrammingService
{
/****
*
**/
constructor(onDemandService) {
this.onDemandService = onDemandService;
}
getCurrentProgramAndTimeElapsed(moment, channel) {
channel = onDemandService.activateChannelIfNeeded(moment, channel);
return helperFuncs.getCurrentProgramAndTimeElapsed(moment, channel);
}
}
module.exports = ProgrammingService

View File

@ -0,0 +1,349 @@
const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const throttle = require('./throttle');
const orderers = require("./show-orderers");
const MINUTE = 60*1000;
const DAY = 24*60*MINUTE;
const LIMIT = 40000;
function getShow(program) {
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
function getProgramId(program) {
let s = program.serverKey;
if (typeof(s) === 'undefined') {
s = 'unknown';
}
let p = program.key;
if (typeof(p) === 'undefined') {
p = 'unknown';
}
return s + "|" + p;
}
function addProgramToShow(show, program) {
if ( (show.id == 'flex.') || show.id.startsWith("redirect.") ) {
//nothing to do
return;
}
let id = getProgramId(program)
if(show.programs[id] !== true) {
show.programs.push(program);
show.programs[id] = true
}
}
module.exports = async( programs, schedule ) => {
if (! Array.isArray(programs) ) {
return { userError: 'Expected a programs array' };
}
if (typeof(schedule) === 'undefined') {
return { userError: 'Expected a schedule' };
}
//verify that the schedule is in the correct format
if (! Array.isArray(schedule.slots) ) {
return { userError: 'Expected a "slots" array in schedule' };
}
if (typeof(schedule).period === 'undefined') {
schedule.period = DAY;
}
for (let i = 0; i < schedule.slots.length; i++) {
if (typeof(schedule.slots[i].duration) === 'undefined') {
return { userError: "Each slot should have a duration" };
}
if (typeof(schedule.slots[i].showId) === 'undefined') {
return { userError: "Each slot should have a showId" };
}
if (
(schedule.slots[i].duration <= 0)
|| (Math.floor(schedule.slots[i].duration) != schedule.slots[i].duration)
) {
return { userError: "Slot duration should be a integer number of milliseconds greater than 0" };
}
if ( isNaN(schedule.slots[i].cooldown) ) {
schedule.slots[i].cooldown = 0;
}
if ( isNaN(schedule.slots[i].weight) ) {
schedule.slots[i].weight = 1;
}
}
if (typeof(schedule.pad) === 'undefined') {
return { userError: "Expected schedule.pad" };
}
if (typeof(schedule.maxDays) == 'undefined') {
return { userError: "schedule.maxDays must be defined." };
}
if (typeof(schedule.flexPreference) === 'undefined') {
schedule.flexPreference = "distribute";
}
if (typeof(schedule.padStyle) === 'undefined') {
schedule.padStyle = "slot";
}
if (schedule.padStyle !== "slot" && schedule.padStyle !== "episode") {
return { userError: `Invalid schedule.padStyle value: "${schedule.padStyle}"` };
}
let flexBetween = ( schedule.flexPreference !== "end" );
let showsById = {};
let shows = [];
function getNextForSlot(slot, remaining) {
//remaining doesn't restrict what next show is picked. It is only used
//for shows with flexible length (flex and redirects)
if (slot.showId === "flex.") {
return {
isOffline: true,
duration: remaining,
}
}
let show = shows[ showsById[slot.showId] ];
if (slot.showId.startsWith("redirect.")) {
return {
isOffline: true,
type: "redirect",
duration: remaining,
channel: show.channel,
}
} else if (slot.order === 'shuffle') {
return orderers.getShowShuffler(show).current();
} else if (slot.order === 'next') {
return orderers.getShowOrderer(show).current();
}
}
function advanceSlot(slot) {
if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect") ) ) {
return;
}
let show = shows[ showsById[slot.showId] ];
if (slot.order === 'shuffle') {
return orderers.getShowShuffler(show).next();
} else if (slot.order === 'next') {
return orderers.getShowOrderer(show).next();
}
}
function makePadded(item) {
let padOption = schedule.pad;
if (schedule.padStyle === "slot") {
padOption = 1;
}
let x = item.duration;
let m = x % padOption;
let f = 0;
if ( (m > constants.SLACK) && (padOption - m > constants.SLACK) ) {
f = padOption - m;
}
return {
item: item,
pad: f,
totalDuration: item.duration + f,
}
}
// load the programs
for (let i = 0; i < programs.length; i++) {
let p = programs[i];
let show = getShow(p);
if (show != null) {
if (typeof(showsById[show.id] ) === 'undefined') {
showsById[show.id] = shows.length;
shows.push( show );
show.founder = p;
show.programs = [];
} else {
show = shows[ showsById[show.id] ];
}
addProgramToShow( show, p );
}
}
let s = schedule.slots;
let ts = (new Date() ).getTime();
let t0 = ts;
let p = [];
let t = t0;
let hardLimit = t0 + schedule.maxDays * DAY;
let pushFlex = (d) => {
if (d > 0) {
t += d;
if ( (p.length > 0) && p[p.length-1].isOffline && (p[p.length-1].type != 'redirect') ) {
p[p.length-1].duration += d;
} else {
p.push( {
duration: d,
isOffline : true,
} );
}
}
}
let pushProgram = (item) => {
if ( item.isOffline && (item.type !== 'redirect') ) {
pushFlex(item.duration);
} else {
p.push(item);
t += item.duration;
}
};
let slotLastPlayed = {};
while ( (t < hardLimit) && (p.length < LIMIT) ) {
await throttle();
//ensure t is padded
let m = t % schedule.pad;
if ( (t % schedule.pad > constants.SLACK) && (schedule.pad - m > constants.SLACK) ) {
pushFlex( schedule.pad - m );
continue;
}
let slot = null;
let slotIndex = null;
let remaining = null;
let n = 0;
let minNextTime = t + 24*DAY;
for (let i = 0; i < s.length; i++) {
if ( typeof( slotLastPlayed[i] ) !== undefined ) {
let lastt = slotLastPlayed[i];
minNextTime = Math.min( minNextTime, lastt + s[i].cooldown );
if (t - lastt < s[i].cooldown - constants.SLACK ) {
continue;
}
}
n += s[i].weight;
if ( random.bool(s[i].weight,n) ) {
slot = s[i];
slotIndex = i;
remaining = s[i].duration;
}
}
if (slot == null) {
//Nothing to play, likely due to cooldown
pushFlex( minNextTime - t);
continue;
}
let item = getNextForSlot(slot, remaining);
if (item.isOffline) {
//flex or redirect. We can just use the whole duration
item.duration = remaining;
pushProgram(item);
slotLastPlayed[ slotIndex ] = t;
continue;
}
if (item.duration > remaining) {
// Slide
pushProgram(item);
slotLastPlayed[ slotIndex ] = t;
advanceSlot(slot);
continue;
}
let padded = makePadded(item);
let total = padded.totalDuration;
advanceSlot(slot);
let pads = [ padded ];
while(true) {
let item2 = getNextForSlot(slot);
if (total + item2.duration > remaining) {
break;
}
let padded2 = makePadded(item2);
pads.push(padded2);
advanceSlot(slot);
total += padded2.totalDuration;
}
let temt = t + total;
let rem = 0;
if (
(temt % schedule.pad >= constants.SLACK)
&& (temt % schedule.pad < schedule.pad - constants.SLACK)
) {
rem = schedule.pad - temt % schedule.pad;
}
if (flexBetween && (schedule.padStyle === 'episode') ) {
let div = Math.floor(rem / schedule.pad );
let mod = rem % schedule.pad;
// add mod to the latest item
pads[ pads.length - 1].pad += mod;
pads[ pads.length - 1].totalDuration += mod;
let sortedPads = pads.map( (p, $index) => {
return {
pad: p.pad,
index : $index,
}
});
sortedPads.sort( (a,b) => { return a.pad - b.pad; } );
for (let i = 0; i < pads.length; i++) {
let q = Math.floor( div / pads.length );
if (i < div % pads.length) {
q++;
}
let j = sortedPads[i].index;
pads[j].pad += q * schedule.pad;
}
} else if (flexBetween) {
//just distribute it equitatively
let div = Math.floor( rem / pads.length );
let totalAdded = 0;
for (let i = 0; i < pads.length; i++) {
pads[i].pad += div;
totalAdded += div;
}
pads[0].pad += rem - totalAdded;
} else {
//also add div to the latest item
pads[ pads.length - 1].pad += rem;
pads[ pads.length - 1].totalDuration += rem;
}
// now unroll them all
for (let i = 0; i < pads.length; i++) {
pushProgram( pads[i].item );
slotLastPlayed[ slotIndex ] = t;
pushFlex( pads[i].pad );
}
}
while ( (t > hardLimit) || (p.length >= LIMIT) ) {
t -= p.pop().duration;
}
let m = (t - t0) % schedule.period;
if (m != 0) {
//ensure the schedule is a multiple of period
pushFlex( schedule.period - m);
}
return {
programs: p,
startTime: (new Date(t0)).toISOString(),
}
}

View File

@ -0,0 +1,156 @@
const random = require('../helperFuncs').random;
const getShowData = require("./get-show-data")();
const randomJS = require("random-js");
const Random = randomJS.Random;
/****
*
* Code shared by random slots and time slots for keeping track of the order
* of episodes
*
**/
function shuffle(array, lo, hi, randomOverride ) {
let r = randomOverride;
if (typeof(r) === 'undefined') {
r = random;
}
if (typeof(lo) === 'undefined') {
lo = 0;
hi = array.length;
}
let currentIndex = hi, temporaryValue, randomIndex
while (lo !== currentIndex) {
randomIndex = r.integer(lo, currentIndex-1);
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
function getShowOrderer(show) {
if (typeof(show.orderer) === 'undefined') {
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let position = 0;
while (
(position + 1 < sortedPrograms.length )
&&
(
getShowData(show.founder).order
!==
getShowData(sortedPrograms[position]).order
)
) {
position++;
}
show.orderer = {
current : () => {
return sortedPrograms[position];
},
next: () => {
position = (position + 1) % sortedPrograms.length;
},
}
}
return show.orderer;
}
function getShowShuffler(show) {
if (typeof(show.shuffler) === 'undefined') {
if (typeof(show.programs) === 'undefined') {
throw Error(show.id + " has no programs?")
}
let sortedPrograms = JSON.parse( JSON.stringify(show.programs) );
sortedPrograms.sort((a, b) => {
let showA = getShowData(a);
let showB = getShowData(b);
return showA.order - showB.order;
});
let n = sortedPrograms.length;
let splitPrograms = [];
let randomPrograms = [];
for (let i = 0; i < n; i++) {
splitPrograms.push( sortedPrograms[i] );
randomPrograms.push( {} );
}
let showId = getShowData(show.programs[0]).showId;
let position = show.founder.shuffleOrder;
if (typeof(position) === 'undefined') {
position = 0;
}
let localRandom = null;
let initGeneration = (generation) => {
let seed = [];
for (let i = 0 ; i < show.showId.length; i++) {
seed.push( showId.charCodeAt(i) );
}
seed.push(generation);
localRandom = new Random( randomJS.MersenneTwister19937.seedWithArray(seed) )
if (generation == 0) {
shuffle( splitPrograms, 0, n , localRandom );
}
for (let i = 0; i < n; i++) {
randomPrograms[i] = splitPrograms[i];
}
let a = Math.floor(n / 2);
shuffle( randomPrograms, 0, a, localRandom );
shuffle( randomPrograms, a, n, localRandom );
};
initGeneration(0);
let generation = Math.floor( position / n );
initGeneration( generation );
show.shuffler = {
current : () => {
let prog = JSON.parse(
JSON.stringify(randomPrograms[position % n] )
);
prog.shuffleOrder = position;
return prog;
},
next: () => {
position++;
if (position % n == 0) {
let generation = Math.floor( position / n );
initGeneration( generation );
}
},
}
}
return show.shuffler;
}
module.exports = {
getShowOrderer : getShowOrderer,
getShowShuffler: getShowShuffler,
}

6
src/services/throttle.js Normal file
View File

@ -0,0 +1,6 @@
//Adds a slight pause so that long operations
module.exports = function() {
return new Promise((resolve) => {
setImmediate(() => resolve());
});
}

View File

@ -0,0 +1,349 @@
const constants = require("../constants");
const getShowData = require("./get-show-data")();
const random = require('../helperFuncs').random;
const throttle = require('./throttle');
const orderers = require("./show-orderers");
const MINUTE = 60*1000;
const DAY = 24*60*MINUTE;
const LIMIT = 40000;
function getShow(program) {
let d = getShowData(program);
if (! d.hasShow) {
return null;
} else {
d.description = d.showDisplayName;
d.id = d.showId;
return d;
}
}
function getProgramId(program) {
let s = program.serverKey;
if (typeof(s) === 'undefined') {
s = 'unknown';
}
let p = program.key;
if (typeof(p) === 'undefined') {
p = 'unknown';
}
return s + "|" + p;
}
function addProgramToShow(show, program) {
if ( (show.id == 'flex.') || show.id.startsWith("redirect.") ) {
//nothing to do
return;
}
let id = getProgramId(program)
if(show.programs[id] !== true) {
show.programs.push(program);
show.programs[id] = true
}
}
module.exports = async( programs, schedule ) => {
if (! Array.isArray(programs) ) {
return { userError: 'Expected a programs array' };
}
if (typeof(schedule) === 'undefined') {
return { userError: 'Expected a schedule' };
}
if (typeof(schedule.timeZoneOffset) === 'undefined') {
return { userError: 'Expected a time zone offset' };
}
//verify that the schedule is in the correct format
if (! Array.isArray(schedule.slots) ) {
return { userError: 'Expected a "slots" array in schedule' };
}
if (typeof(schedule).period === 'undefined') {
schedule.period = DAY;
}
for (let i = 0; i < schedule.slots.length; i++) {
if (typeof(schedule.slots[i].time) === 'undefined') {
return { userError: "Each slot should have a time" };
}
if (typeof(schedule.slots[i].showId) === 'undefined') {
return { userError: "Each slot should have a showId" };
}
if (
(schedule.slots[i].time < 0)
|| (schedule.slots[i].time >= schedule.period)
|| (Math.floor(schedule.slots[i].time) != schedule.slots[i].time)
) {
return { userError: "Slot times should be a integer number of milliseconds between 0 and period-1, inclusive" };
}
schedule.slots[i].time = ( schedule.slots[i].time + 10*schedule.period + schedule.timeZoneOffset*MINUTE) % schedule.period;
}
schedule.slots.sort( (a,b) => {
return (a.time - b.time);
} );
for (let i = 1; i < schedule.slots.length; i++) {
if (schedule.slots[i].time == schedule.slots[i-1].time) {
return { userError: "Slot times should be unique."};
}
}
if (typeof(schedule.pad) === 'undefined') {
return { userError: "Expected schedule.pad" };
}
if (typeof(schedule.lateness) == 'undefined') {
return { userError: "schedule.lateness must be defined." };
}
if (typeof(schedule.maxDays) == 'undefined') {
return { userError: "schedule.maxDays must be defined." };
}
if (typeof(schedule.flexPreference) === 'undefined') {
schedule.flexPreference = "distribute";
}
if (schedule.flexPreference !== "distribute" && schedule.flexPreference !== "end") {
return { userError: `Invalid schedule.flexPreference value: "${schedule.flexPreference}"` };
}
let flexBetween = ( schedule.flexPreference !== "end" );
// throttle so that the stream is not affected negatively
let steps = 0;
let showsById = {};
let shows = [];
function getNextForSlot(slot, remaining) {
//remaining doesn't restrict what next show is picked. It is only used
//for shows with flexible length (flex and redirects)
if (slot.showId === "flex.") {
return {
isOffline: true,
duration: remaining,
}
}
let show = shows[ showsById[slot.showId] ];
if (slot.showId.startsWith("redirect.")) {
return {
isOffline: true,
type: "redirect",
duration: remaining,
channel: show.channel,
}
} else if (slot.order === 'shuffle') {
return orderers.getShowShuffler(show).current();
} else if (slot.order === 'next') {
return orderers.getShowOrderer(show).current();
}
}
function advanceSlot(slot) {
if ( (slot.showId === "flex.") || (slot.showId.startsWith("redirect.") ) ) {
return;
}
let show = shows[ showsById[slot.showId] ];
if (slot.order === 'shuffle') {
return orderers.getShowShuffler(show).next();
} else if (slot.order === 'next') {
return orderers.getShowOrderer(show).next();
}
}
function makePadded(item) {
let x = item.duration;
let m = x % schedule.pad;
let f = 0;
if ( (m > constants.SLACK) && (schedule.pad - m > constants.SLACK) ) {
f = schedule.pad - m;
}
return {
item: item,
pad: f,
totalDuration: item.duration + f,
}
}
// load the programs
for (let i = 0; i < programs.length; i++) {
let p = programs[i];
let show = getShow(p);
if (show != null) {
if (typeof(showsById[show.id] ) === 'undefined') {
showsById[show.id] = shows.length;
shows.push( show );
show.founder = p;
show.programs = [];
} else {
show = shows[ showsById[show.id] ];
}
addProgramToShow( show, p );
}
}
let s = schedule.slots;
let ts = (new Date() ).getTime();
let curr = ts - ts % (schedule.period);
let t0 = curr + s[0].time;
let p = [];
let t = t0;
let wantedFinish = t % schedule.period;
let hardLimit = t0 + schedule.maxDays * DAY;
let pushFlex = (d) => {
if (d > 0) {
t += d;
if ( (p.length > 0) && p[p.length-1].isOffline && (p[p.length-1].type != 'redirect') ) {
p[p.length-1].duration += d;
} else {
p.push( {
duration: d,
isOffline : true,
} );
}
}
}
let pushProgram = (item) => {
if ( item.isOffline && (item.type !== 'redirect') ) {
pushFlex(item.duration);
} else {
p.push(item);
t += item.duration;
}
};
if (ts > t0) {
pushFlex( ts - t0 );
}
while ( (t < hardLimit) && (p.length < LIMIT) ) {
await throttle();
//ensure t is padded
let m = t % schedule.pad;
if ( (t % schedule.pad > constants.SLACK) && (schedule.pad - m > constants.SLACK) ) {
pushFlex( schedule.pad - m );
continue;
}
let dayTime = t % schedule.period;
let slot = null;
let remaining = null;
let late = null;
for (let i = 0; i < s.length; i++) {
let endTime;
if (i == s.length - 1) {
endTime = s[0].time + schedule.period;
} else {
endTime = s[i+1].time;
}
if ((s[i].time <= dayTime) && (dayTime < endTime)) {
slot = s[i];
remaining = endTime - dayTime;
late = dayTime - s[i].time;
break;
}
if ((s[i].time <= dayTime + schedule.period) && (dayTime + schedule.period < endTime)) {
slot = s[i];
dayTime += schedule.period;
remaining = endTime - dayTime;
late = dayTime + schedule.period - s[i].time;
break;
}
}
if (slot == null) {
throw Error("Unexpected. Unable to find slot for time of day " + t + " " + dayTime);
}
let item = getNextForSlot(slot, remaining);
if (late >= schedule.lateness + constants.SLACK ) {
//it's late.
item = {
isOffline : true,
duration: remaining,
}
}
if (item.isOffline) {
//flex or redirect. We can just use the whole duration
item.duration = remaining;
pushProgram(item);
continue;
}
if (item.duration > remaining) {
// Slide
pushProgram(item);
advanceSlot(slot);
continue;
}
let padded = makePadded(item);
let total = padded.totalDuration;
advanceSlot(slot);
let pads = [ padded ];
while(true) {
let item2 = getNextForSlot(slot, remaining);
if (total + item2.duration > remaining) {
break;
}
let padded2 = makePadded(item2);
pads.push(padded2);
advanceSlot(slot);
total += padded2.totalDuration;
}
let rem = Math.max(0, remaining - total);
if (flexBetween) {
let div = Math.floor(rem / schedule.pad );
let mod = rem % schedule.pad;
// add mod to the latest item
pads[ pads.length - 1].pad += mod;
pads[ pads.length - 1].totalDuration += mod;
let sortedPads = pads.map( (p, $index) => {
return {
pad: p.pad,
index : $index,
}
});
sortedPads.sort( (a,b) => { return a.pad - b.pad; } );
for (let i = 0; i < pads.length; i++) {
let q = Math.floor( div / pads.length );
if (i < div % pads.length) {
q++;
}
let j = sortedPads[i].index;
pads[j].pad += q * schedule.pad;
}
} else {
//also add div to the latest item
pads[ pads.length - 1].pad += rem;
pads[ pads.length - 1].totalDuration += rem;
}
// now unroll them all
for (let i = 0; i < pads.length; i++) {
pushProgram( pads[i].item );
pushFlex( pads[i].pad );
}
}
while ( (t > hardLimit) || (p.length >= LIMIT) ) {
t -= p.pop().duration;
}
let m = (t - t0) % schedule.period;
if (m > 0) {
//ensure the schedule is a multiple of period
pushFlex( schedule.period - m);
}
return {
programs: p,
startTime: (new Date(t0)).toISOString(),
}
}

File diff suppressed because it is too large Load Diff

135
src/svg/dizquetv.svg Normal file
View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="200"
height="200"
viewBox="0 0 52.9168 52.916668"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="dizquetv.svg"
inkscape:export-filename="/home/vx/dev/pseudotv/resources/dizquetv.png"
inkscape:export-xdpi="240"
inkscape:export-ydpi="240">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="74.610162"
inkscape:cy="39.873047"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-244.08278)">
<rect
style="opacity:1;fill:#3f3f3f;fill-opacity:0.86666667;stroke:none;stroke-width:1.46501946;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4712"
width="52.206699"
height="35.866219"
x="-1.3992016"
y="254.87126"
transform="matrix(0.99990505,-0.01378015,0.00643904,0.99997927,0,0)" />
<g
id="g4581"
style="fill:#080808;fill-opacity:1;stroke-width:0.82573813"
transform="matrix(1.2119871,0,0,1.2100891,-82.577875,-32.337926)">
<rect
transform="rotate(-0.94645665)"
y="239.28041"
x="65.156158"
height="27.75024"
width="41.471352"
id="rect4524"
style="opacity:1;fill:#080808;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<rect
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4518"
width="10.338528"
height="23.042738"
x="8.7463942"
y="260.34518"
transform="matrix(0.99995865,0.00909414,-0.00926779,0.99995705,0,0)" />
<ellipse
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4568"
cx="44.118061"
cy="263.32062"
rx="2.4216392"
ry="2.3988426" />
<ellipse
cy="275.02563"
cx="44.765343"
id="circle4570"
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
rx="2.4216392"
ry="2.3988426" />
<g
id="g1705">
<rect
transform="matrix(0.99967585,0.02545985,-0.02594573,0.99966335,0,0)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="10.338468"
height="23.042864"
x="23.424755"
y="259.99872" />
</g>
<rect
transform="matrix(0.99837418,-0.05699994,0.05808481,0.99831165,0,0)"
y="261.80557"
x="10.394932"
height="23.043449"
width="10.33821"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.03992438px;line-height:125%;font-family:'Liberation Serif';-inkscape-font-specification:'Liberation Serif';letter-spacing:0px;word-spacing:0px;fill:#e6e6e6;fill-opacity:1;stroke:none;stroke-width:0.264584px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="16.934374"
y="288.46368"
id="text1730"
transform="rotate(-1.2296789)"><tspan
sodipodi:role="line"
id="tspan1728"
x="16.934374"
y="288.46368"
style="fill:#e6e6e6;fill-opacity:1;stroke-width:0.264584px">dizqueTV</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1920"
height="1080"
viewBox="0 0 507.99999 285.75001"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="generic-music-screen.svg"
inkscape:export-filename="/home/vx/dev/pseudotv/resources/generic-music-screen.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.4585776"
inkscape:cx="925.75604"
inkscape:cy="448.17449"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-11.249983)">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.20000029;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect836"
width="508"
height="285.75"
x="0"
y="11.249983" />
<g
id="g6050"
transform="translate(-8.4960767,30.053154)">
<rect
transform="rotate(0.52601418)"
y="85.000603"
x="214.56714"
height="73.832573"
width="32.814484"
id="rect4518"
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
transform="rotate(1.4727575)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="32.81448"
height="73.832573"
x="248.74632"
y="80.901688" />
<rect
transform="rotate(-3.2986121)"
y="103.78287"
x="269.35843"
height="73.832588"
width="32.814476"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:76.95687866px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="147.14322"
y="234.94209"
id="text838"
transform="scale(1.3642872,0.73298349)"><tspan
sodipodi:role="line"
id="tspan836"
x="147.14322"
y="234.94209"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:KacstPoster;-inkscape-font-specification:KacstPoster;stroke-width:0.26458335px">♪</tspan></text>
<ellipse
style="opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.52916664;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path840"
cx="240.60326"
cy="169.0907"
rx="15.090722"
ry="15.089045" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

107
src/svg/loading-screen.svg Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1920"
height="1080"
viewBox="0 0 507.99999 285.75001"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="loading-screen.svg"
inkscape:export-filename="/home/vx/dev/pseudotv/resources/loading-screen.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.37433674"
inkscape:cx="1004.7641"
inkscape:cy="545.11626"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-11.249983)">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.20000029;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect836"
width="508"
height="285.75"
x="0"
y="11.249983" />
<g
id="g6050"
transform="translate(-8.4960767,30.053154)">
<rect
transform="rotate(0.52601418)"
y="85.000603"
x="214.56714"
height="73.832573"
width="32.814484"
id="rect4518"
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
transform="rotate(1.4727575)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="32.81448"
height="73.832573"
x="248.74632"
y="80.901688" />
<rect
transform="rotate(-3.2986121)"
y="103.78287"
x="269.35843"
height="73.832588"
width="32.814476"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="228.70648"
y="210.99644"
id="text939"><tspan
sodipodi:role="line"
id="tspan937"
x="228.70648"
y="210.99644"
style="fill:#f9f9f9;stroke-width:0.26458332px">Loading...</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

64
src/throttler.js Normal file
View File

@ -0,0 +1,64 @@
let constants = require('./constants');
let cache = {}
function equalItems(a, b) {
if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) {
return false;
}
if (
(a.type === "loading") || (a.type === "interlude")
|| (b.type === "loading") || (b.type === "interlude")
) {
return (a.type === b.type);
}
if (a.type != b.type) {
return false;
}
if (a.type !== "program") {
console.log("no idea how to compare this: " + JSON.stringify(a).slice(0,100) );
console.log(" with this: " + JSON.stringify(b).slice(0,100) );
}
return a.title === b.title;
}
function wereThereTooManyAttempts(sessionId, lineupItem) {
let t1 = (new Date()).getTime();
let previous = cache[sessionId];
let result = false;
if (typeof(previous) === 'undefined') {
previous = cache[sessionId] = {
t0: t1 - constants.TOO_FREQUENT * 5,
lineupItem: null,
};
} else if (t1 - previous.t0 < constants.TOO_FREQUENT) {
//certainly too frequent
result = equalItems( previous.lineupItem, lineupItem );
}
cache[sessionId] = {
t0: t1,
lineupItem : lineupItem,
};
setTimeout( () => {
if (
(typeof(cache[sessionId]) !== 'undefined')
&&
(cache[sessionId].t0 === t1)
) {
delete cache[sessionId];
}
}, constants.TOO_FREQUENT * 5 );
return result;
}
module.exports = wereThereTooManyAttempts;

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,45 @@
const XMLWriter = require('xml-writer')
const fs = require('fs')
const helperFuncs = require('./helperFuncs')
module.exports = { WriteXMLTV: WriteXMLTV }
module.exports = { WriteXMLTV: WriteXMLTV, shutdown: shutdown }
function WriteXMLTV(channels, xmlSettings) {
let isShutdown = false;
let isWorking = false;
async function WriteXMLTV(json, xmlSettings, throttle, cacheImageService) {
if (isShutdown) {
return;
}
if (isWorking) {
console.log("Concurrent xmltv write attempt detected, skipping");
return;
}
isWorking = true;
try {
await writePromise(json, xmlSettings, throttle, cacheImageService);
} catch (err) {
console.error("Error writing xmltv", err);
}
isWorking = false;
}
function writePromise(json, xmlSettings, throttle, cacheImageService) {
return new Promise((resolve, reject) => {
let date = new Date()
let ws = fs.createWriteStream(xmlSettings.file)
let xw = new XMLWriter(true, (str, enc) => ws.write(str, enc))
ws.on('close', () => { resolve() })
ws.on('error', (err) => { reject(err) })
_writeDocStart(xw)
async function middle() {
if (channels.length === 0) { // Write Dummy PseudoTV Channel if no channel exists
_writeChannels(xw, [{ number: 1, name: "PseudoTV", icon: "https://raw.githubusercontent.com/DEFENDORe/pseudotv/master/resources/pseudotv.png" }])
let program = {
program: {
type: 'movie',
title: 'No Channels Configured',
summary: 'Configure your channels using the PseudoTV Web UI.'
},
channel: '1',
start: date,
stop: new Date(date.valueOf() + xmlSettings.cache * 60 * 60 * 1000)
}
await _writeProgramme(xw, program)
} else {
_writeChannels(xw, channels)
for (let i = 0; i < channels.length; i++) {
await _writePrograms(xw, channels[i], date, xmlSettings.cache)
async function middle() {
let channelNumbers = [];
Object.keys(json).forEach( (key, index) => channelNumbers.push(key) );
let channels = channelNumbers.map( (number) => json[number].channel );
_writeChannels( xw, channels );
for (let i = 0; i < channelNumbers.length; i++) {
let number = channelNumbers[i];
await _writePrograms(xw, json[number].channel, json[number].programs, throttle, xmlSettings, cacheImageService);
}
}
}
middle().then( () => {
_writeDocEnd(xw, ws)
}).catch( (err) => {
@ -44,7 +51,7 @@ function WriteXMLTV(channels, xmlSettings) {
function _writeDocStart(xw) {
xw.startDocument()
xw.startElement('tv')
xw.writeAttribute('generator-info-name', 'psuedotv-plex')
xw.writeAttribute('generator-info-name', 'dizquetv')
}
function _writeDocEnd(xw, ws) {
xw.endElement()
@ -68,87 +75,99 @@ function _writeChannels(xw, channels) {
}
}
async function _writePrograms(xw, channel, date, cache) {
let prog = helperFuncs.getCurrentProgramAndTimeElapsed(date, channel)
let cutoff = new Date((date.valueOf() - prog.timeElapsed) + (cache * 60 * 60 * 1000))
let temp = new Date(date.valueOf() - prog.timeElapsed)
if (channel.programs.length === 0)
return
let i = prog.programIndex
for (; temp < cutoff;) {
await _throttle(); //let's not block for this process
let program = {
program: channel.programs[i],
channel: channel.number,
start: new Date(temp.valueOf()),
stop: new Date(temp.valueOf() + channel.programs[i].duration)
async function _writePrograms(xw, channel, programs, throttle, xmlSettings, cacheImageService) {
for (let i = 0; i < programs.length; i++) {
if (! isShutdown) {
await throttle();
}
_writeProgramme(xw, program)
temp.setMilliseconds(temp.getMilliseconds() + channel.programs[i].duration)
i++
if (i >= channel.programs.length)
i = 0
await _writeProgramme(channel, programs[i], xw, xmlSettings, cacheImageService);
}
}
async function _writeProgramme(xw, program) {
if (program.program.isOffline === true) {
//do not write anything for the offline period
return;
}
async function _writeProgramme(channel, program, xw, xmlSettings, cacheImageService) {
// Programme
xw.startElement('programme')
xw.writeAttribute('start', _createXMLTVDate(program.start))
xw.writeAttribute('stop', _createXMLTVDate(program.stop))
xw.writeAttribute('channel', program.channel)
xw.writeAttribute('stop', _createXMLTVDate(program.stop ))
xw.writeAttribute('channel', channel.number)
// Title
xw.startElement('title')
xw.writeAttribute('lang', 'en')
xw.text(program.title);
xw.endElement();
xw.writeRaw('\n <previously-shown/>')
if (program.program.type === 'episode') {
xw.text(program.program.showTitle)
xw.endElement()
xw.writeRaw('\n <previously-shown/>')
// Sub-Title
//sub-title
// TODO: Add support for track data (artist, album) here
if ( typeof(program.sub) !== 'undefined') {
xw.startElement('sub-title')
xw.writeAttribute('lang', 'en')
xw.text(program.program.title)
xw.text(program.sub.title)
xw.endElement()
// Episode-Number
xw.startElement('episode-num')
xw.writeAttribute('system', 'onscreen')
xw.text( "S" + (program.sub.season) + ' E' + (program.sub.episode) )
xw.endElement()
xw.startElement('episode-num')
xw.writeAttribute('system', 'xmltv_ns')
xw.text((program.program.season - 1) + ' . ' + (program.program.episode - 1) + ' . 0/1')
xw.endElement()
} else {
xw.text(program.program.title)
xw.text((program.sub.season - 1) + '.' + (program.sub.episode - 1) + '.0/1')
xw.endElement()
}
// Icon
if (typeof program.program.icon !== 'undefined') {
xw.startElement('icon')
xw.writeAttribute('src', program.program.icon)
xw.endElement()
if (typeof program.icon !== 'undefined') {
xw.startElement('icon');
let icon = program.icon;
if (xmlSettings.enableImageCache === true) {
const imgUrl = cacheImageService.registerImageOnDatabase(icon);
icon = `{{host}}/cache/images/${imgUrl}`;
}
xw.writeAttribute('src', icon);
xw.endElement();
}
// Desc
xw.startElement('desc')
xw.writeAttribute('lang', 'en')
xw.text(program.program.summary)
if ( (typeof(program.summary) !== 'undefined') && (program.summary.length > 0) ) {
xw.text(program.summary)
} else {
xw.text(channel.name)
}
xw.endElement()
// Rating
if (typeof program.program.rating !== 'undefined') {
if ( (program.rating != null) && (typeof program.rating !== 'undefined') ) {
xw.startElement('rating')
xw.writeAttribute('system', 'MPAA')
xw.writeElement('value', program.program.rating)
xw.writeElement('value', program.rating)
xw.endElement()
}
// End of Programme
xw.endElement()
}
function _createXMLTVDate(d) {
return d.toISOString().substring(0,19).replace(/[-T:]/g,"") + " +0000";
return d.substring(0,19).replace(/[-T:]/g,"") + " +0000";
}
function _throttle() {
function wait(x) {
return new Promise((resolve) => {
setTimeout(resolve, 0);
setTimeout(resolve, x);
});
}
}
async function shutdown() {
isShutdown = true;
console.log("Shutting down xmltv writer.");
if (isWorking) {
let s = "Wait for xmltv writer...";
while (isWorking) {
console.log(s);
await wait(100);
s = "Still waiting for xmltv writer...";
}
console.log("Write finished.");
} else {
console.log("xmltv writer had no pending jobs.");
}
}

View File

@ -3,11 +3,40 @@ require('angular-router-browserify')(angular)
require('./ext/lazyload')(angular)
require('./ext/dragdrop')
require('./ext/angularjs-scroll-glue')
require('angular-vs-repeat');
require('angular-sanitize');
const i18next = require('i18next');
const i18nextHttpBackend = require('i18next-http-backend');
window.i18next = i18next;
var app = angular.module('myApp', ['ngRoute', 'angularLazyImg', 'dndLists', 'luegg.directives'])
window.i18next.use(i18nextHttpBackend);
window.i18next.init({
// debug: true,
lng: 'en',
fallbackLng: 'en',
preload: ['en'],
ns: [ 'main' ],
defaultNS: [ 'main' ],
initImmediate: false,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json'
},
useCookie: false,
useLocalStorage: false,
}, function (err, t) {
console.log('resources loaded');
});
require('ng-i18next');
var app = angular.module('myApp', ['ngRoute', 'vs-repeat', 'angularLazyImg', 'dndLists', 'luegg.directives', 'jm.i18next'])
app.service('plex', require('./services/plex'))
app.service('pseudotv', require('./services/pseudotv'))
app.service('dizquetv', require('./services/dizquetv'))
app.service('resolutionOptions', require('./services/resolution-options'))
app.service('getShowData', require('./services/get-show-data'))
app.service('commonProgramTools', require('./services/common-program-tools'))
app.directive('plexSettings', require('./directives/plex-settings'))
app.directive('ffmpegSettings', require('./directives/ffmpeg-settings'))
@ -15,12 +44,28 @@ app.directive('xmltvSettings', require('./directives/xmltv-settings'))
app.directive('hdhrSettings', require('./directives/hdhr-settings'))
app.directive('plexLibrary', require('./directives/plex-library'))
app.directive('programConfig', require('./directives/program-config'))
app.directive('offlineConfig', require('./directives/offline-config'))
app.directive('flexConfig', require('./directives/flex-config'))
app.directive('timeSlotsTimeEditor', require('./directives/time-slots-time-editor'))
app.directive('toastNotifications', require('./directives/toast-notifications'))
app.directive('fillerConfig', require('./directives/filler-config'))
app.directive('showConfig', require('./directives/show-config'))
app.directive('deleteFiller', require('./directives/delete-filler'))
app.directive('frequencyTweak', require('./directives/frequency-tweak'))
app.directive('removeShows', require('./directives/remove-shows'))
app.directive('channelRedirect', require('./directives/channel-redirect'))
app.directive('plexServerEdit', require('./directives/plex-server-edit'))
app.directive('channelConfig', require('./directives/channel-config'))
app.directive('timeSlotsScheduleEditor', require('./directives/time-slots-schedule-editor'))
app.directive('randomSlotsScheduleEditor', require('./directives/random-slots-schedule-editor'))
app.controller('settingsCtrl', require('./controllers/settings'))
app.controller('channelsCtrl', require('./controllers/channels'))
app.controller('versionCtrl', require('./controllers/version'))
app.controller('libraryCtrl', require('./controllers/library'))
app.controller('guideCtrl', require('./controllers/guide'))
app.controller('playerCtrl', require('./controllers/player'))
app.controller('fillerCtrl', require('./controllers/filler'))
app.controller('customShowsCtrl', require('./controllers/custom-shows'))
app.config(function ($routeProvider) {
$routeProvider
@ -32,11 +77,31 @@ app.config(function ($routeProvider) {
templateUrl: "views/channels.html",
controller: 'channelsCtrl'
})
.when("/filler", {
templateUrl: "views/filler.html",
controller: 'fillerCtrl'
})
.when("/custom-shows", {
templateUrl: "views/custom-shows.html",
controller: 'customShowsCtrl'
})
.when("/library", {
templateUrl: "views/library.html",
controller: 'libraryCtrl'
})
.when("/guide", {
templateUrl: "views/guide.html",
controller: 'guideCtrl'
})
.when("/player", {
templateUrl: "views/player.html",
controller: 'playerCtrl'
})
.when("/version", {
templateUrl: "views/version.html",
controller: 'versionCtrl'
})
.otherwise({
redirectTo: "channels"
redirectTo: "guide"
})
})

View File

@ -1,43 +1,98 @@
module.exports = function ($scope, pseudotv) {
module.exports = function ($scope, dizquetv) {
$scope.channels = []
$scope.showChannelConfig = false
$scope.selectedChannel = null
$scope.selectedChannelIndex = -1
pseudotv.getChannels().then((channels) => {
$scope.channels = channels
})
$scope.removeChannel = (channel) => {
if (confirm("Are you sure to delete channel: " + channel.name + "?")) {
pseudotv.removeChannel(channel).then((channels) => {
$scope.channels = channels
})
$scope.refreshChannels = async () => {
$scope.channels = [ { number: 1, pending: true} ]
let channelNumbers = await dizquetv.getChannelNumbers();
$scope.channels = channelNumbers.map( (x) => {
return {
number: x,
pending: true,
}
});
$scope.$apply();
$scope.queryChannels();
}
$scope.refreshChannels();
$scope.queryChannels = () => {
for (let i = 0; i < $scope.channels.length; i++) {
$scope.queryChannel(i, $scope.channels[i] );
}
}
$scope.onChannelConfigDone = (channel) => {
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.channels[index] = ch;
$scope.$apply();
}
$scope.removeChannel = async ($index, channel) => {
if (confirm("Are you sure to delete channel: " + channel.name + "?")) {
$scope.channels[$index].pending = true;
await dizquetv.removeChannel(channel);
$scope.refreshChannels();
}
}
$scope.onChannelConfigDone = async (channel) => {
if ($scope.selectedChannelIndex != -1) {
$scope.channels[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof channel !== 'undefined') {
if ($scope.selectedChannelIndex == -1) { // add new channel
pseudotv.addChannel(channel).then((channels) => {
$scope.channels = channels
})
await dizquetv.addChannel(channel);
$scope.showChannelConfig = false
$scope.refreshChannels();
} else if (
(typeof($scope.originalChannelNumber) !== 'undefined')
&& ($scope.originalChannelNumber != channel.number)
) {
//update + change channel number.
$scope.channels[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateChannel(channel),
await dizquetv.removeChannel( { number: $scope.originalChannelNumber } )
$scope.showChannelConfig = false
$scope.$apply();
$scope.refreshChannels();
} else { // update existing channel
pseudotv.updateChannel(channel).then((channels) => {
$scope.channels = channels
})
$scope.channels[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateChannel(channel);
$scope.showChannelConfig = false
$scope.$apply();
$scope.refreshChannels();
}
} else {
$scope.showChannelConfig = false
}
$scope.showChannelConfig = false
}
$scope.selectChannel = (index) => {
if (index === -1) {
$scope.selectChannel = async (index) => {
if ( (index === -1) || $scope.channels[index].pending ) {
$scope.originalChannelNumber = undefined;
$scope.selectedChannel = null
$scope.selectedChannelIndex = -1
$scope.showChannelConfig = true
} else {
let newObj = JSON.parse(angular.toJson($scope.channels[index]))
$scope.channels[index].pending = true;
let p = await Promise.all([
dizquetv.getChannelProgramless($scope.channels[index].number),
dizquetv.getChannelPrograms($scope.channels[index].number),
]);
let ch = p[0];
ch.programs = p[1];
let newObj = ch;
newObj.startTime = new Date(newObj.startTime)
$scope.originalChannelNumber = newObj.number;
$scope.selectedChannel = newObj
$scope.selectedChannelIndex = index
$scope.showChannelConfig = true
$scope.$apply();
}
$scope.showChannelConfig = true
}
}

View File

@ -0,0 +1,94 @@
module.exports = function ($scope, $timeout, dizquetv) {
$scope.showss = []
$scope.showShowConfig = false
$scope.selectedShow = null
$scope.selectedShowIndex = -1
$scope.refreshShow = async () => {
$scope.shows = [ { id: '?', pending: true} ]
$timeout();
let shows = await dizquetv.getAllShowsInfo();
shows.sort( (a,b) => {
return a.name > b.name;
} );
$scope.shows = shows;
$timeout();
}
$scope.refreshShow();
let feedToShowConfig = () => {};
let feedToDeleteShow = feedToShowConfig;
$scope.registerShowConfig = (feed) => {
feedToShowConfig = feed;
}
$scope.registerDeleteShow = (feed) => {
feedToDeleteShow = feed;
}
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.shows[index] = ch;
$scope.$apply();
}
$scope.onShowConfigDone = async (show) => {
if ($scope.selectedChannelIndex != -1) {
$scope.shows[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof show !== 'undefined') {
// not canceled
if ($scope.selectedChannelIndex == -1) { // add new channel
await dizquetv.createShow(show);
} else {
$scope.shows[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateShow(show.id, show);
}
await $scope.refreshShow();
}
}
$scope.selectShow = async (index) => {
try {
if ( (index != -1) && $scope.shows[index].pending) {
return;
}
$scope.selectedChannelIndex = index;
if (index === -1) {
feedToShowConfig();
} else {
$scope.shows[index].pending = true;
let f = await dizquetv.getShow($scope.shows[index].id);
feedToShowConfig(f);
$timeout();
}
} catch( err ) {
console.error("Could not fetch show.", err);
}
}
$scope.deleteShow = async (index) => {
try {
if ( $scope.shows[index].pending) {
return;
}
let show = $scope.shows[index];
if (confirm("Are you sure to delete show: " + show.name + "? This will NOT delete the show's programs from channels that are using.")) {
show.pending = true;
await dizquetv.deleteShow(show.id);
$timeout();
await $scope.refreshShow();
$timeout();
}
} catch (err) {
console.error("Could not delete show.", err);
}
}
}

108
web/controllers/filler.js Normal file
View File

@ -0,0 +1,108 @@
module.exports = function ($scope, $timeout, dizquetv) {
$scope.fillers = []
$scope.showFillerConfig = false
$scope.selectedFiller = null
$scope.selectedFillerIndex = -1
$scope.refreshFiller = async () => {
$scope.fillers = [ { id: '?', pending: true} ]
$timeout();
let fillers = await dizquetv.getAllFillersInfo();
fillers.sort( (a,b) => {
return a.name > b.name;
} );
$scope.fillers = fillers;
$timeout();
}
$scope.refreshFiller();
let feedToFillerConfig = () => {};
let feedToDeleteFiller = feedToFillerConfig;
$scope.registerFillerConfig = (feed) => {
feedToFillerConfig = feed;
}
$scope.registerDeleteFiller = (feed) => {
feedToDeleteFiller = feed;
}
$scope.queryChannel = async (index, channel) => {
let ch = await dizquetv.getChannelDescription(channel.number);
ch.pending = false;
$scope.fillers[index] = ch;
$scope.$apply();
}
$scope.onFillerConfigDone = async (filler) => {
if ($scope.selectedChannelIndex != -1) {
$scope.fillers[ $scope.selectedChannelIndex ].pending = false;
}
if (typeof filler !== 'undefined') {
// not canceled
if ($scope.selectedChannelIndex == -1) { // add new channel
await dizquetv.createFiller(filler);
} else {
$scope.fillers[ $scope.selectedChannelIndex ].pending = true;
await dizquetv.updateFiller(filler.id, filler);
}
await $scope.refreshFiller();
}
}
$scope.selectFiller = async (index) => {
try {
if ( (index != -1) && $scope.fillers[index].pending) {
return;
}
$scope.selectedChannelIndex = index;
if (index === -1) {
feedToFillerConfig();
} else {
$scope.fillers[index].pending = true;
let f = await dizquetv.getFiller($scope.fillers[index].id);
feedToFillerConfig(f);
$timeout();
}
} catch( err ) {
console.error("Could not fetch filler.", err);
}
}
$scope.deleteFiller = async (index) => {
try {
if ( $scope.fillers[index].pending) {
return;
}
$scope.deleteFillerIndex = index;
$scope.fillers[index].pending = true;
let id = $scope.fillers[index].id;
let channels = await dizquetv.getChannelsUsingFiller(id);
feedToDeleteFiller( {
id: id,
name: $scope.fillers[index].name,
channels : channels,
} );
$timeout();
} catch (err) {
console.error("Could not start delete filler dialog.", err);
}
}
$scope.onFillerDelete = async( id ) => {
try {
$scope.fillers[ $scope.deleteFillerIndex ].pending = false;
$timeout();
if (typeof(id) !== 'undefined') {
$scope.fillers[ $scope.deleteFillerIndex ].pending = true;
await dizquetv.deleteFiller(id);
$timeout();
await $scope.refreshFiller();
$timeout();
}
} catch (err) {
console.error("Error attempting to delete filler", err);
}
}
}

363
web/controllers/guide.js Normal file
View File

@ -0,0 +1,363 @@
const MINUTE = 60 * 1000;
module.exports = function ($scope, $timeout, dizquetv) {
$scope.offset = 0;
$scope.M = 60 * MINUTE;
$scope.zoomLevel = 3
$scope.T = 190 * MINUTE;
$scope.before = 15 * MINUTE;
$scope.enableNext = false;
$scope.enableBack = false;
$scope.showNow = false;
$scope.nowPosition = 0;
$scope.refreshHandle = null;
const intl = new Intl.DateTimeFormat('default',
{
hour12: true,
hour: 'numeric',
minute: 'numeric'
});
let hourMinute = (d) => {
return intl.format(d);
};
$scope.updateBasics = () => {
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
$scope.totalSpan = Math.floor($scope.T / MINUTE);
$scope.colspanPercent = (100 - $scope.channelWidth) / ($scope.T / MINUTE);
$scope.channelColspan = Math.floor($scope.channelWidth / $scope.colspanPercent);
$scope.channelNumberColspan = Math.floor($scope.channelNumberWidth / $scope.colspanPercent);
$scope.channelIconColspan = $scope.channelColspan - $scope.channelNumberColspan;
}
$scope.updateBasics();
$scope.channelNumberWidth = 5;
$scope.channelIconWidth = 8;
$scope.channelWidth = $scope.channelNumberWidth + $scope.channelIconWidth;
//we want 1 minute = 1 colspan
$scope.applyLater = () => {
$timeout( () => $scope.$apply(), 0 );
};
$scope.channelNumbers = [];
$scope.channels = {};
$scope.lastUpdate = -1;
$scope.updateJustNow = () => {
$scope.t1 = (new Date()).getTime();
if ($scope.t0 <= $scope.t1 && $scope.t1 < $scope.t0 + $scope.T) {
let n = ($scope.t1 - $scope.t0) / MINUTE;
$scope.nowPosition = ($scope.channelColspan + n) * $scope.colspanPercent
if ($scope.nowPosition >= 50 && $scope.offset >= 0) {
$scope.offset = 0;
$scope.adjustZoom();
}
$scope.showNow = true;
} else {
$scope.showNow = false;
}
}
$scope.nowTimer = () => {
$scope.updateJustNow();
$timeout( () => $scope.nowTimer() , 10000);
}
$timeout( () => $scope.nowTimer() , 10000);
$scope.refreshManaged = async (skipStatus) => {
$scope.t1 = (new Date()).getTime();
$scope.t1 = ($scope.t1 - $scope.t1 % MINUTE );
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
$scope.times = [];
$scope.updateJustNow();
let pending = 0;
let addDuration = (d) => {
let m = (pending + d) % MINUTE;
let r = (pending + d) - m;
pending = m;
return Math.floor( r / MINUTE );
}
let deleteIfZero = () => {
if ( $scope.times.length > 0 && $scope.times[$scope.times.length - 1].duration < 1) {
$scope.times = $scope.times.slice(0, $scope.times.length - 1);
}
}
let rem = $scope.T;
let t = $scope.t0;
if (t % $scope.M != 0) {
let dif = $scope.M - t % $scope.M;
$scope.times.push( {
duration : addDuration(dif),
} );
deleteIfZero();
t += dif;
rem -= dif;
}
while (rem > 0) {
let d = Math.min(rem, $scope.M );
$scope.times.push( {
duration : addDuration(d),
label: hourMinute( new Date(t) ),
} );
t += d;
rem -= d;
}
if (skipStatus !== true) {
$scope.channelNumbers = [0];
$scope.channels = {} ;
$scope.channels[0] = {
loading: true,
}
$scope.applyLater();
console.log("getting status...");
let status = await dizquetv.getGuideStatus();
$scope.lastUpdate = new Date(status.lastUpdate).getTime();
console.log("got status: " + JSON.stringify(status) );
$scope.channelNumbers = status.channelNumbers;
$scope.channels = {} ;
}
for (let i = 0; i < $scope.channelNumbers.length; i++) {
if ( typeof($scope.channels[$scope.channelNumbers[i]]) === 'undefined') {
$scope.channels[$scope.channelNumbers[i]] = {};
}
$scope.channels[$scope.channelNumbers[i]].loading = true;
}
$scope.applyLater();
$scope.enableBack = false;
$scope.enableNext = false;
await Promise.all($scope.channelNumbers.map( $scope.loadChannel) );
setupTimer();
};
let cancelTimerIfExists = () => {
if ($scope.refreshHandle != null) {
$timeout.cancel($scope.refreshHandle);
}
}
$scope.$on('$locationChangeStart', () => {
console.log("$locationChangeStart" );
cancelTimerIfExists();
} );
let setupTimer = () => {
cancelTimerIfExists();
$scope.refreshHandle = $timeout( () => $scope.checkUpdates(), 60000 );
}
$scope.adjustZoom = async() => {
switch ($scope.zoomLevel) {
case 1:
$scope.T = 50 * MINUTE;
$scope.M = 10 * MINUTE;
$scope.before = 5 * MINUTE;
break;
case 2:
$scope.T = 100 * MINUTE;
$scope.M = 15 * MINUTE;
$scope.before = 10 * MINUTE;
break;
case 3:
$scope.T = 190 * MINUTE;
$scope.M = 30 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 4:
$scope.T = 270 * MINUTE;
$scope.M = 60 * MINUTE;
$scope.before = 15 * MINUTE;
break;
case 5:
$scope.T = 380 * MINUTE;
$scope.M = 90 * MINUTE;
$scope.before = 15 * MINUTE;
break;
}
$scope.updateBasics();
await $scope.refresh(true);
}
$scope.zoomOut = async() => {
$scope.zoomLevel = Math.min( 5, $scope.zoomLevel + 1 );
await $scope.adjustZoom();
}
$scope.zoomIn = async() => {
$scope.zoomLevel = Math.max( 1, $scope.zoomLevel - 1 );
await $scope.adjustZoom();
}
$scope.zoomOutEnabled = () => {
return $scope.zoomLevel < 5;
}
$scope.zoomInEnabled = () => {
return $scope.zoomLevel > 1;
}
$scope.next = async() => {
$scope.offset += $scope.M * 7 / 8
await $scope.adjustZoom();
}
$scope.back = async() => {
$scope.offset -= $scope.M * 7 / 8
await $scope.adjustZoom();
}
$scope.backEnabled = () => {
return $scope.enableBack;
}
$scope.nextEnabled = () => {
return $scope.enableNext;
}
$scope.loadChannel = async (number) => {
console.log(`number=${number}` );
let d0 = new Date($scope.t0);
let d1 = new Date($scope.t0 + $scope.T);
let lineup = await dizquetv.getChannelLineup(number, d0, d1);
let ch = {
icon : lineup.icon,
number : lineup.number,
name: lineup.name,
altTitle: `${lineup.number} - ${lineup.name}`,
programs: [],
};
let pending = 0;
let totalAdded = 0;
let addDuration = (d) => {
totalAdded += d;
let m = (pending + d) % MINUTE;
let r = (pending + d) - m;
pending = m;
return Math.floor( r / MINUTE );
}
let deleteIfZero = () => {
if ( ch.programs.length > 0 && ch.programs[ ch.programs.length - 1].duration < 1) {
ch.programs = ch.programs.slice(0, ch.programs.length - 1);
}
}
for (let i = 0; i < lineup.programs.length; i++) {
let program = lineup.programs[i];
let ad = new Date(program.start);
let bd = new Date(program.stop);
let a = ad.getTime();
let b = bd.getTime();
let hasStart = true;
let hasStop = true;
if (a < $scope.t0) {
//cut-off
a = $scope.t0;
hasStart = false;
$scope.enableBack = true;
} else if ( (a > $scope.t0) && (i == 0) ) {
ch.programs.push( {
duration: addDuration( (a - $scope.t0) ),
showTitle: "",
start: false,
end: true,
} );
deleteIfZero();
}
if (b > $scope.t0 + $scope.T) {
b = $scope.t0 + $scope.T;
hasStop = false;
$scope.enableNext = true;
}
let subTitle = undefined;
let episodeTitle = undefined;
let altTitle = hourMinute(ad) + "-" + hourMinute(bd);
if (typeof(program.title) !== 'undefined') {
altTitle = altTitle + " · " + program.title;
}
if (typeof(program.sub) !== 'undefined') {
ps = "" + program.sub.season;
if (ps.length < 2) {
ps = "0" + ps;
}
pe = "" + program.sub.episode;
if (pe.length < 2) {
pe = "0" + pe;
}
subTitle = `S${ps} · E${pe}`;
altTitle = altTitle + " " + subTitle;
episodeTitle = program.sub.title;
} else if ( typeof(program.date) === 'undefined' ) {
subTitle = '.';
} else {
subTitle = program.date.slice(0,4);
}
ch.programs.push( {
duration: addDuration(b - a),
altTitle: altTitle,
showTitle: program.title, // movie title, episode title or track title
subTitle: subTitle,
episodeTitle : episodeTitle,
start: hasStart,
end: hasStop,
} );
deleteIfZero();
}
if (totalAdded < $scope.T) {
ch.programs.push( {
duration: addDuration( $scope.T - totalAdded ),
showTitle: "",
start: false,
end: true,
} );
deleteIfZero();
}
$scope.channels[number] = ch;
$scope.applyLater();
}
$scope.refresh = async (skipStatus) => {
try {
await $scope.refreshManaged(skipStatus);
} catch (err) {
console.error("Refresh failed?", err);
}
}
$scope.adjustZoom();
$scope.refresh();
$scope.checkUpdates = async () => {
try {
console.log("get status " + new Date() );
let status = await dizquetv.getGuideStatus();
let t = new Date(status.lastUpdate).getTime();
if ( t > $scope.lastUpdate) {
$scope.refreshManaged();
} else {
setupTimer();
}
} catch(err) {
console.error(err);
}
};
}

View File

@ -0,0 +1,2 @@
module.exports = function () {
}

73
web/controllers/player.js Normal file
View File

@ -0,0 +1,73 @@
module.exports = function ($scope, dizquetv, $timeout) {
$scope.loading = true;
$scope.channelOptions = [
{ id: undefined, description: "Select a channel" },
];
$scope.icons = {};
$scope.endpointOptions = [
{ id: "video", description: "/video - Channel mpegts" },
{ id: "m3u8", description: "/m3u8 - Playlist of individual videos" },
{ id: "radio", description: "/radio - Audio-only channel mpegts" },
];
$scope.selectedEndpoint = "video";
$scope.channel = undefined;
$scope.endpointButtonHref = () => {
if ( $scope.selectedEndpoint == "video") {
return `./media-player/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "m3u8") {
return `./media-player/fast/${$scope.channel}.m3u`
} else if ( $scope.selectedEndpoint == "radio") {
return `./media-player/radio/${$scope.channel}.m3u`
}
}
$scope.buttonDisabled = () => {
return typeof($scope.channel) === 'undefined';
}
$scope.endpoint = () => {
if ( typeof($scope.channel) === 'undefined' ) {
return "--"
}
let path = "";
if ( $scope.selectedEndpoint == "video") {
path = `/video?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "m3u8") {
path = `/m3u8?channel=${$scope.channel}`
} else if ( $scope.selectedEndpoint == "radio") {
path= `/radio?channel=${$scope.channel}`
}
return window.location.href.replace("/#!/player", path);
}
let loadChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
$scope.channelOptions.push( option );
$scope.icons[x] = desc.icon;
}) );
$scope.channelOptions.sort( (a,b) => {
let za = ( (typeof(a.id) === undefined)?-1:a.id);
let zb = ( (typeof(b.id) === undefined)?-1:b.id);
return za - zb;
} );
$scope.loading = false;
$scope.$apply();
} catch (err) {
console.error(err);
}
$timeout( () => $scope.$apply(), 0);
}
loadChannels();
}

View File

@ -1,7 +1,10 @@
module.exports = function ($scope, pseudotv) {
$scope.version = "Getting PseudoTV version..."
pseudotv.getVersion().then((version) => {
$scope.version = version.pseudotv
module.exports = function ($scope, dizquetv) {
$scope.version = ""
$scope.ffmpegVersion = ""
dizquetv.getVersion().then((version) => {
$scope.version = version.dizquetv;
$scope.ffmpegVersion = version.ffmpeg;
$scope.nodejs = version.nodejs;
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
module.exports = function ($timeout, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/channel-redirect.html',
replace: true,
scope: {
formTitle: "=formTitle",
visible: "=visible",
program: "=program",
_onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.error = "";
scope.options = [];
scope.loading = true;
scope.$watch('program', () => {
if (typeof(scope.program) === 'undefined') {
return;
}
if ( isNaN(scope.program.duration) ) {
scope.program.duration = 15000;
}
scope.durationSeconds = Math.ceil( scope.program.duration / 1000.0 );;
})
scope.refreshChannels = async() => {
let channelNumbers = await dizquetv.getChannelNumbers();
try {
await Promise.all( channelNumbers.map( async(x) => {
let desc = await dizquetv.getChannelDescription(x);
let option = {
id: x,
description: `${x} - ${desc.name}`,
};
let i = 0;
while (i < scope.options.length) {
if (scope.options[i].id == x) {
scope.options[i] = option;
break;
}
i++;
}
if (i == scope.options.length) {
scope.options.push(option);
}
scope.$apply();
}) );
} catch (err) {
console.error(err);
}
scope.options.sort( (a,b) => a.id - b.id );
scope.loading = false;
$timeout( () => scope.$apply(), 0);
};
scope.refreshChannels();
scope.onCancel = () => {
scope.visible = false;
}
scope.onDone = () => {
scope.error = "";
if (typeof(scope.program.channel) === 'undefined') {
scope.error = "Please select a channel.";
}
if ( isNaN(scope.program.channel) ) {
scope.error = "Channel must be a number.";
}
if ( isNaN(scope.durationSeconds) ) {
scope.error = "Duration must be a number.";
}
if ( scope.error != "" ) {
$timeout( () => scope.error = "", 60000);
return;
}
scope.program.duration = scope.durationSeconds * 1000;
scope._onDone( scope.program );
scope.visible = false;
};
}
};
}

View File

@ -0,0 +1,32 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/delete-filler.html',
replace: true,
scope: {
linker: "=linker",
onExit: "=onExit"
},
link: function (scope, element, attrs) {
scope.name = '';
scope.channels = [];
scope.visible = false;
scope.linker( (filler) => {
scope.name = filler.name;
scope.id = filler.id;
scope.channels = filler.channels;
scope.visible = true;
} );
scope.finished = (cancelled) => {
scope.visible = false;
if (! cancelled) {
scope.onExit( scope.id );
} else {
scope.onExit();
}
}
}
};
}

View File

@ -1,4 +1,4 @@
module.exports = function (pseudotv) {
module.exports = function (dizquetv, resolutionOptions) {
return {
restrict: 'E',
templateUrl: 'templates/ffmpeg-settings.html',
@ -6,16 +6,22 @@
scope: {
},
link: function (scope, element, attrs) {
pseudotv.getFfmpegSettings().then((settings) => {
//add validations to ffmpeg settings, speciall commas in codec name
dizquetv.getFfmpegSettings().then((settings) => {
scope.settings = settings
})
scope.updateSettings = (settings) => {
pseudotv.updateFfmpegSettings(settings).then((_settings) => {
delete scope.settingsError;
dizquetv.updateFfmpegSettings(settings).then((_settings) => {
scope.settings = _settings
}).catch( (err) => {
if ( typeof(err.data) === "string") {
scope.settingsError = err.data;
}
})
}
scope.resetSettings = (settings) => {
pseudotv.resetFfmpegSettings(settings).then((_settings) => {
dizquetv.resetFfmpegSettings(settings).then((_settings) => {
scope.settings = _settings
})
}
@ -25,17 +31,7 @@
scope.hideIfNotAutoPlay = () => {
return scope.settings.enableAutoPlay != true
};
scope.resolutionOptions=[
{id:"420x420",description:"420x420 (1:1)"},
{id:"576x320",description:"576x320 (18:10)"},
{id:"640×360",description:"640×360 (nHD 16:9)"},
{id:"720x480",description:"720x480 (WVGA 3:2)"},
{id:"800x600",description:"800x600 (SVGA 4:3)"},
{id:"1024x768",description:"1024x768 (WXGA 4:3)"},
{id:"1280x720",description:"1280x720 (HD 16:9)"},
{id:"1920x1080",description:"1920x1080 (FHD 16:9)"},
{id:"3840x2160",description:"3840x2160 (4K 16:9)"},
];
scope.resolutionOptions= resolutionOptions.get();
scope.muxDelayOptions=[
{id:"0",description:"0 Seconds"},
{id:"1",description:"1 Seconds"},
@ -58,6 +54,32 @@
{value:"sine", description:"Beep"},
{value:"silent", description:"No Audio"},
]
scope.fpsOptions = [
{id: 23.976, description: "23.976 frames per second"},
{id: 24, description: "24 frames per second"},
{id: 25, description: "25 frames per second"},
{id: 29.97, description: "29.97 frames per second"},
{id: 30, description: "30 frames per second"},
{id: 50, description: "50 frames per second"},
{id: 59.94, description: "59.94 frames per second"},
{id: 60, description: "60 frames per second"},
{id: 120, description: "120 frames per second"},
];
scope.scalingOptions = [
{id: "bicubic", description: "bicubic (default)"},
{id: "fast_bilinear", description: "fast_bilinear"},
{id: "lanczos", description: "lanczos"},
{id: "spline", description: "spline"},
];
scope.deinterlaceOptions = [
{value: "none", description: "do not deinterlace"},
{value: "bwdif=0", description: "bwdif send frame"},
{value: "bwdif=1", description: "bwdif send field"},
{value: "w3fdif", description: "w3fdif"},
{value: "yadif=0", description: "yadif send frame"},
{value: "yadif=1", description: "yadif send field"}
];
}
}
}

View File

@ -0,0 +1,198 @@
module.exports = function ($timeout, commonProgramTools, getShowData) {
return {
restrict: 'E',
templateUrl: 'templates/filler-config.html',
replace: true,
scope: {
linker: "=linker",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showTools = false;
scope.showPlexLibrary = false;
scope.content = [];
scope.visible = false;
scope.error = undefined;
function refreshContentIndexes() {
for (let i = 0; i < scope.content.length; i++) {
scope.content[i].$index = i;
}
}
scope.contentSplice = (a,b) => {
scope.content.splice(a,b)
refreshContentIndexes();
}
scope.dropFunction = (dropIndex, program) => {
let y = program.$index;
let z = dropIndex + scope.currentStartIndex - 1;
scope.content.splice(y, 1);
if (z >= y) {
z--;
}
scope.content.splice(z, 0, program );
refreshContentIndexes();
$timeout();
return false;
}
scope.setUpWatcher = function setupWatchers() {
this.$watch('vsRepeat.startIndex', function(val) {
scope.currentStartIndex = val;
});
};
scope.movedFunction = (index) => {
console.log("movedFunction(" + index + ")");
}
scope.linker( (filler) => {
if ( typeof(filler) === 'undefined') {
scope.name = "";
scope.content = [];
scope.id = undefined;
scope.title = "Create Filler List";
} else {
scope.name = filler.name;
scope.content = filler.content;
scope.id = filler.id;
scope.title = "Edit Filler List";
}
refreshContentIndexes();
scope.visible = true;
} );
scope.finished = (cancelled) => {
if (cancelled) {
scope.visible = false;
return scope.onDone();
}
if ( (typeof(scope.name) === 'undefined') || (scope.name.length == 0) ) {
scope.error = "Please enter a name";
}
if ( scope.content.length == 0) {
scope.error = "Please add at least one clip.";
}
if (typeof(scope.error) !== 'undefined') {
$timeout( () => {
scope.error = undefined;
}, 30000);
return;
}
scope.visible = false;
scope.onDone( {
name: scope.name,
content: scope.content.map( (c) => {
delete c.$index
return c;
} ),
id: scope.id,
} );
}
scope.getText = (clip) => {
let show = getShowData(clip);
if (show.hasShow && show.showId !== "movie." ) {
return show.showDisplayName + " - " + clip.title;
} else {
return clip.title;
}
}
scope.showList = () => {
return ! scope.showPlexLibrary;
}
scope.sortFillersByLength = () => {
scope.content.sort( (a,b) => { return a.duration - b.duration } );
refreshContentIndexes();
}
scope.sortFillersCorrectly = () => {
scope.content = commonProgramTools.sortShows(scope.content);
refreshContentIndexes();
}
scope.fillerRemoveAllFiller = () => {
scope.content = [];
refreshContentIndexes();
}
scope.fillerRemoveDuplicates = () => {
function getKey(p) {
return p.serverKey + "|" + p.plexFile;
}
let seen = {};
let newFiller = [];
for (let i = 0; i < scope.content.length; i++) {
let p = scope.content[i];
let k = getKey(p);
if ( typeof(seen[k]) === 'undefined') {
seen[k] = true;
newFiller.push(p);
}
}
scope.content = newFiller;
refreshContentIndexes();
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
}
scope.content = scope.content.concat(selectedPrograms);
refreshContentIndexes();
scope.showPlexLibrary = false;
}
scope.durationString = (duration) => {
var date = new Date(0);
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
let interpolate = ( () => {
let h = 60*60*1000 / 6;
let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h];
let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0];
let n = ix.length;
return (x) => {
for (let i = 0; i < n-1; i++) {
if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) {
return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) );
}
}
}
} )();
scope.programSquareStyle = (program, dash) => {
let background = "rgb(255, 255, 255)";
let ems = Math.pow( Math.min(60*60*1000, program.duration), 0.7 );
ems = ems / Math.pow(1*60*1000., 0.7);
ems = Math.max( 0.25 , ems);
let top = Math.max(0.0, (1.75 - ems) / 2.0) ;
if (top == 0.0) {
top = "1px";
}
let solidOrDash = (dash? 'dashed' : 'solid');
let f = interpolate;
let w = 5.0;
let t = 4*60*60*1000;
let a = ( f(program.duration) *w) / f(t);
a = Math.min( w, Math.max(0.3, a) );
b = w - a + 0.01;
return {
'width': `${a}%`,
'height': '1.3em',
'margin-right': `${b}%`,
'background': background,
'border': `1px ${solidOrDash} black`,
'margin-top': top,
'margin-bottom': '1px',
};
}
}
};
}

View File

@ -0,0 +1,46 @@
module.exports = function ($timeout, dizquetv) {
return {
restrict: 'E',
templateUrl: 'templates/flex-config.html',
replace: true,
scope: {
title: "@offlineTitle",
program: "=program",
visible: "=visible",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
let updateNext = true;
scope.$watch('program', () => {
try {
if ( (typeof(scope.program) === 'undefined') || (scope.program == null) ) {
updateNext = true;
return;
} else if (! updateNext) {
return;
}
updateNext = false;
scope.error = null;
} catch (err) {
console.error(err);
}
})
scope.finished = (prog) => {
scope.error = null;
if (isNaN(prog.durationSeconds) || prog.durationSeconds < 0 ) {
scope.error = { duration: 'Duration must be a positive integer' }
}
if (scope.error != null) {
$timeout(() => {
scope.error = null
}, 30000)
return
}
scope.onDone(JSON.parse(angular.toJson(prog)))
scope.program = null
}
}
};
}

View File

@ -0,0 +1,24 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/frequency-tweak.html',
replace: true,
scope: {
programs: "=programs",
visible: "=visible",
onDone: "=onDone",
modified: "=modified",
message: "=message",
},
link: function (scope, element, attrs) {
scope.setModified = () => {
scope.modified = true;
}
scope.finished = (programs) => {
let p = programs;
scope.programs = null;
scope.onDone(p);
}
}
};
}

View File

@ -1,4 +1,4 @@
module.exports = function (pseudotv, $timeout) {
module.exports = function (dizquetv, $timeout) {
return {
restrict: 'E',
templateUrl: 'templates/hdhr-settings.html',
@ -6,7 +6,7 @@ module.exports = function (pseudotv, $timeout) {
scope: {
},
link: function (scope, element, attrs) {
pseudotv.getHdhrSettings().then((settings) => {
dizquetv.getHdhrSettings().then((settings) => {
scope.settings = settings
})
scope.updateSettings = (settings) => {
@ -19,12 +19,12 @@ module.exports = function (pseudotv, $timeout) {
$timeout(() => {
scope.error = null
}, 3500)
pseudotv.updateHdhrSettings(settings).then((_settings) => {
dizquetv.updateHdhrSettings(settings).then((_settings) => {
scope.settings = _settings
})
}
scope.resetSettings = (settings) => {
pseudotv.resetHdhrSettings(settings).then((_settings) => {
dizquetv.resetHdhrSettings(settings).then((_settings) => {
scope.settings = _settings
})
}

View File

@ -1,60 +0,0 @@
module.exports = function ($timeout) {
return {
restrict: 'E',
templateUrl: 'templates/offline-config.html',
replace: true,
scope: {
title: "@offlineTitle",
program: "=program",
visible: "=visible",
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showPlexLibrary = false;
scope.showFallbackPlexLibrary = false;
scope.finished = (prog) => {
if (
prog.channelOfflineMode != 'pic'
&& (prog.fallback.length == 0)
) {
scope.error = { fallback: 'Either add a fallback clip or change the fallback mode to Picture.' }
}
if (isNaN(prog.durationSeconds) || prog.durationSeconds < 0 ) {
scope.error = { duration: 'Duration must be a positive integer' }
}
if (scope.error != null) {
$timeout(() => {
scope.error = null
}, 3500)
return
}
scope.onDone(JSON.parse(angular.toJson(prog)))
scope.program = null
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
}
scope.program.filler = scope.program.filler.concat(selectedPrograms);
}
scope.importFallback = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l && i < 1; i++) {
selectedPrograms[i].commercials = []
}
scope.program.fallback = [];
if (selectedPrograms.length > 0) {
scope.program.fallback = [ selectedPrograms[0] ];
}
}
scope.durationString = (duration) => {
var date = new Date(0);
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
}
};
}

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