Compare commits

...

67 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
31f7011c86
Merge pull request #379 from vexorian/20210921_dev
20210921 dev
2021-09-21 08:57:17 -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
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
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
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
50 changed files with 939 additions and 23292 deletions

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

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ bin/
.pseudotv/
.dizquetv/
web/public/bundle.js
*.orig
*.orig
package-lock.json

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit ""

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec < /dev/tty && node_modules/.bin/cz --hook || true

View File

@ -2,73 +2,64 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
We pledge to make our community welcoming, safe, and equitable for all.
## Our Standards
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.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
## Encouraged Behaviors
Examples of unacceptable behavior by participants include:
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.
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
## Our Responsibilities
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**.
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Restricted Behaviors
## Scope
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
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.
## Enforcement
### Other Restrictions
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at vexorian@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
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.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## '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 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
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

View File

@ -1,6 +1,6 @@
FROM node:12.18-alpine3.12
WORKDIR /home/node/app
COPY package*.json ./
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 . .

View File

@ -1,4 +1,4 @@
# dizqueTV 1.5.0-development
# 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.
@ -25,6 +25,7 @@ EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml`
- Subtitle support.
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
- 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
@ -32,6 +33,7 @@ EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml`
- 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.
## Releases
@ -74,3 +76,14 @@ npm run dev-server
* 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.

View File

@ -1,6 +1,7 @@
const db = require('diskdb')
const fs = require('fs')
const unzip = require('unzipper')
const path = require('path')
const express = require('express')
const bodyParser = require('body-parser')
@ -29,6 +30,8 @@ const EventService = require("./src/services/event-service");
const OnDemandService = require("./src/services/on-demand-service");
const ProgrammingService = require("./src/services/programming-service");
const ActiveChannelService = require('./src/services/active-channel-service')
const ProgramPlayTimeDB = require('./src/dao/program-play-time-db')
const FfmpegSettingsService = require('./src/services/ffmpeg-settings-service')
const onShutdown = require("node-graceful-shutdown").onShutdown;
@ -48,12 +51,16 @@ if (NODE < 12) {
console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`);
}
unlockPath = false;
for (let i = 0, l = process.argv.length; i < l; i++) {
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)
process.env.PORT = process.argv[i + 1]
if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l)
process.env.DATABASE = process.argv[i + 1]
if (process.argv[i] === "--unlock") {
unlockPath = true;
}
}
process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv")
@ -89,13 +96,29 @@ if(!fs.existsSync(path.join(process.env.DATABASE, 'cache','images'))) {
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id', 'cache-images', 'settings'])
let fontAwesome = "fontawesome-free-5.15.4-web";
let bootstrap = "bootstrap-4.4.1-dist";
initDB(db, channelDB)
channelService = new ChannelService(channelDB);
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelService );
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
let programPlayTimeDB = new ProgramPlayTimeDB( path.join(process.env.DATABASE, 'play-cache') );
let ffmpegSettingsService = new FfmpegSettingsService(db, unlockPath);
async function initializeProgramPlayTimeDB() {
try {
let t0 = new Date().getTime();
await programPlayTimeDB.load();
let t1 = new Date().getTime();
console.log(`Program Play Time Cache loaded in ${t1-t0} milliseconds.`);
} catch (err) {
console.log(err);
}
}
initializeProgramPlayTimeDB();
fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') );
cacheImageService = new CacheImageService(db, fileCache);
@ -267,10 +290,12 @@ app.use('/favicon.svg', express.static(
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
// API Routers
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService ))
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService))
app.use('/api/cache/images', cacheImageService.apiRouters())
app.use('/' + fontAwesome, express.static(path.join(process.env.DATABASE, fontAwesome)))
app.use('/' + bootstrap, express.static(path.join(process.env.DATABASE, bootstrap)))
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService ))
app.use(video.router( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ))
app.use(hdhr.router)
app.listen(process.env.PORT, () => {
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)
@ -280,6 +305,7 @@ app.listen(process.env.PORT, () => {
})
function initDB(db, channelDB) {
//TODO: this is getting so repetitive, do it better
if (!fs.existsSync(process.env.DATABASE + '/images/dizquetv.png')) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/dizquetv.png')))
fs.writeFileSync(process.env.DATABASE + '/images/dizquetv.png', data)
@ -309,11 +335,32 @@ function initDB(db, channelDB) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
}
if (!fs.existsSync(process.env.DATABASE + '/images/black.png')) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/black.png')))
fs.writeFileSync(process.env.DATABASE + '/images/black.png', data)
}
if (!fs.existsSync( path.join(process.env.DATABASE, 'custom.css') )) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources', 'default-custom.css')))
fs.writeFileSync( path.join(process.env.DATABASE, 'custom.css'), data)
}
if (!fs.existsSync( path.join(process.env.DATABASE, fontAwesome) )) {
let sourceZip = path.resolve(__dirname, 'resources', fontAwesome) + ".zip";
let destinationPath = path.resolve(process.env.DATABASE);
fs.createReadStream(sourceZip)
.pipe(unzip.Extract({ path: destinationPath }));
}
if (!fs.existsSync( path.join(process.env.DATABASE, bootstrap) )) {
let sourceZip = path.resolve(__dirname, 'resources', bootstrap) + ".zip";
let destinationPath = path.resolve(process.env.DATABASE);
fs.createReadStream(sourceZip)
.pipe(unzip.Extract({ path: destinationPath }));
}
}

23088
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,7 @@
"dev-server": "nodemon index.js --ignore ./web/ --ignore ./db/ --ignore ./xmltv.xml",
"compile": "babel index.js -d dist && babel src -d dist/src",
"package": "sh ./make_dist.sh",
"clean": "del-cli --force ./bin ./dist ./.dizquetv ./web/public/bundle.js",
"prepare": "husky install"
"clean": "del-cli --force ./bin ./dist ./.dizquetv ./web/public/bundle.js"
},
"author": "vexorian",
"license": "Zlib",
@ -35,9 +34,11 @@
"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",
@ -50,9 +51,7 @@
"@commitlint/config-conventional": "^12.1.4",
"browserify": "^16.5.1",
"copyfiles": "^2.2.0",
"cz-conventional-changelog": "^3.3.0",
"del-cli": "^3.0.0",
"husky": "^7.0.0",
"nexe": "^3.3.7",
"nodemon": "^2.0.3",
"watchify": "^3.11.1"
@ -61,10 +60,5 @@
"plugins": [
"@babel/plugin-proposal-class-properties"
]
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}

View File

@ -7,8 +7,8 @@
* [ ] I have read the code of conduct.
* [ ] I am submitting to the correct base branch
<!--
* Bug fixes must go to `dev/1.4.x`.
* New features must go to `dev/1.5.x`.
* 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
@ -19,3 +19,7 @@
* [ ] 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.

Binary file not shown.

View File

@ -2,7 +2,6 @@
const express = require('express')
const path = require('path')
const fs = require('fs')
const databaseMigration = require('./database-migration');
const constants = require('./constants');
const JSONStream = require('JSONStream');
const FFMPEGInfo = require('./ffmpeg-info');
@ -25,7 +24,7 @@ function safeString(object) {
}
module.exports = { router: api }
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) {
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService ) {
let m3uService = _m3uService;
const router = express.Router()
const plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db);
@ -529,7 +528,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
// FFMPEG SETTINGS
router.get('/api/ffmpeg-settings', (req, res) => {
try {
let ffmpeg = db['ffmpeg-settings'].find()[0]
let ffmpeg = ffmpegSettingsService.get();
res.send(ffmpeg)
} catch(err) {
console.error(err);
@ -538,9 +537,9 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
})
router.put('/api/ffmpeg-settings', (req, res) => {
try {
db['ffmpeg-settings'].update({ _id: req.body._id }, req.body)
let ffmpeg = db['ffmpeg-settings'].find()[0]
let err = fixupFFMPEGSettings(ffmpeg);
let result = ffmpegSettingsService.update(req.body);
let err = result.error
if (typeof(err) !== 'undefined') {
return res.status(400).send(err);
}
@ -555,7 +554,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
"level" : "info"
}
);
res.send(ffmpeg)
res.send(result.ffmpeg)
} catch(err) {
console.error(err);
res.status(500).send("error");
@ -576,10 +575,8 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
})
router.post('/api/ffmpeg-settings', (req, res) => { // RESET
try {
let ffmpeg = databaseMigration.defaultFFMPEG() ;
ffmpeg.ffmpegPath = req.body.ffmpegPath;
db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg)
ffmpeg = db['ffmpeg-settings'].find()[0]
let ffmpeg = ffmpegSettingsService.reset();
eventService.push(
"settings-update",
{
@ -612,14 +609,6 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
})
function fixupFFMPEGSettings(ffmpeg) {
if (typeof(ffmpeg.maxFPS) === 'undefined') {
ffmpeg.maxFPS = 60;
} else if ( isNaN(ffmpeg.maxFPS) ) {
return "maxFPS should be a number";
}
}
// PLEX SETTINGS
router.get('/api/plex-settings', (req, res) => {
try {

View File

@ -1,7 +1,7 @@
const SLACK = require('./constants').SLACK;
let cache = {};
let programPlayTimeCache = {};
let fillerPlayTimeCache = {};
let configCache = {};
let numbers = null;
@ -14,11 +14,9 @@ async function getChannelConfig(channelDB, channelId) {
if (channel == null) {
configCache[channelId] = [];
} else {
//console.log("channel=" + JSON.stringify(channel) );
configCache[channelId] = [channel];
}
}
//console.log("channel=" + JSON.stringify(configCache[channelId]).slice(0,200) );
return configCache[channelId];
}
@ -106,7 +104,7 @@ function getCurrentLineupItem(channelId, t1) {
return lineupItem;
}
function getKey(channelId, program) {
function getProgramKey(program) {
let serverKey = "!unknown!";
if (typeof(program.serverKey) !== 'undefined') {
if (typeof(program.serverKey) !== 'undefined') {
@ -117,36 +115,37 @@ function getKey(channelId, program) {
if (typeof(program.key) !== 'undefined') {
programKey = program.key;
}
return channelId + "|" + serverKey + "|" + programKey;
return serverKey + "|" + programKey;
}
function getFillerKey(channelId, fillerId) {
return channelId + "|" + fillerId;
}
function recordProgramPlayTime(channelId, lineupItem, t0) {
function recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0) {
let remaining;
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
remaining = lineupItem.streamDuration;
} else {
remaining = lineupItem.duration - lineupItem.start;
}
programPlayTimeCache[ getKey(channelId, lineupItem) ] = t0 + remaining;
setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t0 + remaining);
if (typeof(lineupItem.fillerId) !== 'undefined') {
fillerPlayTimeCache[ getFillerKey(channelId, lineupItem.fillerId) ] = t0 + remaining;
}
}
function getProgramLastPlayTime(channelId, program) {
let v = programPlayTimeCache[ getKey(channelId, program) ];
if (typeof(v) === 'undefined') {
return 0;
} else {
return v;
}
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) {
@ -158,8 +157,8 @@ function getFillerLastPlayTime(channelId, fillerId) {
}
}
function recordPlayback(channelId, t0, lineupItem) {
recordProgramPlayTime(channelId, lineupItem, t0);
function recordPlayback(programPlayTime, channelId, t0, lineupItem) {
recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0);
cache[channelId] = {
t0: t0,

View File

@ -5,6 +5,13 @@ module.exports = {
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,
@ -28,5 +35,5 @@ module.exports = {
// staying active, it checks every 5 seconds
PLAYED_MONITOR_CHECK_FREQUENCY: 5*1000,
VERSION_NAME: "1.5.0-development"
VERSION_NAME: "1.5.5"
}

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;

View File

@ -20,7 +20,8 @@
const path = require('path');
var fs = require('fs');
const TARGET_VERSION = 803;
const TARGET_VERSION = 805;
const DAY_MS = 1000 * 60 * 60 * 24;
const STEPS = [
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
@ -43,6 +44,8 @@ const STEPS = [
[ 800, 801, (db) => addImageCache(db) ],
[ 801, 802, () => addGroupTitle() ],
[ 802, 803, () => fixNonIntegerDurations() ],
[ 803, 805, (db) => addFFMpegLock(db) ],
[ 804, 805, (db) => addFFMpegLock(db) ],
]
const { v4: uuidv4 } = require('uuid');
@ -384,6 +387,7 @@ function ffmpeg() {
//How default ffmpeg settings should look
configVersion: 5,
ffmpegPath: "/usr/bin/ffmpeg",
ffmpegPathLockDate: new Date().getTime() + DAY_MS,
threads: 4,
concatMuxDelay: "0",
logFfmpeg: false,
@ -765,6 +769,19 @@ function addScalingAlgorithm(db) {
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
}
function addFFMpegLock(db) {
let ffmpegSettings = db['ffmpeg-settings'].find()[0];
let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json');
if ( typeof(ffmpegSettings.ffmpegPathLockDate) === 'undefined' || ffmpegSettings.ffmpegPathLockDate == null ) {
console.log("Adding ffmpeg lock. For your security it will not be possible to modify the ffmpeg path using the UI anymore unless you launch dizquetv by following special instructions..");
// We are migrating an existing db that had a ffmpeg path. Make sure
// it's already locked.
ffmpegSettings.ffmpegPathLockDate = new Date().getTime() - 2 * DAY_MS;
}
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
}
function moveBackup(path) {
if (fs.existsSync(`${process.env.DATABASE}${path}`) ) {
let i = 0;

View File

@ -18,7 +18,7 @@ class FFMPEGInfo {
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 s;
return "Unknown";
}
return m[1];
} catch (err) {

View File

@ -198,8 +198,9 @@ class FFMPEG extends events.EventEmitter {
iW = this.wantedW;
iH = this.wantedH;
let durstr = `duration=${streamStats.duration}ms`;
if (this.audioOnly !== true) {
ffmpegArgs.push("-r" , "24");
let pic = null;
//does an image to play exist?
@ -216,6 +217,11 @@ class FFMPEG extends events.EventEmitter {
}
if (pic != null) {
if (this.opts.noRealTime === true) {
ffmpegArgs.push("-r" , "60");
} else {
ffmpegArgs.push("-r" , "24");
}
ffmpegArgs.push(
'-i', pic,
);
@ -230,11 +236,17 @@ class FFMPEG extends events.EventEmitter {
videoComplex = `;[${inputFiles++}:0]format=yuv420p[formatted]`;
videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`;
videoComplex += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`;
videoComplex += `;[padded]loop=loop=-1:size=1:start=0[looped]`;
videoComplex +=`;[looped]realtime[videox]`;
videoComplex += `;[padded]loop=loop=-1:size=1:start=0`;
if (this.opts.noRealTime !== true) {
videoComplex +=`[looped];[looped]realtime[videox]`;
} else {
videoComplex +=`[videox]`
}
//this tune apparently makes the video compress better
// when it is the same image
stillImage = true;
this.volumePercent = Math.min(70, this.volumePercent);
} else if (this.opts.errorScreen == 'static') {
ffmpegArgs.push(
'-f', 'lavfi',
@ -269,7 +281,7 @@ class FFMPEG extends events.EventEmitter {
videoComplex = `;realtime[videox]`;
}
}
let durstr = `duration=${streamStats.duration}ms`;
if (typeof(streamUrl.errorTitle) !== 'undefined') {
//silent
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
@ -471,7 +483,8 @@ class FFMPEG extends events.EventEmitter {
`-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
`-sc_threshold`, `1000000000`,
);
if (stillImage) {
// do not use -tune stillimage for nv
if (stillImage && ! this.opts.videoEncoder.toLowerCase().includes("nv") ) {
ffmpegArgs.push('-tune', 'stillimage');
}
}
@ -482,10 +495,17 @@ class FFMPEG extends events.EventEmitter {
if ( transcodeVideo && (this.audioOnly !== true) ) {
// add the video encoder flags
ffmpegArgs.push(
`-b:v`, `${this.opts.videoBitrate}k`,
'-crf', '22',
`-maxrate:v`, `${this.opts.videoBitrate}k`,
`-bufsize:v`, `${this.opts.videoBufSize}k`
);
if (this.opts.videoEncoder.toLowerCase() === "mpeg2video") {
// This makes message "impossible bitrate constraints, this will fail" appear but nothing actually fails and it really looks like b:v is the only way to make the video look good when using mpeg2video
ffmpegArgs.push(
`-qscale:v`, `1`,
'-b:v', `${this.opts.videoBitrate}k`
);
}
}
if ( transcodeAudio ) {
// add the audio encoder flags
@ -549,6 +569,7 @@ class FFMPEG extends events.EventEmitter {
if (this.hasBeenKilled) {
return ;
}
//console.log(this.ffmpegPath + " " + ffmpegArgs.join(" ") );
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
if (this.hasBeenKilled) {
console.log("Send SIGKILL to ffmpeg");

View File

@ -6,8 +6,10 @@ module.exports = {
}
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() );
@ -61,7 +63,7 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
}
function createLineup(obj, channel, fillers, 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
@ -96,7 +98,7 @@ function createLineup(obj, channel, fillers, isFirst) {
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
special = JSON.parse(JSON.stringify(channel.fallback[0]));
}
let randomResult = pickRandomWithMaxDuration(channel, fillers, remaining + (isFirst? (7*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;
@ -178,7 +180,7 @@ function weighedPick(a, total) {
return random.bool(a, total);
}
function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
let list = [];
for (let i = 0; i < fillers.length; i++) {
list = list.concat(fillers[i].content);
@ -194,16 +196,36 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
}
let listM = 0;
let fillerId = undefined;
for (let j = 0; j < fillers.length; j++) {
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.duration <= maxDuration + SLACK ) {
let t1 = channelCache.getProgramLastPlayTime( channel.number, clip );
let t1 = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip );
if (t1 > maximumPlayTimeAllowed) {
continue;
}
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
@ -247,11 +269,13 @@ function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
}
}
}
}
if (pick1 != null) {
break;
}
}
let pick = pick1;
let pickTitle = "null";
if (pick != null) {
pickTitle = pick.title;
pick = JSON.parse( JSON.stringify(pick) );
pick.fillerId = fillerId;
}
@ -322,6 +346,26 @@ function getWatermark( ffmpegSettings, channel, type) {
}
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++) {

View File

@ -18,6 +18,11 @@ class OfflinePlayer {
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);
}

View File

@ -34,6 +34,7 @@ class ProgramPlayer {
// 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);
@ -42,6 +43,11 @@ class ProgramPlayer {
/* 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 */

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

@ -562,6 +562,9 @@ function makeEntry(channel, x) {
episode: x.program.episode,
title: x.program.title,
}
} else if (x.program.type === 'track') {
title = x.program.title;
// TODO: Add sub data for tracks here for XML writing
}
}
if (typeof(title)==='undefined') {

View File

@ -7,7 +7,20 @@ function equalItems(a, b) {
if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) {
return false;
}
return ( a.type === b.type);
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;
}
@ -17,15 +30,14 @@ 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,
};
}
let result = false;
if (t1 - previous.t0 < constants.TOO_FREQUENT) {
} else if (t1 - previous.t0 < constants.TOO_FREQUENT) {
//certainly too frequent
result = equalItems( previous.lineupItem, lineupItem );
}
@ -49,4 +61,4 @@ function wereThereTooManyAttempts(sessionId, lineupItem) {
}
module.exports = wereThereTooManyAttempts;
module.exports = wereThereTooManyAttempts;

View File

@ -18,7 +18,7 @@ async function shutdown() {
stopPlayback = true;
}
function video( channelService, fillerDB, db, programmingService, activeChannelService ) {
function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ) {
var router = express.Router()
router.get('/setup', (req, res) => {
@ -51,7 +51,10 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
})
})
// Continuously stream video to client. Leverage ffmpeg concat for piecing together videos
let concat = async (req, res, audioOnly) => {
let concat = async (req, res, audioOnly, step) => {
if ( typeof(step) === 'undefined') {
step = 0;
}
if (stopPlayback) {
res.status(503).send("Server is shutting down.")
return;
@ -78,9 +81,11 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
return
}
res.writeHead(200, {
'Content-Type': 'video/mp2t'
})
if (step == 0) {
res.writeHead(200, {
'Content-Type': 'video/mp2t'
})
}
console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`)
@ -107,7 +112,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
return;
})
ffmpeg.on('close', stop)
//ffmpeg.on('close', stop)
res.on('close', () => { // on HTTP close, kill ffmpeg
console.log(`\r\nStream ended. Channel: ${channel.number} (${channel.name})`);
@ -115,13 +120,13 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
})
ffmpeg.on('end', () => {
console.log("Video queue exhausted. Either you played 100 different clips in a row or there were technical issues that made all of the possible 100 attempts fail.")
stop();
console.log("Queue exhausted so we are appending the channel stream again to the http output.")
concat(req, res, audioOnly, step+1);
})
let channelNum = parseInt(req.query.channel, 10)
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}`);
ff.pipe(res );
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}&stepNumber={step}`);
ff.pipe(res, { end: false} );
};
router.get('/video', async(req, res) => {
return await concat(req, res, false);
@ -167,6 +172,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
isFirst = true;
}
let isBetween = ( (typeof req.query.between !== 'undefined') && (req.query.between=='1') );
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
// Check if ffmpeg path is valid
@ -176,24 +183,46 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
return
}
if (ffmpegSettings.disablePreludes === true) {
//disable the preludes
isBetween = false;
}
// Get video lineup (array of video urls with calculated start times and durations.)
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
let prog = null;
let brandChannel = channel;
let redirectChannels = [];
let upperBounds = [];
const GAP_DURATION = constants.GAP_DURATION;
if (isLoading) {
lineupItem = {
type: 'loading',
streamDuration: 40,
duration: 40,
title: "Loading Screen",
noRealTime: true,
streamDuration: GAP_DURATION,
duration: GAP_DURATION,
redirectChannels: [channel],
start: 0,
};
} else if (lineupItem != null) {
} else if (isBetween) {
lineupItem = {
type: 'interlude',
title: "Interlude Screen",
noRealTime: true,
streamDuration: GAP_DURATION,
duration: GAP_DURATION,
redirectChannels: [channel],
start: 0,
};
} else {
lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
}
if (lineupItem != null) {
redirectChannels = lineupItem.redirectChannels;
upperBounds = lineupItem.upperBounds;
brandChannel = redirectChannels[ redirectChannels.length -1];
@ -208,7 +237,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
if ( !(prog.program.isOffline) || (prog.program.type != 'redirect') ) {
break;
}
channelCache.recordPlayback( brandChannel.number, t0, {
channelCache.recordPlayback(programPlayTimeDB,
brandChannel.number, t0, {
/*type: 'offline',*/
title: 'Error',
err: Error("Recursive channel redirect found"),
@ -274,11 +304,20 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
throw "No video to play, this means there's a serious unexpected bug or the channel db is corrupted."
}
let fillers = await fillerDB.getFillersFromChannel(brandChannel);
let lineup = helperFuncs.createLineup(prog, brandChannel, fillers, isFirst)
lineupItem = lineup.shift();
try {
let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst)
lineupItem = lineup.shift();
} catch (err) {
console.log("Error when attempting to pick video: " +err.stack);
lineupItem = {
isOffline: true,
err: err,
duration : 60000,
};
}
}
if ( !isLoading && (lineupItem != null) ) {
if ( !isBetween && !isLoading && (lineupItem != null) ) {
let upperBound = 1000000000;
let beginningOffset = 0;
if (typeof(lineupItem.beginningOffset) !== 'undefined') {
@ -298,10 +337,13 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
lineupItem.streamDuration = Math.min(u2, u);
upperBound = lineupItem.streamDuration;
}
channelCache.recordPlayback( redirectChannels[i].number, t0, lineupItem );
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
}
}
let t2 = (new Date()).getTime();
console.log( `Decision Latency: (${t2-t0})ms` );
console.log("=========================================================");
console.log("! Start playback");
@ -317,8 +359,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
}
console.log("=========================================================");
if (! isLoading) {
channelCache.recordPlayback(channel.number, t0, lineupItem);
if (! isLoading && ! isBetween) {
channelCache.recordPlayback(programPlayTimeDB, channel.number, t0, lineupItem);
}
if (wereThereTooManyAttempts(session, lineupItem)) {
console.error("There are too many attempts to play the same item in a short period of time, playing the error stream instead.");
@ -363,7 +405,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
try {
playerObj = await player.play(res);
t1 = (new Date()).getTime();
console.log("Latency: (" + (t1- t0) );
console.log( `Player Latency: (${t1-t0})ms` );
} catch (err) {
console.log("Error when attempting to play video: " +err.stack);
try {
@ -523,6 +565,10 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
return
}
let stepNumber = parseInt(req.query.stepNumber, 10)
if (isNaN(stepNumber)) {
stepNumber = 0;
}
let channelNum = parseInt(req.query.channel, 10)
let channel = await channelService.getChannel(channelNum );
if (channel == null) {
@ -541,20 +587,35 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
let sessionId = StreamCount++;
let audioOnly = ("true" == req.query.audioOnly);
if (
(ffmpegSettings.enableFFMPEGTranscoding === true)
let transcodingEnabled = (ffmpegSettings.enableFFMPEGTranscoding === true)
&& (ffmpegSettings.normalizeVideoCodec === true)
&& (ffmpegSettings.normalizeAudioCodec === true)
&& (ffmpegSettings.normalizeResolution === true)
&& (ffmpegSettings.normalizeAudio === true)
&& (ffmpegSettings.normalizeAudio === true);
if (
transcodingEnabled
&& (audioOnly !== true) /* loading screen is pointless in audio mode (also for some reason it makes it fail when codec is aac, and I can't figure out why) */
&& (stepNumber == 0)
) {
//loading screen
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}&audioOnly=${audioOnly}'\n`;
}
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
let remaining = maxStreamsToPlayInARow;
if (stepNumber == 0) {
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
if (transcodingEnabled && (audioOnly !== true)) {
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&between=1&session=${sessionId}&audioOnly=${audioOnly}'\n`;
}
remaining--;
}
for (var i = 0; i < remaining; i++) {
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}&audioOnly=${audioOnly}'\n`
if (transcodingEnabled && (audioOnly !== true) ) {
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&between=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
}
}
res.send(data)

View File

@ -98,6 +98,7 @@ async function _writeProgramme(channel, program, xw, xmlSettings, cacheImageServ
xw.writeRaw('\n <previously-shown/>')
//sub-title
// TODO: Add support for track data (artist, album) here
if ( typeof(program.sub) !== 'undefined') {
xw.startElement('sub-title')
xw.writeAttribute('lang', 'en')

View File

@ -8,6 +8,10 @@ module.exports = function ($scope, $timeout, dizquetv) {
$scope.shows = [ { id: '?', pending: true} ]
$timeout();
let shows = await dizquetv.getAllShowsInfo();
shows.sort( (a,b) => {
return a.name > b.name;
} );
$scope.shows = shows;
$timeout();
}

View File

@ -8,13 +8,14 @@ module.exports = function ($scope, $timeout, dizquetv) {
$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;

View File

@ -312,7 +312,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
ch.programs.push( {
duration: addDuration(b - a),
altTitle: altTitle,
showTitle: program.title,
showTitle: program.title, // movie title, episode title or track title
subTitle: subTitle,
episodeTitle : episodeTitle,
start: hasStart,

View File

@ -11,7 +11,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
},
link: {
post: function (scope, element, attrs) {
post: function (scope, $element, attrs) {
scope.screenW = 1920;
scope.screenh = 1080;
@ -326,9 +326,10 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
duration: duration,
isOffline: true
}
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
scope._selectedOffline = null
scope._addingOffline = null;
scrollToLast();
updateChannelDuration()
}
@ -1077,11 +1078,37 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
}
}
scope.importPrograms = (selectedPrograms) => {
function getAllMethods(object) {
return Object.getOwnPropertyNames(object).filter(function (p) {
return typeof object[p] == 'function';
});
}
function scrollToLast() {
var programListElement = document.getElementById("channelConfigProgramList");
$timeout(() => { programListElement.scrollTo(0, 2000000); }, 0)
}
scope.importPrograms = (selectedPrograms, insertPoint) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
delete selectedPrograms[i].commercials;
}
scope.channel.programs = scope.channel.programs.concat(selectedPrograms)
var programListElement = document.getElementById("channelConfigProgramList");
if (insertPoint === "start") {
scope.channel.programs = selectedPrograms.concat(scope.channel.programs);
programListElement.scrollTo(0, 0);
} else if (insertPoint === "current") {
scope.channel.programs = [
...scope.channel.programs.slice(0, scope.currentStartIndex),
...selectedPrograms,
...scope.channel.programs.slice(scope.currentStartIndex)
];
} else {
scope.channel.programs = scope.channel.programs.concat(selectedPrograms)
scrollToLast();
}
updateChannelDuration()
setTimeout(
() => {
@ -1093,7 +1120,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
}
scope.finishRedirect = (program) => {
if (scope.selectedProgram == -1) {
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
scrollToLast();
} else {
scope.channel.programs[ scope.selectedProgram ] = program;
}

View File

@ -11,8 +11,13 @@ module.exports = function (dizquetv, resolutionOptions) {
scope.settings = settings
})
scope.updateSettings = (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) => {

View File

@ -6,6 +6,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
scope: {
onFinish: "=onFinish",
height: "=height",
positionChoice: "=positionChoice",
visible: "=visible",
limit: "=limit",
},
@ -14,6 +15,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
if ( typeof(scope.limit) == 'undefined') {
scope.limit = 1000000000;
}
scope.insertPoint = "end";
scope.customShows = [];
scope.origins = [];
scope.currentOrigin = undefined;
@ -37,7 +39,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
updateCustomShows();
}
}
scope._onFinish = (s) => {
scope._onFinish = (s, insertPoint) => {
if (s.length > scope.limit) {
if (scope.limit == 1) {
scope.error = "Please select only one clip.";
@ -45,7 +47,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
scope.error = `Please select at most ${scope.limit} clips.`;
}
} else {
scope.onFinish(s)
scope.onFinish(s, insertPoint)
scope.selection = []
scope.visible = false
}
@ -69,7 +71,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
}
}
scope.selectLibrary = async (library) => {
await scope.fillNestedIfNecessary(library);
await scope.fillNestedIfNecessary(library, true);
let p = library.nested.length;
scope.pending += library.nested.length;
try {

View File

@ -4,8 +4,8 @@
<title>dizqueTV</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/favicon.svg" ></link>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
<link rel="stylesheet" href="bootstrap-4.4.1-dist/css/bootstrap.min.css">
<link rel="stylesheet" href="fontawesome-free-5.15.4-web/css/all.css">
<link href="style.css" rel="stylesheet">
<link href="custom.css" rel="stylesheet">
<script src="version.js"></script>

View File

@ -390,4 +390,8 @@ div.programming-programs div.list-group-item {
width: 1em;
height: 1em;
margin-bottom: 0.25em;
}
.list-group-item .library-item-hover:hover {
background: #D0D0FF
}

View File

@ -170,6 +170,7 @@
ng-init="setUpWatcher()"
ng-if="true"
ng-style="{'max-height':programmingHeight()}"
id="channelConfigProgramList"
>
<div ng-repeat="x in channel.programs track by x.$index"
ng-click="selectProgram(x.$index)"
@ -938,7 +939,7 @@
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
<remove-shows program-infos="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" position-choice=true on-finish="importPrograms"></plex-library>
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
<time-slots-schedule-editor linker="registerTimeSlots" on-done="onTimeSlotsDone"></time-slots-schedule-editor>

View File

@ -1,4 +1,8 @@
<div>
<small class="text-danger" nf-show="settingsError">
{{ settingsError }}
</small>
<h5>FFMPEG Settings
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
@ -8,9 +12,48 @@
Reset Options
</button>
</h5>
<h6>FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)</h6>
<input type="text" class="form-control form-control-sm" ria-describedby="ffmpegHelp" ng-model="settings.ffmpegPath"></input>
<small id="ffmpegHelp" class="form-text text-muted">FFMPEG version 4.2+ required. Check by opening the version tab</small>
<hr></hr>
<h6>FFMPEG Executable Path</h6>
<div class="row" ng-show="settings.lock !== true">
<div class="col-sm-9">
<div class="form-group">
<div class="form-group">
<label>Path</label>
<input id="ffmpegPath" ria-describedby="ffmpegHelp" type="text" class="form-control form-control-sm" ng-model="settings.ffmpegPath"></input>
<small class="form-text text-muted">
The path to the ffmpeg executable. (e.g: /usr/bin/ffmpeg or C:\ffmpeg\bin\ffmpeg.exe) FFMPEG version 4.2+ required. Check by opening the version tab.
</small>
</div>
</div>
</div>
<div class="col-sm-9">
<div class="form-group">
<input id="lockFfmpeg" type="checkbox" ng-model="settings.addLock"></input>
<label for="lockFfmpeg">Lock ffmpeg path setting</label>
<small class="form-text text-muted">This will lock the ffmpeg path setting so that it is no longer editable from UI. Even if you don't toggle this option, the setting will get locked in 24 hours.</small>
</div>
</div>
</div>
<div class="row" ng-show="settings.lock === true">
<div class="col-sm-9">
<div class="form-group">
<div class="form-group">
<label>Path</label>
<input id="ffmpegPath" ria-describedby="ffmpegHelp" type="text" class="form-control form-control-sm" ng-model="settings.ffmpegPath" readonly></input>
<small class="form-text text-muted">
The ffmpeg path setting is currently locked and can't be edited from the UI. It's not usually necessary to update this path once it's known to be working. Run dizquetv with the <b>--unlock</b> command line argument to enable editing it again.
</small>
</div>
</div>
</div>
</div>
<hr></hr>
<h6>Miscellaneous Options</h6>
<div class="row">
@ -196,6 +239,18 @@
</div>
</div>
<br ></br>
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<input id="disablePreludes" type="checkbox" ng-model="settings.disablePreludes" ng-disabled="isTranscodingNotNeeded()" ></input>
<label for="disablePreludes">Disable Preludes</label>
<small class="form-text text-muted">In an attempt to improve playback, dizqueTV insets really short clips of black screen between videos. The idea is that if the stream pauses because Plex is taking too long to reply, it will pause during one of those black screens instead of interrupting the last second of a video. If you suspect these black screens are causing trouble instead of helping, you can disable them with this option.
</small>
</div>
</div>
</div>
</div>

View File

@ -28,46 +28,59 @@
</span>
</div>
<div class="modal-body">
<select class="form-control form-control-sm custom-select" ng-model="currentOrigin"
<div class="mb-3">
<label for="source-selector" class="form-label">Source:</label>
<select class="form-select form-select-sm custom-select" ng-model="currentOrigin"
size="2"
id="source-selector"
ng-options="x.name for x in origins" ng-change="selectOrigin(currentOrigin)"></select>
<hr ></hr>
</div>
<div class="mb-3">
<label class="form-label">
<button class="btn btn-sm btn-link" ng-click="selectOrigin(currentOrigin)">
<span class="text-info fa fa-sync" ></span>
</button>
Content:
</label>
<ul ng-show="currentOrigin.type=='plex' " class="list-group list-group-root plex-panel" ng-init="setHeight = {'height': height + 'px'}" ng-style="setHeight" lazy-img-container>
<li class="list-group-item" ng-repeat="a in libraries">
<div class="flex-container {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
<div class="flex-container library-item-hover {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{a.icon}}" ></img>
<span>{{ displayTitle(a) }}</span><!-- Library -->
<span ng-if="a.type === 'show' || a.type === 'movie' || a.type === 'artist'" class="flex-pull-right" ng-click='$event.stopPropagation(); selectLibrary(a)'>
<span class="fa fa-plus btn"></span>
<span class="fa fa-plus-circle btn"></span>
</span>
</div>
<ul ng-if="a.collapse" class="list-group">
<li class="list-group-item {{ b.type !== 'movie' ? 'list-group-item-secondary' : 'list-group-item-video' }}"
ng-repeat="b in a.nested">
<div class="flex-container"
<div class="flex-container library-item-hover"
ng-click="b.type !== 'movie' ? getNested(b) : selectItem(b, true)">
<span ng-if="b.type === 'movie'" class="fa fa-plus-circle tab"></span>
<span ng-if="b.type !== 'movie'" class="tab"></span>
<span ng-if="b.type !== 'movie'" class="fa {{ b.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{ b.type === 'episode' ? b.episodeIcon : b.icon }}" ></img>
{{ displayTitle(b) }}
<span ng-if="b.type === 'movie'" class="flex-pull-right">
<span class="flex-grow-1">{{ displayTitle(b) }}</span>
<span ng-if="b.type === 'movie'" class="">
{{b.durationStr}}
</span>
<span class="flex-pull-right" ng-if="b.type === 'movie'">
<span class="fa fa-plus-circle btn"></span>
</span>
<span ng-if="b.type === 'playlist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectPlaylist(b);">
<span class="fa fa-plus btn"></span>
<span class="fa fa-plus-circle btn"></span>
</span>
<span ng-if="b.type === 'show' || b.type === 'collection' || b.type === 'genre' || b.type === 'artist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectShow(b);">
<span class="fa fa-plus btn"></span>
<span class="fa fa-plus-circle btn"></span>
</span>
</div>
<ul ng-if="b.collapse" class="list-group">
<li ng-repeat="c in b.nested"
class="list-group-item {{ c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? 'list-group-item-dark' : 'list-group-item-video' }}">
<div class="flex-container"
<div class="flex-container library-item-hover"
ng-click="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? getNested(c) : selectItem(c, true)">
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track'"
class="fa fa-plus-circle tab"></span>
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
class="tab"></span>
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
@ -75,23 +88,30 @@
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
class="fa {{ c.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
<img ng-if="displayImages" lazy-img="{{c.type === 'episode' ? c.episodeIcon : c.icon }}" ></img>
{{ displayTitle(c) }}
<span class="flex-grow-1">{{ displayTitle(c) }}</span>
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track' "
class="flex-pull-right">
class="">
{{c.durationStr}}
</span>
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track'" class="flex-pull-right">
<span
class="fa fa-plus-circle btn"></span>
</span>
<span ng-if="c.type === 'season' || c.type === 'album'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectSeason(c);">
<span class="fa fa-plus btn"></span>
<span class="fa fa-plus-circle btn"></span>
</span>
</div>
<ul ng-if="c.collapse" class="list-group">
<li class="list-group-item list-group-item-video"
ng-repeat="d in c.nested">
<div class="flex-container" ng-click="selectItem(d, true)">
<span class="fa fa-plus-circle tab"></span>
<div class="flex-container library-item-hover" ng-click="selectItem(d, true)">
<img ng-if="displayImages" lazy-img="{{d.episodeIcon}}" ></img>
{{ displayTitle(d) }}
<span class="flex-pull-right">{{d.durationStr}}</span>
<span class="flex-grow-1">{{ displayTitle(d) }}</span>
<span class="">{{d.durationStr}}</span>
<span class="flex-pull-right">
<span class="fa fa-plus-circle btn"></span>
</span>
<!-- Episode -->
</div>
</li>
@ -111,6 +131,7 @@
</div>
</li>
</ul>
</div>
<hr></hr>
<div class="loader" ng-if="pending &gt; 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
@ -129,9 +150,21 @@
</ul>
</div>
<div class='text-danger'>{{error}}</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="_onFinish([])">Cancel</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="_onFinish(selection);">Done</button>
<div class="modal-footer flex">
<div class="flex-grow-1" ng-show="positionChoice === true"">
<select class="form-select form-select-sm custom-select" ng-model="insertPoint"
id="position-selector">
<option value="end">Insert at the end of list</option>
<option value="start">Insert at the beginning of list</option>
<option value="current">Insert at current scroll position</option>
</select>
</div>
<div><button type="button" class="btn btn-sm btn-link" ng-click="_onFinish([])">Cancel</button></div>
<div><button type="button" class="btn btn-sm btn-primary" ng-click="_onFinish(selection, insertPoint);">Done</button></div>
</div>
</div>
</div>

View File

@ -65,7 +65,7 @@
</div>
<div class='form-group col-md-7' ng-if="schedule.randomDistribution == 'weighted'" >
<label ng-if="$index==0" for="weightRange{{$index}}">Weight</label>
<input class='form-control-range custom-range' id="weightRange{{$index}}" type='range' ng-model='slot.weight' min=1 max=600
<input class='form-control-range custom-range' id="weightRange{{$index}}" type='range' ng-model='slot.weight' min=1 max=60000
data-toggle="tooltip" data-placement="bottom" title="Slots with more weight will be picked more frequently."
ng-change="refreshSlots()"
>

View File

@ -283,7 +283,7 @@ module.exports = function ($http, $window, $interval) {
console.error(msg , err);
}
}
if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) {
if (includeCollections === true) {
let k = res.librarySectionID;
k = `/library/sections/${k}/collections`;