Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8481e2cea1 | ||
|
|
86d5e0018a | ||
|
|
350d59d928 |
@ -1,6 +1,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
*Dockerfile*
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
|||||||
13
.github/workflows/README.md
vendored
13
.github/workflows/README.md
vendored
@ -1,13 +0,0 @@
|
|||||||
# 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
46
.github/workflows/binaries-build.yaml
vendored
@ -1,46 +0,0 @@
|
|||||||
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 }}
|
|
||||||
13
.github/workflows/development-binaries.yaml
vendored
13
.github/workflows/development-binaries.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
13
.github/workflows/development-tag.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
44
.github/workflows/docker-build.yaml
vendored
@ -1,44 +0,0 @@
|
|||||||
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
13
.github/workflows/edge-tag.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
13
.github/workflows/latest-tag.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
13
.github/workflows/named-tag.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
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
13
.github/workflows/tag-binaries.yaml
vendored
@ -1,13 +0,0 @@
|
|||||||
name: Release Binaries
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
binaries:
|
|
||||||
uses: ./.github/workflows/binaries-build.yaml
|
|
||||||
with:
|
|
||||||
release: ${{ github.ref_name }}
|
|
||||||
secrets: inherit
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,5 +4,4 @@ bin/
|
|||||||
.pseudotv/
|
.pseudotv/
|
||||||
.dizquetv/
|
.dizquetv/
|
||||||
web/public/bundle.js
|
web/public/bundle.js
|
||||||
*.orig
|
*.orig
|
||||||
package-lock.json
|
|
||||||
@ -2,64 +2,73 @@
|
|||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
|
|
||||||
We pledge to make our community welcoming, safe, and equitable for all.
|
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 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.
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
## Encouraged Behaviors
|
* 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
|
||||||
|
|
||||||
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.
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including:
|
* 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
|
||||||
|
|
||||||
1. Respecting the **purpose of our community**, our activities, and our ways of gathering.
|
## Our Responsibilities
|
||||||
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.
|
||||||
|
|
||||||
## Restricted Behaviors
|
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.
|
||||||
|
|
||||||
We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct.
|
## Scope
|
||||||
|
|
||||||
1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop.
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people.
|
when an individual is representing the project or its community. Examples of
|
||||||
3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits.
|
representing a project or community include using an official project e-mail
|
||||||
4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community.
|
address, posting via an official social media account, or acting as an appointed
|
||||||
5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission.
|
representative at an online or offline event. Representation of a project may be
|
||||||
6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group.
|
further defined and clarified by project maintainers.
|
||||||
7. Behaving in other ways that **threaten the well-being** of our community.
|
|
||||||
|
|
||||||
### Other Restrictions
|
## Enforcement
|
||||||
|
|
||||||
1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
2. **Failing to credit sources.** Not properly crediting the sources of content you contribute.
|
reported by contacting the project team at vexorian@gmail.com. All
|
||||||
3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community.
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors.
|
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.
|
||||||
|
|
||||||
## 'AI' policy
|
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
|
||||||
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.
|
members of the project's leadership.
|
||||||
|
|
||||||
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
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 3.0,
|
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 . The AI policy is a modification.
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
FROM node:12.18-alpine3.12
|
FROM node:12.18-alpine3.12
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
COPY package.json ./
|
COPY package*.json ./
|
||||||
RUN npm install && npm install -g browserify nexe@3.3.7
|
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 --from=vexorian/dizquetv:nexecache /var/nexe/linux-x64-12.16.2 /var/nexe/
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:14-alpine3.14
|
FROM node:12.18-alpine3.12
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install && npm install -g browserify nexe@3.3.7
|
RUN npm install && npm install -g browserify nexe@3.3.7
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM node:14-alpine3.14
|
FROM node:12.18-alpine3.12
|
||||||
WORKDIR /home/node/app
|
WORKDIR /home/node/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install && npm install -g browserify nexe@3.3.7
|
RUN npm install && npm install -g browserify nexe@3.3.7
|
||||||
|
|||||||
14
Dockerfile-qsv
Normal file
14
Dockerfile-qsv
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM node:12.18-alpine3.12
|
||||||
|
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 wzhy1234/ffmpeg-qsv
|
||||||
|
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
|
||||||
15
README.md
15
README.md
@ -1,4 +1,4 @@
|
|||||||
# dizqueTV 1.5.5
|
# dizqueTV 1.4.6-development
|
||||||
  
|
  
|
||||||
|
|
||||||
Create live TV channel streams from media on your Plex servers.
|
Create live TV channel streams from media on your Plex servers.
|
||||||
@ -25,7 +25,6 @@ EPG (Guide Information) data is stored to `.dizquetv/xmltv.xml`
|
|||||||
- Subtitle support.
|
- Subtitle support.
|
||||||
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
|
- Auto deinterlace any Plex media not marked `"scanType": "progressive"`
|
||||||
- Can be configured to completely force Direct play, if you are ready for the caveats.
|
- 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
|
## Limitations
|
||||||
|
|
||||||
@ -33,7 +32,6 @@ 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'll have to update the server settings manually in that case.
|
- 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'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.
|
- 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's contents.
|
- If you configure Plex DVR, it will always be recording and transcoding the channel'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
|
## Releases
|
||||||
|
|
||||||
@ -76,14 +74,3 @@ 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)
|
* 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
|
* 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 +0,0 @@
|
|||||||
module.exports = {extends: ['@commitlint/config-conventional']}
|
|
||||||
154
index.js
154
index.js
@ -1,14 +1,10 @@
|
|||||||
|
|
||||||
const db = require('diskdb')
|
const db = require('diskdb')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const unzip = require('unzipper')
|
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const bodyParser = require('body-parser')
|
const bodyParser = require('body-parser')
|
||||||
const fileUpload = require('express-fileupload');
|
const fileUpload = require('express-fileupload');
|
||||||
const i18next = require('i18next');
|
|
||||||
const i18nextMiddleware = require('i18next-http-middleware/cjs');
|
|
||||||
const i18nextBackend = require('i18next-fs-backend/cjs');
|
|
||||||
|
|
||||||
const api = require('./src/api')
|
const api = require('./src/api')
|
||||||
const dbMigration = require('./src/database-migration');
|
const dbMigration = require('./src/database-migration');
|
||||||
@ -16,10 +12,10 @@ const video = require('./src/video')
|
|||||||
const HDHR = require('./src/hdhr')
|
const HDHR = require('./src/hdhr')
|
||||||
const FileCacheService = require('./src/services/file-cache-service');
|
const FileCacheService = require('./src/services/file-cache-service');
|
||||||
const CacheImageService = require('./src/services/cache-image-service');
|
const CacheImageService = require('./src/services/cache-image-service');
|
||||||
const ChannelService = require("./src/services/channel-service");
|
|
||||||
|
|
||||||
const xmltv = require('./src/xmltv')
|
const xmltv = require('./src/xmltv')
|
||||||
const Plex = require('./src/plex');
|
const Plex = require('./src/plex');
|
||||||
|
const channelCache = require('./src/channel-cache');
|
||||||
const constants = require('./src/constants')
|
const constants = require('./src/constants')
|
||||||
const ChannelDB = require("./src/dao/channel-db");
|
const ChannelDB = require("./src/dao/channel-db");
|
||||||
const M3uService = require("./src/services/m3u-service");
|
const M3uService = require("./src/services/m3u-service");
|
||||||
@ -27,12 +23,6 @@ const FillerDB = require("./src/dao/filler-db");
|
|||||||
const CustomShowDB = require("./src/dao/custom-show-db");
|
const CustomShowDB = require("./src/dao/custom-show-db");
|
||||||
const TVGuideService = require("./src/services/tv-guide-service");
|
const TVGuideService = require("./src/services/tv-guide-service");
|
||||||
const EventService = require("./src/services/event-service");
|
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;
|
const onShutdown = require("node-graceful-shutdown").onShutdown;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -51,16 +41,12 @@ if (NODE < 12) {
|
|||||||
console.error(`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`);
|
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++) {
|
for (let i = 0, l = process.argv.length; i < l; i++) {
|
||||||
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)
|
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)
|
||||||
process.env.PORT = process.argv[i + 1]
|
process.env.PORT = process.argv[i + 1]
|
||||||
if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l)
|
if ((process.argv[i] === "-d" || process.argv[i] === "--database") && i + 1 !== l)
|
||||||
process.env.DATABASE = process.argv[i + 1]
|
process.env.DATABASE = process.argv[i + 1]
|
||||||
|
|
||||||
if (process.argv[i] === "--unlock") {
|
|
||||||
unlockPath = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv")
|
process.env.DATABASE = process.env.DATABASE || path.join(".", ".dizquetv")
|
||||||
@ -94,86 +80,51 @@ if(!fs.existsSync(path.join(process.env.DATABASE, 'cache','images'))) {
|
|||||||
|
|
||||||
|
|
||||||
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
|
channelDB = new ChannelDB( path.join(process.env.DATABASE, 'channels') );
|
||||||
|
fillerDB = new FillerDB( path.join(process.env.DATABASE, 'filler') , channelDB, channelCache );
|
||||||
|
|
||||||
|
customShowDB = new CustomShowDB( path.join(process.env.DATABASE, 'custom-shows') );
|
||||||
|
|
||||||
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id', 'cache-images', 'settings'])
|
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') );
|
fileCache = new FileCacheService( path.join(process.env.DATABASE, 'cache') );
|
||||||
cacheImageService = new CacheImageService(db, fileCache);
|
cacheImageService = new CacheImageService(db, fileCache);
|
||||||
m3uService = new M3uService(fileCache, channelService)
|
m3uService = new M3uService(channelDB, fileCache, channelCache)
|
||||||
|
|
||||||
onDemandService = new OnDemandService(channelService);
|
|
||||||
programmingService = new ProgrammingService(onDemandService);
|
|
||||||
activeChannelService = new ActiveChannelService(onDemandService, channelService);
|
|
||||||
|
|
||||||
eventService = new EventService();
|
eventService = new EventService();
|
||||||
|
|
||||||
i18next
|
initDB(db, channelDB)
|
||||||
.use(i18nextBackend)
|
|
||||||
.use(i18nextMiddleware.LanguageDetector)
|
|
||||||
.init({
|
|
||||||
// debug: true,
|
|
||||||
initImmediate: false,
|
|
||||||
backend: {
|
|
||||||
loadPath: path.join(__dirname, '/locales/server/{{lng}}.json'),
|
|
||||||
addPath: path.join(__dirname, '/locales/server/{{lng}}.json')
|
|
||||||
},
|
|
||||||
lng: 'en',
|
|
||||||
fallbackLng: 'en',
|
|
||||||
preload: ['en'],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const guideService = new TVGuideService(xmltv, db, cacheImageService, null, i18next);
|
const guideService = new TVGuideService(xmltv, db, cacheImageService);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let xmltvInterval = {
|
let xmltvInterval = {
|
||||||
interval: null,
|
interval: null,
|
||||||
lastRefresh: null,
|
lastRefresh: null,
|
||||||
updateXML: async () => {
|
updateXML: async () => {
|
||||||
|
let getChannelsCached = async() => {
|
||||||
|
let channelNumbers = await channelDB.getAllChannelNumbers();
|
||||||
|
return await Promise.all( channelNumbers.map( async (x) => {
|
||||||
|
return (await channelCache.getChannelConfig(channelDB, x))[0];
|
||||||
|
}) );
|
||||||
|
}
|
||||||
|
|
||||||
let channels = [];
|
let channels = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
channels = await channelService.getAllChannels();
|
channels = await getChannelsCached();
|
||||||
let xmltvSettings = db['xmltv-settings'].find()[0];
|
let xmltvSettings = db['xmltv-settings'].find()[0];
|
||||||
let t = guideService.prepareRefresh(channels, xmltvSettings.cache*60*60*1000);
|
let t = guideService.prepareRefresh(channels, xmltvSettings.cache*60*60*1000);
|
||||||
channels = null;
|
channels = null;
|
||||||
|
|
||||||
guideService.refresh(t);
|
await guideService.refresh(t);
|
||||||
|
xmltvInterval.lastRefresh = new Date()
|
||||||
|
console.log('XMLTV Updated at ', xmltvInterval.lastRefresh.toLocaleString());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Unable to update TV guide?", err);
|
console.error("Unable to update TV guide?", err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
channels = await getChannelsCached();
|
||||||
|
|
||||||
notifyPlex: async() => {
|
|
||||||
xmltvInterval.lastRefresh = new Date()
|
|
||||||
console.log('XMLTV Updated at ', xmltvInterval.lastRefresh.toLocaleString());
|
|
||||||
|
|
||||||
channels = await channelService.getAllChannels();
|
|
||||||
|
|
||||||
let plexServers = db['plex-servers'].find()
|
let plexServers = db['plex-servers'].find()
|
||||||
for (let i = 0, l = plexServers.length; i < l; i++) { // Foreach plex server
|
for (let i = 0, l = plexServers.length; i < l; i++) { // Foreach plex server
|
||||||
@ -204,7 +155,6 @@ let xmltvInterval = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
startInterval: () => {
|
startInterval: () => {
|
||||||
let xmltvSettings = db['xmltv-settings'].find()[0]
|
let xmltvSettings = db['xmltv-settings'].find()[0]
|
||||||
if (xmltvSettings.refresh !== 0) {
|
if (xmltvSettings.refresh !== 0) {
|
||||||
@ -224,39 +174,13 @@ let xmltvInterval = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guideService.on("xmltv-updated", (data) => {
|
|
||||||
try {
|
|
||||||
xmltvInterval.notifyPlex();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Unexpected issue when reacting to xmltv update", err);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
xmltvInterval.updateXML()
|
xmltvInterval.updateXML()
|
||||||
xmltvInterval.startInterval()
|
xmltvInterval.startInterval()
|
||||||
|
|
||||||
|
|
||||||
//setup xmltv update
|
|
||||||
channelService.on("channel-update", (data) => {
|
|
||||||
try {
|
|
||||||
console.log("Updating TV Guide due to channel update...");
|
|
||||||
//TODO: this could be smarter, like avoid updating 3 times if the channel was saved three times in a short time interval...
|
|
||||||
xmltvInterval.updateXML()
|
|
||||||
xmltvInterval.restartInterval()
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Unexpected error issuing TV Guide udpate", err);
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
|
|
||||||
|
|
||||||
let hdhr = HDHR(db, channelDB)
|
let hdhr = HDHR(db, channelDB)
|
||||||
let app = express()
|
let app = express()
|
||||||
eventService.setup(app);
|
eventService.setup(app);
|
||||||
|
|
||||||
app.use(
|
|
||||||
i18nextMiddleware.handle(i18next, {})
|
|
||||||
);
|
|
||||||
|
|
||||||
app.use(fileUpload({
|
app.use(fileUpload({
|
||||||
createParentPath: true
|
createParentPath: true
|
||||||
}));
|
}));
|
||||||
@ -290,12 +214,10 @@ app.use('/favicon.svg', express.static(
|
|||||||
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
|
app.use('/custom.css', express.static(path.join(process.env.DATABASE, 'custom.css')))
|
||||||
|
|
||||||
// API Routers
|
// API Routers
|
||||||
app.use(api.router(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService, ffmpegSettingsService))
|
app.use(api.router(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, m3uService, eventService ))
|
||||||
app.use('/api/cache/images', cacheImageService.apiRouters())
|
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, programPlayTimeDB ))
|
app.use(video.router( channelDB, fillerDB, db))
|
||||||
app.use(hdhr.router)
|
app.use(hdhr.router)
|
||||||
app.listen(process.env.PORT, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)
|
console.log(`HTTP server running on port: http://*:${process.env.PORT}`)
|
||||||
@ -305,7 +227,6 @@ app.listen(process.env.PORT, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function initDB(db, channelDB) {
|
function initDB(db, channelDB) {
|
||||||
//TODO: this is getting so repetitive, do it better
|
|
||||||
if (!fs.existsSync(process.env.DATABASE + '/images/dizquetv.png')) {
|
if (!fs.existsSync(process.env.DATABASE + '/images/dizquetv.png')) {
|
||||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/dizquetv.png')))
|
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/dizquetv.png')))
|
||||||
fs.writeFileSync(process.env.DATABASE + '/images/dizquetv.png', data)
|
fs.writeFileSync(process.env.DATABASE + '/images/dizquetv.png', data)
|
||||||
@ -335,32 +256,11 @@ function initDB(db, channelDB) {
|
|||||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
|
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
|
||||||
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
|
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') )) {
|
if (!fs.existsSync( path.join(process.env.DATABASE, 'custom.css') )) {
|
||||||
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources', 'default-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)
|
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 }));
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -377,7 +277,7 @@ async function sendEventAfterTime() {
|
|||||||
eventService.push(
|
eventService.push(
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
{
|
{
|
||||||
"message": i18next.t("event.server_started"),
|
"message": `Server Started`,
|
||||||
"detail" : {
|
"detail" : {
|
||||||
"time": t,
|
"time": t,
|
||||||
},
|
},
|
||||||
@ -396,7 +296,7 @@ onShutdown("log" , [], async() => {
|
|||||||
eventService.push(
|
eventService.push(
|
||||||
"lifecycle",
|
"lifecycle",
|
||||||
{
|
{
|
||||||
"message": i18next.t("event.server_shutdown"),
|
"message": `Initiated Server Shutdown`,
|
||||||
"detail" : {
|
"detail" : {
|
||||||
"time": t,
|
"time": t,
|
||||||
},
|
},
|
||||||
@ -410,10 +310,4 @@ onShutdown("log" , [], async() => {
|
|||||||
onShutdown("xmltv-writer" , [], async() => {
|
onShutdown("xmltv-writer" , [], async() => {
|
||||||
await xmltv.shutdown();
|
await xmltv.shutdown();
|
||||||
} );
|
} );
|
||||||
onShutdown("active-channels", [], async() => {
|
|
||||||
await activeChannelService.shutdown();
|
|
||||||
} );
|
|
||||||
|
|
||||||
onShutdown("video", [], async() => {
|
|
||||||
await video.shutdown();
|
|
||||||
} );
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -10,7 +10,6 @@ npm run build || exit 1
|
|||||||
npm run compile || exit 1
|
npm run compile || exit 1
|
||||||
cp -R ./web ./dist/web
|
cp -R ./web ./dist/web
|
||||||
cp -R ./resources ./dist/
|
cp -R ./resources ./dist/
|
||||||
cp -R ./locales/ ./dist/locales/
|
|
||||||
cd dist
|
cd dist
|
||||||
if [ "$MODE" == "all" ]; then
|
if [ "$MODE" == "all" ]; then
|
||||||
nexe --temp /var/nexe -r "./**/*" -t windows-x64-12.18.2 --output $WIN64
|
nexe --temp /var/nexe -r "./**/*" -t windows-x64-12.18.2 --output $WIN64
|
||||||
|
|||||||
8108
package-lock.json
generated
Normal file
8108
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -16,29 +16,21 @@
|
|||||||
"author": "vexorian",
|
"author": "vexorian",
|
||||||
"license": "Zlib",
|
"license": "Zlib",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"JSONStream": "1.0.5",
|
||||||
"angular": "^1.8.0",
|
"angular": "^1.8.0",
|
||||||
"angular-router-browserify": "0.0.2",
|
"angular-router-browserify": "0.0.2",
|
||||||
"angular-sanitize": "^1.8.2",
|
|
||||||
"angular-vs-repeat": "2.0.13",
|
"angular-vs-repeat": "2.0.13",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
|
"merge" : "2.1.1",
|
||||||
"diskdb": "0.1.17",
|
"diskdb": "0.1.17",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-fileupload": "^1.2.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-graceful-shutdown": "1.1.0",
|
||||||
"node-ssdp": "^4.0.0",
|
"node-ssdp": "^4.0.0",
|
||||||
"quickselect": "2.0.0",
|
|
||||||
"random-js": "2.1.0",
|
"random-js": "2.1.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"uuid": "9.0.1",
|
"uuid": "^8.0.0",
|
||||||
"unzipper": "0.10.14",
|
|
||||||
"xml-writer": "^1.7.0"
|
"xml-writer": "^1.7.0"
|
||||||
},
|
},
|
||||||
"bin": "dist/index.js",
|
"bin": "dist/index.js",
|
||||||
@ -47,14 +39,12 @@
|
|||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.5",
|
||||||
"@commitlint/cli": "^12.1.4",
|
|
||||||
"@commitlint/config-conventional": "^12.1.4",
|
|
||||||
"browserify": "^16.5.1",
|
"browserify": "^16.5.1",
|
||||||
"copyfiles": "^2.2.0",
|
"copyfiles": "^2.2.0",
|
||||||
"del-cli": "^3.0.0",
|
"del-cli": "^3.0.0",
|
||||||
"nexe": "^3.3.7",
|
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
"watchify": "^3.11.1"
|
"watchify": "^3.11.1",
|
||||||
|
"nexe": "^3.3.7"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|||||||
@ -7,8 +7,8 @@
|
|||||||
* [ ] I have read the code of conduct.
|
* [ ] I have read the code of conduct.
|
||||||
* [ ] I am submitting to the correct base branch
|
* [ ] I am submitting to the correct base branch
|
||||||
<!--
|
<!--
|
||||||
* Bug fixes for 'stable' versions must go to `patch`.
|
* Bug fixes must go to `dev/1.4.x`.
|
||||||
* New features and fixes for 'edge' version must go to `development`.
|
* New features must go to `dev/1.5.x`.
|
||||||
-->
|
-->
|
||||||
### Changes that modify the db structure
|
### Changes that modify the db structure
|
||||||
|
|
||||||
@ -19,7 +19,3 @@
|
|||||||
|
|
||||||
* [ ] 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.
|
* [ ] 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. -->
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Binary file not shown.
121
src/api.js
121
src/api.js
@ -2,6 +2,8 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const databaseMigration = require('./database-migration');
|
||||||
|
const channelCache = require('./channel-cache')
|
||||||
const constants = require('./constants');
|
const constants = require('./constants');
|
||||||
const JSONStream = require('JSONStream');
|
const JSONStream = require('JSONStream');
|
||||||
const FFMPEGInfo = require('./ffmpeg-info');
|
const FFMPEGInfo = require('./ffmpeg-info');
|
||||||
@ -24,10 +26,10 @@ function safeString(object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { router: api }
|
module.exports = { router: api }
|
||||||
function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService, ffmpegSettingsService ) {
|
function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService, _m3uService, eventService ) {
|
||||||
let m3uService = _m3uService;
|
let m3uService = _m3uService;
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const plexServerDB = new PlexServerDB(channelService, fillerDB, customShowDB, db);
|
const plexServerDB = new PlexServerDB(channelDB, channelCache, fillerDB, customShowDB, db);
|
||||||
|
|
||||||
router.get('/api/version', async (req, res) => {
|
router.get('/api/version', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -61,7 +63,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
});
|
});
|
||||||
if (servers.length != 1) {
|
if (servers.length != 1) {
|
||||||
return res.status(404).send(req.t("api.plex_server_not_found"));
|
return res.status(404).send("Plex server not found.");
|
||||||
}
|
}
|
||||||
let plex = new Plex(servers[0]);
|
let plex = new Plex(servers[0]);
|
||||||
let s = await Promise.race( [
|
let s = await Promise.race( [
|
||||||
@ -221,7 +223,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
// Channels
|
// Channels
|
||||||
router.get('/api/channels', async (req, res) => {
|
router.get('/api/channels', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let channels = await channelService.getAllChannelNumbers();
|
let channels = await channelDB.getAllChannels();
|
||||||
channels.sort((a, b) => { return a.number < b.number ? -1 : 1 })
|
channels.sort((a, b) => { return a.number < b.number ? -1 : 1 })
|
||||||
res.send(channels)
|
res.send(channels)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@ -232,9 +234,10 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
router.get('/api/channel/:number', async (req, res) => {
|
router.get('/api/channel/:number', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let number = parseInt(req.params.number, 10);
|
let number = parseInt(req.params.number, 10);
|
||||||
let channel = await channelService.getChannel(number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel.length == 1) {
|
||||||
|
channel = channel[0];
|
||||||
res.send(channel);
|
res.send(channel);
|
||||||
} else {
|
} else {
|
||||||
return res.status(404).send("Channel not found");
|
return res.status(404).send("Channel not found");
|
||||||
@ -247,9 +250,10 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
router.get('/api/channel/programless/:number', async (req, res) => {
|
router.get('/api/channel/programless/:number', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let number = parseInt(req.params.number, 10);
|
let number = parseInt(req.params.number, 10);
|
||||||
let channel = await channelService.getChannel(number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel.length == 1) {
|
||||||
|
channel = channel[0];
|
||||||
let copy = {};
|
let copy = {};
|
||||||
Object.keys(channel).forEach( (key) => {
|
Object.keys(channel).forEach( (key) => {
|
||||||
if (key != 'programs') {
|
if (key != 'programs') {
|
||||||
@ -269,9 +273,10 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
router.get('/api/channel/programs/:number', async (req, res) => {
|
router.get('/api/channel/programs/:number', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let number = parseInt(req.params.number, 10);
|
let number = parseInt(req.params.number, 10);
|
||||||
let channel = await channelService.getChannel(number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel.length == 1) {
|
||||||
|
channel = channel[0];
|
||||||
let programs = channel.programs;
|
let programs = channel.programs;
|
||||||
if (typeof(programs) === 'undefined') {
|
if (typeof(programs) === 'undefined') {
|
||||||
return res.status(404).send("Channel doesn't have programs?");
|
return res.status(404).send("Channel doesn't have programs?");
|
||||||
@ -300,8 +305,9 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
router.get('/api/channel/description/:number', async (req, res) => {
|
router.get('/api/channel/description/:number', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let number = parseInt(req.params.number, 10);
|
let number = parseInt(req.params.number, 10);
|
||||||
let channel = await channelService.getChannel(number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
if (channel != null) {
|
if (channel.length == 1) {
|
||||||
|
channel = channel[0];
|
||||||
res.send({
|
res.send({
|
||||||
number: channel.number,
|
number: channel.number,
|
||||||
icon: channel.icon,
|
icon: channel.icon,
|
||||||
@ -318,7 +324,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
})
|
})
|
||||||
router.get('/api/channelNumbers', async (req, res) => {
|
router.get('/api/channelNumbers', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let channels = await channelService.getAllChannelNumbers();
|
let channels = await channelDB.getAllChannelNumbers();
|
||||||
channels.sort( (a,b) => { return parseInt(a) - parseInt(b) } );
|
channels.sort( (a,b) => { return parseInt(a) - parseInt(b) } );
|
||||||
res.send(channels)
|
res.send(channels)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@ -326,30 +332,39 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
res.status(500).send("error");
|
res.status(500).send("error");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// we urgently need an actual channel service
|
|
||||||
router.post('/api/channel', async (req, res) => {
|
router.post('/api/channel', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await channelService.saveChannel( req.body.number, req.body );
|
await m3uService.clearCache();
|
||||||
|
cleanUpChannel(req.body);
|
||||||
|
await channelDB.saveChannel( req.body.number, req.body );
|
||||||
|
channelCache.clear();
|
||||||
res.send( { number: req.body.number} )
|
res.send( { number: req.body.number} )
|
||||||
|
updateXmltv()
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send("error");
|
res.status(500).send("error");
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
router.put('/api/channel', async (req, res) => {
|
router.put('/api/channel', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await channelService.saveChannel( req.body.number, req.body );
|
await m3uService.clearCache();
|
||||||
|
cleanUpChannel(req.body);
|
||||||
|
await channelDB.saveChannel( req.body.number, req.body );
|
||||||
|
channelCache.clear();
|
||||||
res.send( { number: req.body.number} )
|
res.send( { number: req.body.number} )
|
||||||
|
updateXmltv()
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send("error");
|
res.status(500).send("error");
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
router.delete('/api/channel', async (req, res) => {
|
router.delete('/api/channel', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await channelService.deleteChannel(req.body.number);
|
await m3uService.clearCache();
|
||||||
|
await channelDB.deleteChannel( req.body.number );
|
||||||
|
channelCache.clear();
|
||||||
res.send( { number: req.body.number} )
|
res.send( { number: req.body.number} )
|
||||||
|
updateXmltv()
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send("error");
|
res.status(500).send("error");
|
||||||
@ -528,7 +543,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
// FFMPEG SETTINGS
|
// FFMPEG SETTINGS
|
||||||
router.get('/api/ffmpeg-settings', (req, res) => {
|
router.get('/api/ffmpeg-settings', (req, res) => {
|
||||||
try {
|
try {
|
||||||
let ffmpeg = ffmpegSettingsService.get();
|
let ffmpeg = db['ffmpeg-settings'].find()[0]
|
||||||
res.send(ffmpeg)
|
res.send(ffmpeg)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -537,9 +552,9 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
})
|
})
|
||||||
router.put('/api/ffmpeg-settings', (req, res) => {
|
router.put('/api/ffmpeg-settings', (req, res) => {
|
||||||
try {
|
try {
|
||||||
let result = ffmpegSettingsService.update(req.body);
|
db['ffmpeg-settings'].update({ _id: req.body._id }, req.body)
|
||||||
let err = result.error
|
let ffmpeg = db['ffmpeg-settings'].find()[0]
|
||||||
|
let err = fixupFFMPEGSettings(ffmpeg);
|
||||||
if (typeof(err) !== 'undefined') {
|
if (typeof(err) !== 'undefined') {
|
||||||
return res.status(400).send(err);
|
return res.status(400).send(err);
|
||||||
}
|
}
|
||||||
@ -554,7 +569,7 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
"level" : "info"
|
"level" : "info"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
res.send(result.ffmpeg)
|
res.send(ffmpeg)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send("error");
|
res.status(500).send("error");
|
||||||
@ -575,8 +590,10 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
})
|
})
|
||||||
router.post('/api/ffmpeg-settings', (req, res) => { // RESET
|
router.post('/api/ffmpeg-settings', (req, res) => { // RESET
|
||||||
try {
|
try {
|
||||||
let ffmpeg = ffmpegSettingsService.reset();
|
let ffmpeg = databaseMigration.defaultFFMPEG() ;
|
||||||
|
ffmpeg.ffmpegPath = req.body.ffmpegPath;
|
||||||
|
db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg)
|
||||||
|
ffmpeg = db['ffmpeg-settings'].find()[0]
|
||||||
eventService.push(
|
eventService.push(
|
||||||
"settings-update",
|
"settings-update",
|
||||||
{
|
{
|
||||||
@ -609,6 +626,14 @@ 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
|
// PLEX SETTINGS
|
||||||
router.get('/api/plex-settings', (req, res) => {
|
router.get('/api/plex-settings', (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -1022,11 +1047,53 @@ function api(db, channelService, fillerDB, customShowDB, xmltvInterval, guideSe
|
|||||||
xmltvInterval.updateXML()
|
xmltvInterval.updateXML()
|
||||||
xmltvInterval.restartInterval()
|
xmltvInterval.restartInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async function streamToolResult(toolRes, res) {
|
async function streamToolResult(toolRes, res) {
|
||||||
let programs = toolRes.programs;
|
let programs = toolRes.programs;
|
||||||
delete toolRes.programs;
|
delete toolRes.programs;
|
||||||
let s = JSON.stringify(toolRes);
|
let s = JSON.stringify(toolRes);
|
||||||
s = s.slice(0, -1);
|
s = s.slice(0, -1);
|
||||||
|
console.log( JSON.stringify(toolRes));
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
const SLACK = require('./constants').SLACK;
|
const SLACK = require('./constants').SLACK;
|
||||||
|
|
||||||
let cache = {};
|
let cache = {};
|
||||||
|
let programPlayTimeCache = {};
|
||||||
let fillerPlayTimeCache = {};
|
let fillerPlayTimeCache = {};
|
||||||
let configCache = {};
|
let configCache = {};
|
||||||
let numbers = null;
|
let numbers = null;
|
||||||
@ -14,15 +14,17 @@ async function getChannelConfig(channelDB, channelId) {
|
|||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
configCache[channelId] = [];
|
configCache[channelId] = [];
|
||||||
} else {
|
} else {
|
||||||
|
//console.log("channel=" + JSON.stringify(channel) );
|
||||||
configCache[channelId] = [channel];
|
configCache[channelId] = [channel];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//console.log("channel=" + JSON.stringify(configCache[channelId]).slice(0,200) );
|
||||||
return configCache[channelId];
|
return configCache[channelId];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAllNumbers(channelDB) {
|
async function getAllNumbers(channelDB) {
|
||||||
if (numbers === null) {
|
if (numbers === null) {
|
||||||
let n = await channelDB.getAllChannelNumbers();
|
let n = channelDB.getAllChannelNumbers();
|
||||||
numbers = n;
|
numbers = n;
|
||||||
}
|
}
|
||||||
return numbers;
|
return numbers;
|
||||||
@ -30,41 +32,14 @@ async function getAllNumbers(channelDB) {
|
|||||||
|
|
||||||
async function getAllChannels(channelDB) {
|
async function getAllChannels(channelDB) {
|
||||||
let channelNumbers = await getAllNumbers(channelDB);
|
let channelNumbers = await getAllNumbers(channelDB);
|
||||||
return (await Promise.all( channelNumbers.map( async (x) => {
|
return await Promise.all( channelNumbers.map( async (x) => {
|
||||||
return (await getChannelConfig(channelDB, x))[0];
|
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 ) {
|
function saveChannelConfig(number, channel ) {
|
||||||
configCache[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) {
|
function getCurrentLineupItem(channelId, t1) {
|
||||||
@ -104,7 +79,7 @@ function getCurrentLineupItem(channelId, t1) {
|
|||||||
return lineupItem;
|
return lineupItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProgramKey(program) {
|
function getKey(channelId, program) {
|
||||||
let serverKey = "!unknown!";
|
let serverKey = "!unknown!";
|
||||||
if (typeof(program.serverKey) !== 'undefined') {
|
if (typeof(program.serverKey) !== 'undefined') {
|
||||||
if (typeof(program.serverKey) !== 'undefined') {
|
if (typeof(program.serverKey) !== 'undefined') {
|
||||||
@ -115,9 +90,9 @@ function getProgramKey(program) {
|
|||||||
if (typeof(program.key) !== 'undefined') {
|
if (typeof(program.key) !== 'undefined') {
|
||||||
programKey = program.key;
|
programKey = program.key;
|
||||||
}
|
}
|
||||||
return serverKey + "|" + programKey;
|
return channelId + "|" + serverKey + "|" + programKey;
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function getFillerKey(channelId, fillerId) {
|
function getFillerKey(channelId, fillerId) {
|
||||||
return channelId + "|" + fillerId;
|
return channelId + "|" + fillerId;
|
||||||
@ -125,27 +100,26 @@ function getFillerKey(channelId, fillerId) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
function recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0) {
|
function recordProgramPlayTime(channelId, lineupItem, t0) {
|
||||||
let remaining;
|
let remaining;
|
||||||
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
|
if ( typeof(lineupItem.streamDuration) !== 'undefined') {
|
||||||
remaining = lineupItem.streamDuration;
|
remaining = lineupItem.streamDuration;
|
||||||
} else {
|
} else {
|
||||||
remaining = lineupItem.duration - lineupItem.start;
|
remaining = lineupItem.duration - lineupItem.start;
|
||||||
}
|
}
|
||||||
setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t0 + remaining);
|
programPlayTimeCache[ getKey(channelId, lineupItem) ] = t0 + remaining;
|
||||||
if (typeof(lineupItem.fillerId) !== 'undefined') {
|
if (typeof(lineupItem.fillerId) !== 'undefined') {
|
||||||
fillerPlayTimeCache[ getFillerKey(channelId, lineupItem.fillerId) ] = t0 + remaining;
|
fillerPlayTimeCache[ getFillerKey(channelId, lineupItem.fillerId) ] = t0 + remaining;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProgramLastPlayTime(programPlayTime, channelId, lineupItem, t) {
|
function getProgramLastPlayTime(channelId, program) {
|
||||||
let programKey = getProgramKey(lineupItem);
|
let v = programPlayTimeCache[ getKey(channelId, program) ];
|
||||||
programPlayTime.update(channelId, programKey, t);
|
if (typeof(v) === 'undefined') {
|
||||||
}
|
return 0;
|
||||||
|
} else {
|
||||||
function getProgramLastPlayTime(programPlayTime, channelId, program) {
|
return v;
|
||||||
let programKey = getProgramKey(program);
|
}
|
||||||
return programPlayTime.getProgramLastPlayTime(channelId, programKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFillerLastPlayTime(channelId, fillerId) {
|
function getFillerLastPlayTime(channelId, fillerId) {
|
||||||
@ -157,8 +131,8 @@ function getFillerLastPlayTime(channelId, fillerId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recordPlayback(programPlayTime, channelId, t0, lineupItem) {
|
function recordPlayback(channelId, t0, lineupItem) {
|
||||||
recordProgramPlayTime(programPlayTime, channelId, lineupItem, t0);
|
recordProgramPlayTime(channelId, lineupItem, t0);
|
||||||
|
|
||||||
cache[channelId] = {
|
cache[channelId] = {
|
||||||
t0: t0,
|
t0: t0,
|
||||||
@ -183,7 +157,6 @@ module.exports = {
|
|||||||
clear: clear,
|
clear: clear,
|
||||||
getProgramLastPlayTime: getProgramLastPlayTime,
|
getProgramLastPlayTime: getProgramLastPlayTime,
|
||||||
getAllChannels: getAllChannels,
|
getAllChannels: getAllChannels,
|
||||||
getAllNumbers: getAllNumbers,
|
|
||||||
getChannelConfig: getChannelConfig,
|
getChannelConfig: getChannelConfig,
|
||||||
saveChannelConfig: saveChannelConfig,
|
saveChannelConfig: saveChannelConfig,
|
||||||
getFillerLastPlayTime: getFillerLastPlayTime,
|
getFillerLastPlayTime: getFillerLastPlayTime,
|
||||||
|
|||||||
@ -5,35 +5,5 @@ module.exports = {
|
|||||||
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
|
TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000,
|
||||||
TOO_FREQUENT: 1000,
|
TOO_FREQUENT: 1000,
|
||||||
|
|
||||||
// Duration of things like the loading screen and the interlude (the black
|
VERSION_NAME: "1.4.6-development"
|
||||||
// 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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,11 @@ let fs = require('fs');
|
|||||||
|
|
||||||
class FillerDB {
|
class FillerDB {
|
||||||
|
|
||||||
constructor(folder, channelService) {
|
constructor(folder, channelDB, channelCache) {
|
||||||
this.folder = folder;
|
this.folder = folder;
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.channelService = channelService;
|
this.channelDB = channelDB;
|
||||||
|
this.channelCache = channelCache;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -78,10 +79,10 @@ class FillerDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFillerChannels(id) {
|
async getFillerChannels(id) {
|
||||||
let numbers = await this.channelService.getAllChannelNumbers();
|
let numbers = await this.channelDB.getAllChannelNumbers();
|
||||||
let channels = [];
|
let channels = [];
|
||||||
await Promise.all( numbers.map( async(number) => {
|
await Promise.all( numbers.map( async(number) => {
|
||||||
let ch = await this.channelService.getChannel(number);
|
let ch = await this.channelDB.getChannel(number);
|
||||||
let name = ch.name;
|
let name = ch.name;
|
||||||
let fillerCollections = ch.fillerCollections;
|
let fillerCollections = ch.fillerCollections;
|
||||||
for (let i = 0 ; i < fillerCollections.length; i++) {
|
for (let i = 0 ; i < fillerCollections.length; i++) {
|
||||||
@ -104,13 +105,13 @@ class FillerDB {
|
|||||||
let channels = await this.getFillerChannels(id);
|
let channels = await this.getFillerChannels(id);
|
||||||
await Promise.all( channels.map( async(channel) => {
|
await Promise.all( channels.map( async(channel) => {
|
||||||
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
|
console.log(`Updating channel ${channel.number} , remove filler: ${id}`);
|
||||||
let json = await channelService.getChannel(channel.number);
|
let json = await channelDB.getChannel(channel.number);
|
||||||
json.fillerCollections = json.fillerCollections.filter( (col) => {
|
json.fillerCollections = json.fillerCollections.filter( (col) => {
|
||||||
return col.id != id;
|
return col.id != id;
|
||||||
} );
|
} );
|
||||||
await this.channelService.saveChannel( channel.number, json );
|
await this.channelDB.saveChannel( channel.number, json );
|
||||||
} ) );
|
} ) );
|
||||||
|
this.channelCache.clear();
|
||||||
let f = path.join(this.folder, `${id}.json` );
|
let f = path.join(this.folder, `${id}.json` );
|
||||||
await new Promise( (resolve, reject) => {
|
await new Promise( (resolve, reject) => {
|
||||||
fs.unlink(f, function (err) {
|
fs.unlink(f, function (err) {
|
||||||
|
|||||||
@ -4,21 +4,20 @@ const ICON_REGEX = /https?:\/\/.*(\/library\/metadata\/\d+\/thumb\/\d+).X-Plex-T
|
|||||||
|
|
||||||
const ICON_FIELDS = ["icon", "showIcon", "seasonIcon", "episodeIcon"];
|
const ICON_FIELDS = ["icon", "showIcon", "seasonIcon", "episodeIcon"];
|
||||||
|
|
||||||
// DB is a misnomer here, this is closer to a service
|
|
||||||
class PlexServerDB
|
class PlexServerDB
|
||||||
{
|
{
|
||||||
constructor(channelService, fillerDB, showDB, db) {
|
constructor(channelDB, channelCache, fillerDB, showDB, db) {
|
||||||
this.channelService = channelService;
|
this.channelDB = channelDB;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.channelCache = channelCache;
|
||||||
this.fillerDB = fillerDB;
|
this.fillerDB = fillerDB;
|
||||||
this.showDB = showDB;
|
this.showDB = showDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fixupAllChannels(name, newServer) {
|
async fixupAllChannels(name, newServer) {
|
||||||
let channelNumbers = await this.channelService.getAllChannelNumbers();
|
let channelNumbers = await this.channelDB.getAllChannelNumbers();
|
||||||
let report = await Promise.all( channelNumbers.map( async (i) => {
|
let report = await Promise.all( channelNumbers.map( async (i) => {
|
||||||
let channel = await this.channelService.getChannel(i);
|
let channel = await this.channelDB.getChannel(i);
|
||||||
let channelReport = {
|
let channelReport = {
|
||||||
channelNumber : channel.number,
|
channelNumber : channel.number,
|
||||||
channelName : channel.name,
|
channelName : channel.name,
|
||||||
@ -39,10 +38,10 @@ class PlexServerDB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.fixupProgramArray(channel.fallback, name,newServer, channelReport);
|
this.fixupProgramArray(channel.fallback, name,newServer, channelReport);
|
||||||
await this.channelService.saveChannel(i, channel);
|
await this.channelDB.saveChannel(i, channel);
|
||||||
return channelReport;
|
return channelReport;
|
||||||
}) );
|
}) );
|
||||||
|
this.channelCache.clear();
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -20,8 +20,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
|
|
||||||
const TARGET_VERSION = 805;
|
const TARGET_VERSION = 803;
|
||||||
const DAY_MS = 1000 * 60 * 60 * 24;
|
|
||||||
|
|
||||||
const STEPS = [
|
const STEPS = [
|
||||||
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
|
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
|
||||||
@ -44,8 +43,6 @@ const STEPS = [
|
|||||||
[ 800, 801, (db) => addImageCache(db) ],
|
[ 800, 801, (db) => addImageCache(db) ],
|
||||||
[ 801, 802, () => addGroupTitle() ],
|
[ 801, 802, () => addGroupTitle() ],
|
||||||
[ 802, 803, () => fixNonIntegerDurations() ],
|
[ 802, 803, () => fixNonIntegerDurations() ],
|
||||||
[ 803, 805, (db) => addFFMpegLock(db) ],
|
|
||||||
[ 804, 805, (db) => addFFMpegLock(db) ],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
@ -387,7 +384,6 @@ function ffmpeg() {
|
|||||||
//How default ffmpeg settings should look
|
//How default ffmpeg settings should look
|
||||||
configVersion: 5,
|
configVersion: 5,
|
||||||
ffmpegPath: "/usr/bin/ffmpeg",
|
ffmpegPath: "/usr/bin/ffmpeg",
|
||||||
ffmpegPathLockDate: new Date().getTime() + DAY_MS,
|
|
||||||
threads: 4,
|
threads: 4,
|
||||||
concatMuxDelay: "0",
|
concatMuxDelay: "0",
|
||||||
logFfmpeg: false,
|
logFfmpeg: false,
|
||||||
@ -769,19 +765,6 @@ function addScalingAlgorithm(db) {
|
|||||||
fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) );
|
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) {
|
function moveBackup(path) {
|
||||||
if (fs.existsSync(`${process.env.DATABASE}${path}`) ) {
|
if (fs.existsSync(`${process.env.DATABASE}${path}`) ) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class FFMPEGInfo {
|
|||||||
var m = s.match( /version\s+([^\s]+)\s+.*Copyright/ )
|
var m = s.match( /version\s+([^\s]+)\s+.*Copyright/ )
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
console.error("ffmpeg -version command output not in the expected format: " + s);
|
console.error("ffmpeg -version command output not in the expected format: " + s);
|
||||||
return "Unknown";
|
return s;
|
||||||
}
|
}
|
||||||
return m[1];
|
return m[1];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -198,9 +198,8 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
iW = this.wantedW;
|
iW = this.wantedW;
|
||||||
iH = this.wantedH;
|
iH = this.wantedH;
|
||||||
|
|
||||||
let durstr = `duration=${streamStats.duration}ms`;
|
|
||||||
|
|
||||||
if (this.audioOnly !== true) {
|
if (this.audioOnly !== true) {
|
||||||
|
ffmpegArgs.push("-r" , "24");
|
||||||
let pic = null;
|
let pic = null;
|
||||||
|
|
||||||
//does an image to play exist?
|
//does an image to play exist?
|
||||||
@ -217,11 +216,6 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pic != null) {
|
if (pic != null) {
|
||||||
if (this.opts.noRealTime === true) {
|
|
||||||
ffmpegArgs.push("-r" , "60");
|
|
||||||
} else {
|
|
||||||
ffmpegArgs.push("-r" , "24");
|
|
||||||
}
|
|
||||||
ffmpegArgs.push(
|
ffmpegArgs.push(
|
||||||
'-i', pic,
|
'-i', pic,
|
||||||
);
|
);
|
||||||
@ -233,20 +227,14 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
//add 150 milliseconds just in case, exact duration seems to cut out the last bits of music some times.
|
//add 150 milliseconds just in case, exact duration seems to cut out the last bits of music some times.
|
||||||
duration = `${streamStats.duration + 150}ms`;
|
duration = `${streamStats.duration + 150}ms`;
|
||||||
}
|
}
|
||||||
videoComplex = `;[${inputFiles++}:0]format=yuv420p[formatted]`;
|
videoComplex = `;[${inputFiles++}:0]format=nv12[formatted]`;
|
||||||
videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`;
|
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 += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`;
|
||||||
videoComplex += `;[padded]loop=loop=-1:size=1:start=0`;
|
videoComplex += `;[padded]loop=loop=-1:size=1:start=0[looped]`;
|
||||||
if (this.opts.noRealTime !== true) {
|
videoComplex +=`;[looped]realtime[videox]`;
|
||||||
videoComplex +=`[looped];[looped]realtime[videox]`;
|
|
||||||
} else {
|
|
||||||
videoComplex +=`[videox]`
|
|
||||||
}
|
|
||||||
//this tune apparently makes the video compress better
|
//this tune apparently makes the video compress better
|
||||||
// when it is the same image
|
// when it is the same image
|
||||||
stillImage = true;
|
stillImage = true;
|
||||||
this.volumePercent = Math.min(70, this.volumePercent);
|
|
||||||
|
|
||||||
} else if (this.opts.errorScreen == 'static') {
|
} else if (this.opts.errorScreen == 'static') {
|
||||||
ffmpegArgs.push(
|
ffmpegArgs.push(
|
||||||
'-f', 'lavfi',
|
'-f', 'lavfi',
|
||||||
@ -281,7 +269,7 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
videoComplex = `;realtime[videox]`;
|
videoComplex = `;realtime[videox]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let durstr = `duration=${streamStats.duration}ms`;
|
||||||
if (typeof(streamUrl.errorTitle) !== 'undefined') {
|
if (typeof(streamUrl.errorTitle) !== 'undefined') {
|
||||||
//silent
|
//silent
|
||||||
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
|
audioComplex = `;aevalsrc=0:${durstr}[audioy]`;
|
||||||
@ -483,8 +471,7 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
`-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
|
`-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'),
|
||||||
`-sc_threshold`, `1000000000`,
|
`-sc_threshold`, `1000000000`,
|
||||||
);
|
);
|
||||||
// do not use -tune stillimage for nv
|
if (stillImage) {
|
||||||
if (stillImage && ! this.opts.videoEncoder.toLowerCase().includes("nv") ) {
|
|
||||||
ffmpegArgs.push('-tune', 'stillimage');
|
ffmpegArgs.push('-tune', 'stillimage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -495,17 +482,10 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
if ( transcodeVideo && (this.audioOnly !== true) ) {
|
if ( transcodeVideo && (this.audioOnly !== true) ) {
|
||||||
// add the video encoder flags
|
// add the video encoder flags
|
||||||
ffmpegArgs.push(
|
ffmpegArgs.push(
|
||||||
'-crf', '22',
|
`-b:v`, `${this.opts.videoBitrate}k`,
|
||||||
`-maxrate:v`, `${this.opts.videoBitrate}k`,
|
`-maxrate:v`, `${this.opts.videoBitrate}k`,
|
||||||
`-bufsize:v`, `${this.opts.videoBufSize}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 ) {
|
if ( transcodeAudio ) {
|
||||||
// add the audio encoder flags
|
// add the audio encoder flags
|
||||||
@ -569,7 +549,6 @@ class FFMPEG extends events.EventEmitter {
|
|||||||
if (this.hasBeenKilled) {
|
if (this.hasBeenKilled) {
|
||||||
return ;
|
return ;
|
||||||
}
|
}
|
||||||
//console.log(this.ffmpegPath + " " + ffmpegArgs.join(" ") );
|
|
||||||
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
|
this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } );
|
||||||
if (this.hasBeenKilled) {
|
if (this.hasBeenKilled) {
|
||||||
console.log("Send SIGKILL to ffmpeg");
|
console.log("Send SIGKILL to ffmpeg");
|
||||||
|
|||||||
@ -6,10 +6,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let channelCache = require('./channel-cache');
|
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 SLACK = require('./constants').SLACK;
|
||||||
const randomJS = require("random-js");
|
const randomJS = require("random-js");
|
||||||
const quickselect = require("quickselect");
|
|
||||||
const Random = randomJS.Random;
|
const Random = randomJS.Random;
|
||||||
const random = new Random( randomJS.MersenneTwister19937.autoSeed() );
|
const random = new Random( randomJS.MersenneTwister19937.autoSeed() );
|
||||||
|
|
||||||
@ -63,7 +61,7 @@ function getCurrentProgramAndTimeElapsed(date, channel) {
|
|||||||
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
|
return { program: channel.programs[currentProgramIndex], timeElapsed: timeElapsed, programIndex: currentProgramIndex }
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
function createLineup(obj, channel, fillers, isFirst) {
|
||||||
let timeElapsed = obj.timeElapsed
|
let timeElapsed = obj.timeElapsed
|
||||||
// Start time of a file is never consistent unless 0. Run time of an episode can vary.
|
// 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
|
// When within 30 seconds of start time, just make the time 0 to smooth things out
|
||||||
@ -98,7 +96,7 @@ function createLineup(programPlayTime, obj, channel, fillers, isFirst) {
|
|||||||
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
|
if ( (channel.offlineMode === 'clip') && (channel.fallback.length != 0) ) {
|
||||||
special = JSON.parse(JSON.stringify(channel.fallback[0]));
|
special = JSON.parse(JSON.stringify(channel.fallback[0]));
|
||||||
}
|
}
|
||||||
let randomResult = pickRandomWithMaxDuration(programPlayTime, channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
|
let randomResult = pickRandomWithMaxDuration(channel, fillers, remaining + (isFirst? (7*24*60*60*1000) : 0) );
|
||||||
filler = randomResult.filler;
|
filler = randomResult.filler;
|
||||||
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
if (filler == null && (typeof(randomResult.minimumWait) !== undefined) && (remaining > randomResult.minimumWait) ) {
|
||||||
remaining = randomResult.minimumWait;
|
remaining = randomResult.minimumWait;
|
||||||
@ -180,7 +178,7 @@ function weighedPick(a, total) {
|
|||||||
return random.bool(a, total);
|
return random.bool(a, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuration) {
|
function pickRandomWithMaxDuration(channel, fillers, maxDuration) {
|
||||||
let list = [];
|
let list = [];
|
||||||
for (let i = 0; i < fillers.length; i++) {
|
for (let i = 0; i < fillers.length; i++) {
|
||||||
list = list.concat(fillers[i].content);
|
list = list.concat(fillers[i].content);
|
||||||
@ -196,36 +194,16 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
}
|
}
|
||||||
let listM = 0;
|
let listM = 0;
|
||||||
let fillerId = undefined;
|
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;
|
list = fillers[j].content;
|
||||||
let pickedList = false;
|
let pickedList = false;
|
||||||
let n = 0;
|
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++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
let clip = list[i];
|
let clip = list[i];
|
||||||
// a few extra milliseconds won't hurt anyone, would it? dun dun dun
|
// a few extra milliseconds won't hurt anyone, would it? dun dun dun
|
||||||
if (clip.duration <= maxDuration + SLACK ) {
|
if (clip.duration <= maxDuration + SLACK ) {
|
||||||
let t1 = channelCache.getProgramLastPlayTime(programPlayTime, channel.number, clip );
|
let t1 = channelCache.getProgramLastPlayTime( channel.number, clip );
|
||||||
if (t1 > maximumPlayTimeAllowed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
let timeSince = ( (t1 == 0) ? D : (t0 - t1) );
|
||||||
|
|
||||||
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
|
if (timeSince < channel.fillerRepeatCooldown - SLACK) {
|
||||||
@ -269,13 +247,11 @@ function pickRandomWithMaxDuration(programPlayTime, channel, fillers, maxDuratio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (pick1 != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let pick = pick1;
|
let pick = pick1;
|
||||||
|
let pickTitle = "null";
|
||||||
if (pick != null) {
|
if (pick != null) {
|
||||||
|
pickTitle = pick.title;
|
||||||
pick = JSON.parse( JSON.stringify(pick) );
|
pick = JSON.parse( JSON.stringify(pick) );
|
||||||
pick.fillerId = fillerId;
|
pick.fillerId = fillerId;
|
||||||
}
|
}
|
||||||
@ -346,26 +322,6 @@ 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) {
|
function generateChannelContext(channel) {
|
||||||
let channelContext = {};
|
let channelContext = {};
|
||||||
for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) {
|
for (let i = 0; i < CHANNEL_CONTEXT_KEYS.length; i++) {
|
||||||
|
|||||||
@ -18,11 +18,6 @@ class OfflinePlayer {
|
|||||||
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`;
|
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`;
|
||||||
context.channel.offlineSoundtrack = undefined;
|
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 = new FFMPEG(context.ffmpegSettings, context.channel);
|
||||||
this.ffmpeg.setAudioOnly(this.context.audioOnly);
|
this.ffmpeg.setAudioOnly(this.context.audioOnly);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,6 @@ class ProgramPlayer {
|
|||||||
// people might want the codec normalization to stay because of player support
|
// people might want the codec normalization to stay because of player support
|
||||||
context.ffmpegSettings.normalizeResolution = false;
|
context.ffmpegSettings.normalizeResolution = false;
|
||||||
}
|
}
|
||||||
context.ffmpegSettings.noRealTime = program.noRealTime;
|
|
||||||
if ( typeof(program.err) !== 'undefined') {
|
if ( typeof(program.err) !== 'undefined') {
|
||||||
console.log("About to play error stream");
|
console.log("About to play error stream");
|
||||||
this.delegate = new OfflinePlayer(true, context);
|
this.delegate = new OfflinePlayer(true, context);
|
||||||
@ -43,11 +42,6 @@ class ProgramPlayer {
|
|||||||
/* loading */
|
/* loading */
|
||||||
context.isLoading = true;
|
context.isLoading = true;
|
||||||
this.delegate = new OfflinePlayer(false, context);
|
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') {
|
} else if (program.type === 'offline') {
|
||||||
console.log("About to play offline stream");
|
console.log("About to play offline stream");
|
||||||
/* offline */
|
/* offline */
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -12,7 +12,6 @@ module.exports = function () {
|
|||||||
showId : "custom." + program.customShowId,
|
showId : "custom." + program.customShowId,
|
||||||
showDisplayName : program.customShowName,
|
showDisplayName : program.customShowName,
|
||||||
order : program.customOrder,
|
order : program.customOrder,
|
||||||
shuffleOrder : program.shuffleOrder,
|
|
||||||
}
|
}
|
||||||
} else if (program.isOffline && program.type === 'redirect') {
|
} else if (program.isOffline && program.type === 'redirect') {
|
||||||
return {
|
return {
|
||||||
@ -36,7 +35,6 @@ module.exports = function () {
|
|||||||
showId : "movie.",
|
showId : "movie.",
|
||||||
showDisplayName : "Movies",
|
showDisplayName : "Movies",
|
||||||
order : movieTitleOrder[key],
|
order : movieTitleOrder[key],
|
||||||
shuffleOrder : program.shuffleOrder,
|
|
||||||
}
|
}
|
||||||
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
|
} else if ( (program.type === 'episode') || (program.type === 'track') ) {
|
||||||
let s = 0;
|
let s = 0;
|
||||||
@ -56,7 +54,6 @@ module.exports = function () {
|
|||||||
showId : prefix + program.showTitle,
|
showId : prefix + program.showTitle,
|
||||||
showDisplayName : program.showTitle,
|
showDisplayName : program.showTitle,
|
||||||
order : s * 1000000 + e,
|
order : s * 1000000 + e,
|
||||||
shuffleOrder : program.shuffleOrder,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -4,13 +4,11 @@
|
|||||||
* @class M3uService
|
* @class M3uService
|
||||||
*/
|
*/
|
||||||
class M3uService {
|
class M3uService {
|
||||||
constructor(fileCacheService, channelService) {
|
constructor(dataBase, fileCacheService, channelCache) {
|
||||||
this.channelService = channelService;
|
this.dataBase = dataBase;
|
||||||
this.cacheService = fileCacheService;
|
this.cacheService = fileCacheService;
|
||||||
|
this.channelCache = channelCache;
|
||||||
this.cacheReady = false;
|
this.cacheReady = false;
|
||||||
this.channelService.on("channel-update", (data) => {
|
|
||||||
this.clearCache();
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,7 +37,7 @@ class M3uService {
|
|||||||
return this.replaceHostOnM3u(host, cachedM3U);
|
return this.replaceHostOnM3u(host, cachedM3U);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let channels = await this.channelService.getAllChannels();
|
let channels = await this.channelCache.getAllChannels(this.dataBase);
|
||||||
|
|
||||||
|
|
||||||
channels.sort((a, b) => {
|
channels.sort((a, b) => {
|
||||||
|
|||||||
@ -1,226 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
@ -2,7 +2,7 @@ const constants = require("../constants");
|
|||||||
const getShowData = require("./get-show-data")();
|
const getShowData = require("./get-show-data")();
|
||||||
const random = require('../helperFuncs').random;
|
const random = require('../helperFuncs').random;
|
||||||
const throttle = require('./throttle');
|
const throttle = require('./throttle');
|
||||||
const orderers = require("./show-orderers");
|
|
||||||
|
|
||||||
const MINUTE = 60*1000;
|
const MINUTE = 60*1000;
|
||||||
const DAY = 24*60*MINUTE;
|
const DAY = 24*60*MINUTE;
|
||||||
@ -22,6 +22,29 @@ function getShow(program) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function shuffle(array, lo, hi ) {
|
||||||
|
if (typeof(lo) === 'undefined') {
|
||||||
|
lo = 0;
|
||||||
|
hi = array.length;
|
||||||
|
}
|
||||||
|
let currentIndex = hi, temporaryValue, randomIndex
|
||||||
|
while (lo !== currentIndex) {
|
||||||
|
randomIndex = random.integer(lo, currentIndex-1);
|
||||||
|
currentIndex -= 1
|
||||||
|
temporaryValue = array[currentIndex]
|
||||||
|
array[currentIndex] = array[randomIndex]
|
||||||
|
array[randomIndex] = temporaryValue
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wait(t) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getProgramId(program) {
|
function getProgramId(program) {
|
||||||
let s = program.serverKey;
|
let s = program.serverKey;
|
||||||
if (typeof(s) === 'undefined') {
|
if (typeof(s) === 'undefined') {
|
||||||
@ -46,6 +69,78 @@ function addProgramToShow(show, program) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 randomPrograms = JSON.parse( JSON.stringify(show.programs) );
|
||||||
|
let n = randomPrograms.length;
|
||||||
|
shuffle( randomPrograms, 0, n);
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
show.shuffler = {
|
||||||
|
|
||||||
|
current : () => {
|
||||||
|
return randomPrograms[position];
|
||||||
|
},
|
||||||
|
|
||||||
|
next: () => {
|
||||||
|
position++;
|
||||||
|
if (position == n) {
|
||||||
|
let a = Math.floor(n / 2);
|
||||||
|
shuffle(randomPrograms, 0, a );
|
||||||
|
shuffle(randomPrograms, a, n );
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return show.shuffler;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = async( programs, schedule ) => {
|
module.exports = async( programs, schedule ) => {
|
||||||
if (! Array.isArray(programs) ) {
|
if (! Array.isArray(programs) ) {
|
||||||
return { userError: 'Expected a programs array' };
|
return { userError: 'Expected a programs array' };
|
||||||
@ -97,6 +192,9 @@ module.exports = async( programs, schedule ) => {
|
|||||||
}
|
}
|
||||||
let flexBetween = ( schedule.flexPreference !== "end" );
|
let flexBetween = ( schedule.flexPreference !== "end" );
|
||||||
|
|
||||||
|
// throttle so that the stream is not affected negatively
|
||||||
|
let steps = 0;
|
||||||
|
|
||||||
let showsById = {};
|
let showsById = {};
|
||||||
let shows = [];
|
let shows = [];
|
||||||
|
|
||||||
@ -118,9 +216,9 @@ module.exports = async( programs, schedule ) => {
|
|||||||
channel: show.channel,
|
channel: show.channel,
|
||||||
}
|
}
|
||||||
} else if (slot.order === 'shuffle') {
|
} else if (slot.order === 'shuffle') {
|
||||||
return orderers.getShowShuffler(show).current();
|
return getShowShuffler(show).current();
|
||||||
} else if (slot.order === 'next') {
|
} else if (slot.order === 'next') {
|
||||||
return orderers.getShowOrderer(show).current();
|
return getShowOrderer(show).current();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +228,9 @@ module.exports = async( programs, schedule ) => {
|
|||||||
}
|
}
|
||||||
let show = shows[ showsById[slot.showId] ];
|
let show = shows[ showsById[slot.showId] ];
|
||||||
if (slot.order === 'shuffle') {
|
if (slot.order === 'shuffle') {
|
||||||
return orderers.getShowShuffler(show).next();
|
return getShowShuffler(show).next();
|
||||||
} else if (slot.order === 'next') {
|
} else if (slot.order === 'next') {
|
||||||
return orderers.getShowOrderer(show).next();
|
return getShowOrderer(show).next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,156 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ const constants = require("../constants");
|
|||||||
const getShowData = require("./get-show-data")();
|
const getShowData = require("./get-show-data")();
|
||||||
const random = require('../helperFuncs').random;
|
const random = require('../helperFuncs').random;
|
||||||
const throttle = require('./throttle');
|
const throttle = require('./throttle');
|
||||||
const orderers = require("./show-orderers");
|
|
||||||
|
|
||||||
const MINUTE = 60*1000;
|
const MINUTE = 60*1000;
|
||||||
const DAY = 24*60*MINUTE;
|
const DAY = 24*60*MINUTE;
|
||||||
@ -23,6 +22,28 @@ function getShow(program) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shuffle(array, lo, hi ) {
|
||||||
|
if (typeof(lo) === 'undefined') {
|
||||||
|
lo = 0;
|
||||||
|
hi = array.length;
|
||||||
|
}
|
||||||
|
let currentIndex = hi, temporaryValue, randomIndex
|
||||||
|
while (lo !== currentIndex) {
|
||||||
|
randomIndex = random.integer(lo, currentIndex-1);
|
||||||
|
currentIndex -= 1
|
||||||
|
temporaryValue = array[currentIndex]
|
||||||
|
array[currentIndex] = array[randomIndex]
|
||||||
|
array[randomIndex] = temporaryValue
|
||||||
|
}
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
function _wait(t) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getProgramId(program) {
|
function getProgramId(program) {
|
||||||
let s = program.serverKey;
|
let s = program.serverKey;
|
||||||
if (typeof(s) === 'undefined') {
|
if (typeof(s) === 'undefined') {
|
||||||
@ -47,6 +68,78 @@ function addProgramToShow(show, program) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 randomPrograms = JSON.parse( JSON.stringify(show.programs) );
|
||||||
|
let n = randomPrograms.length;
|
||||||
|
shuffle( randomPrograms, 0, n);
|
||||||
|
let position = 0;
|
||||||
|
|
||||||
|
show.shuffler = {
|
||||||
|
|
||||||
|
current : () => {
|
||||||
|
return randomPrograms[position];
|
||||||
|
},
|
||||||
|
|
||||||
|
next: () => {
|
||||||
|
position++;
|
||||||
|
if (position == n) {
|
||||||
|
let a = Math.floor(n / 2);
|
||||||
|
shuffle(randomPrograms, 0, a );
|
||||||
|
shuffle(randomPrograms, a, n );
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return show.shuffler;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = async( programs, schedule ) => {
|
module.exports = async( programs, schedule ) => {
|
||||||
if (! Array.isArray(programs) ) {
|
if (! Array.isArray(programs) ) {
|
||||||
return { userError: 'Expected a programs array' };
|
return { userError: 'Expected a programs array' };
|
||||||
@ -131,9 +224,9 @@ module.exports = async( programs, schedule ) => {
|
|||||||
channel: show.channel,
|
channel: show.channel,
|
||||||
}
|
}
|
||||||
} else if (slot.order === 'shuffle') {
|
} else if (slot.order === 'shuffle') {
|
||||||
return orderers.getShowShuffler(show).current();
|
return getShowShuffler(show).current();
|
||||||
} else if (slot.order === 'next') {
|
} else if (slot.order === 'next') {
|
||||||
return orderers.getShowOrderer(show).current();
|
return getShowOrderer(show).current();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,9 +236,9 @@ module.exports = async( programs, schedule ) => {
|
|||||||
}
|
}
|
||||||
let show = shows[ showsById[slot.showId] ];
|
let show = shows[ showsById[slot.showId] ];
|
||||||
if (slot.order === 'shuffle') {
|
if (slot.order === 'shuffle') {
|
||||||
return orderers.getShowShuffler(show).next();
|
return getShowShuffler(show).next();
|
||||||
} else if (slot.order === 'next') {
|
} else if (slot.order === 'next') {
|
||||||
return orderers.getShowOrderer(show).next();
|
return getShowOrderer(show).next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
const events = require('events')
|
|
||||||
const constants = require("../constants");
|
const constants = require("../constants");
|
||||||
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png";
|
const FALLBACK_ICON = "https://raw.githubusercontent.com/vexorain/dizquetv/main/resources/dizquetv.png";
|
||||||
const throttle = require('./throttle');
|
const throttle = require('./throttle');
|
||||||
|
|
||||||
class TVGuideService extends events.EventEmitter
|
class TVGuideService
|
||||||
{
|
{
|
||||||
/****
|
/****
|
||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
constructor(xmltv, db, cacheImageService, eventService, i18next) {
|
constructor(xmltv, db, cacheImageService, eventService) {
|
||||||
super();
|
|
||||||
this.cached = null;
|
this.cached = null;
|
||||||
this.lastUpdate = 0;
|
this.lastUpdate = 0;
|
||||||
|
this.lastBackoff = 100;
|
||||||
this.updateTime = 0;
|
this.updateTime = 0;
|
||||||
this.currentUpdate = -1;
|
this.currentUpdate = -1;
|
||||||
this.currentLimit = -1;
|
this.currentLimit = -1;
|
||||||
@ -21,7 +21,6 @@ class TVGuideService extends events.EventEmitter
|
|||||||
this.cacheImageService = cacheImageService;
|
this.cacheImageService = cacheImageService;
|
||||||
this.eventService = eventService;
|
this.eventService = eventService;
|
||||||
this._throttle = throttle;
|
this._throttle = throttle;
|
||||||
this.i18next = i18next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async get() {
|
async get() {
|
||||||
@ -51,8 +50,7 @@ class TVGuideService extends events.EventEmitter
|
|||||||
|
|
||||||
async refresh(t) {
|
async refresh(t) {
|
||||||
while( this.lastUpdate < t) {
|
while( this.lastUpdate < t) {
|
||||||
await _wait(5000);
|
if (this.currentUpdate == -1) {
|
||||||
if ( ( this.lastUpdate < t) && (this.currentUpdate == -1) ) {
|
|
||||||
this.currentUpdate = this.updateTime;
|
this.currentUpdate = this.updateTime;
|
||||||
this.currentLimit = this.updateLimit;
|
this.currentLimit = this.updateLimit;
|
||||||
this.currentChannels = this.updateChannels;
|
this.currentChannels = this.updateChannels;
|
||||||
@ -71,6 +69,7 @@ class TVGuideService extends events.EventEmitter
|
|||||||
|
|
||||||
await this.buildIt();
|
await this.buildIt();
|
||||||
}
|
}
|
||||||
|
await _wait(100);
|
||||||
}
|
}
|
||||||
return await this.get();
|
return await this.get();
|
||||||
}
|
}
|
||||||
@ -83,17 +82,7 @@ class TVGuideService extends events.EventEmitter
|
|||||||
let arr = new Array( channel.programs.length + 1);
|
let arr = new Array( channel.programs.length + 1);
|
||||||
arr[0] = 0;
|
arr[0] = 0;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
let d = channel.programs[i].duration;
|
arr[i+1] = arr[i] + channel.programs[i].duration;
|
||||||
if (d == 0) {
|
|
||||||
console.log("Found program with duration 0, correcting it");
|
|
||||||
d = 1;
|
|
||||||
}
|
|
||||||
if (! Number.isInteger(d) ) {
|
|
||||||
console.log( `Found program in channel ${channel.number} with non-integer duration ${d}, correcting it`);
|
|
||||||
d = Math.ceil(d);
|
|
||||||
}
|
|
||||||
channel.programs[i].duration = d;
|
|
||||||
arr[i+1] = arr[i] + d;
|
|
||||||
await this._throttle();
|
await this._throttle();
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
@ -101,17 +90,6 @@ class TVGuideService extends events.EventEmitter
|
|||||||
|
|
||||||
async getCurrentPlayingIndex(channel, t) {
|
async getCurrentPlayingIndex(channel, t) {
|
||||||
let s = (new Date(channel.startTime)).getTime();
|
let s = (new Date(channel.startTime)).getTime();
|
||||||
if ( (typeof(channel.onDemand) !== 'undefined') && channel.onDemand.isOnDemand && channel.onDemand.paused ) {
|
|
||||||
// it's as flex
|
|
||||||
return {
|
|
||||||
index : -1,
|
|
||||||
start : t,
|
|
||||||
program : {
|
|
||||||
isOffline : true,
|
|
||||||
duration : 12*60*1000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (t < s) {
|
if (t < s) {
|
||||||
//it's flex time
|
//it's flex time
|
||||||
return {
|
return {
|
||||||
@ -127,17 +105,6 @@ class TVGuideService extends events.EventEmitter
|
|||||||
if (typeof(accumulate) === 'undefined') {
|
if (typeof(accumulate) === 'undefined') {
|
||||||
throw Error(channel.number + " wasn't preprocesed correctly???!?");
|
throw Error(channel.number + " wasn't preprocesed correctly???!?");
|
||||||
}
|
}
|
||||||
if (accumulate[channel.programs.length] === 0) {
|
|
||||||
console.log("[tv-guide] for some reason the total channel length is 0");
|
|
||||||
return {
|
|
||||||
index : -1,
|
|
||||||
start: t,
|
|
||||||
program: {
|
|
||||||
isOffline: true,
|
|
||||||
duration: 15*60*1000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let hi = channel.programs.length;
|
let hi = channel.programs.length;
|
||||||
let lo = 0;
|
let lo = 0;
|
||||||
let d = (t - s) % (accumulate[channel.programs.length]);
|
let d = (t - s) % (accumulate[channel.programs.length]);
|
||||||
@ -151,18 +118,9 @@ class TVGuideService extends events.EventEmitter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( (lo < 0) || (lo >= channel.programs.length) || (accumulate[lo+1] <= d) ) {
|
if (epoch + accumulate[lo+1] <= t) {
|
||||||
console.log("[tv-guide] The binary search algorithm is messed up. Replacing with flex...");
|
throw Error("General algorithm error, completely unexpected");
|
||||||
return {
|
|
||||||
index : -1,
|
|
||||||
start: t,
|
|
||||||
program: {
|
|
||||||
isOffline: true,
|
|
||||||
duration: 15*60*1000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._throttle();
|
await this._throttle();
|
||||||
return {
|
return {
|
||||||
index: lo,
|
index: lo,
|
||||||
@ -216,24 +174,11 @@ class TVGuideService extends events.EventEmitter
|
|||||||
console.error("Redirrect to an unknown channel found! Involved channels = " + JSON.stringify(depth) );
|
console.error("Redirrect to an unknown channel found! Involved channels = " + JSON.stringify(depth) );
|
||||||
} else {
|
} else {
|
||||||
let otherPlaying = await this.getChannelPlaying( channel2, undefined, t, depth );
|
let otherPlaying = await this.getChannelPlaying( channel2, undefined, t, depth );
|
||||||
let a1 = playing.start;
|
let start = Math.max(playing.start, otherPlaying.start);
|
||||||
let b1 = a1 + playing.program.duration;
|
let duration = Math.min(
|
||||||
|
(playing.start + playing.program.duration) - start,
|
||||||
let a2 = otherPlaying.start;
|
(otherPlaying.start + otherPlaying.program.duration) - start
|
||||||
let b2 = a2 + otherPlaying.program.duration;
|
);
|
||||||
|
|
||||||
if ( !(a1 <= t && t < b1) ) {
|
|
||||||
console.error("[tv-guide] algorithm error1 : " + a1 + ", " + t + ", " + b1 );
|
|
||||||
}
|
|
||||||
if ( !(a2 <= t && t < b2) ) {
|
|
||||||
console.error("[tv-guide] algorithm error2 : " + a2 + ", " + t + ", " + b2 );
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = Math.max( a1, a2 );
|
|
||||||
let b = Math.min( b1, b2 );
|
|
||||||
|
|
||||||
let start = a;
|
|
||||||
let duration = b - a;
|
|
||||||
let program2 = clone( otherPlaying.program );
|
let program2 = clone( otherPlaying.program );
|
||||||
program2.duration = duration;
|
program2.duration = duration;
|
||||||
playing = {
|
playing = {
|
||||||
@ -318,12 +263,7 @@ class TVGuideService extends events.EventEmitter
|
|||||||
x.program.duration -= d;
|
x.program.duration -= d;
|
||||||
}
|
}
|
||||||
if (x.program.duration == 0) {
|
if (x.program.duration == 0) {
|
||||||
console.error(channel.number + " There's a program with duration 0? " + JSON.stringify(x.program) + " ; " + t1 );
|
console.error("There's a program with duration 0?");
|
||||||
x.program.duration = 5 * 60 * 1000;
|
|
||||||
} else if ( ! Number.isInteger( x.program.duration ) ) {
|
|
||||||
console.error(channel.number + " There's a program with non-integer duration?? " + JSON.stringify(x.program) + " ; " + t1 );
|
|
||||||
x.program = JSON.parse( JSON.stringify(x.program) );
|
|
||||||
x.program.duration = Math.ceil(x.program.duration );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.programs = [];
|
result.programs = [];
|
||||||
@ -391,9 +331,9 @@ class TVGuideService extends events.EventEmitter
|
|||||||
program: {
|
program: {
|
||||||
duration: 24*60*60*1000,
|
duration: 24*60*60*1000,
|
||||||
icon: FALLBACK_ICON,
|
icon: FALLBACK_ICON,
|
||||||
showTitle: this.i18next.t("tvGuide.no_channels"),
|
showTitle: "No channels configured",
|
||||||
date: formatDateYYYYMMDD(new Date()),
|
date: formatDateYYYYMMDD(new Date()),
|
||||||
summary : this.i18next.t("tvGuide.no_channels_summary")
|
summary : "Use the dizqueTV web UI to configure channels."
|
||||||
}
|
}
|
||||||
} )
|
} )
|
||||||
]
|
]
|
||||||
@ -409,19 +349,18 @@ class TVGuideService extends events.EventEmitter
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildIt(lastRetry) {
|
async buildIt() {
|
||||||
try {
|
try {
|
||||||
this.cached = await this.buildItManaged();
|
this.cached = await this.buildItManaged();
|
||||||
console.log("Internal TV Guide data refreshed at " + (new Date()).toLocaleString() );
|
console.log("Internal TV Guide data refreshed at " + (new Date()).toLocaleString() );
|
||||||
await this.refreshXML();
|
await this.refreshXML();
|
||||||
|
this.lastBackoff = 100;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error("Unable to update internal guide data", err);
|
console.error("Unable to update internal guide data", err);
|
||||||
let w = 100;
|
let w = Math.min(this.lastBackoff * 2, 300000);
|
||||||
if (typeof(lastRetry) !== 'undefined') {
|
|
||||||
w = Math.min(w*2, 5 * 60 * 1000);
|
|
||||||
}
|
|
||||||
await _wait(w);
|
await _wait(w);
|
||||||
console.error("Retrying TV guide...");
|
this.lastBackoff = w;
|
||||||
|
console.error(`Retrying TV guide after ${w} milliseconds wait...`);
|
||||||
await this.buildIt();
|
await this.buildIt();
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
@ -435,11 +374,10 @@ class TVGuideService extends events.EventEmitter
|
|||||||
let xmltvSettings = this.db['xmltv-settings'].find()[0];
|
let xmltvSettings = this.db['xmltv-settings'].find()[0];
|
||||||
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService);
|
await this.xmltv.WriteXMLTV(this.cached, xmltvSettings, async() => await this._throttle(), this.cacheImageService);
|
||||||
let t = "" + ( (new Date()) );
|
let t = "" + ( (new Date()) );
|
||||||
this.emit("xmltv-updated", { time: t } );
|
|
||||||
eventService.push(
|
eventService.push(
|
||||||
"xmltv",
|
"xmltv",
|
||||||
{
|
{
|
||||||
"message": this.i18next.t("tvGuide.xmltv_updated", {t}),
|
"message": `XMLTV updated at server time = ${t}`,
|
||||||
"module" : "xmltv",
|
"module" : "xmltv",
|
||||||
"detail" : {
|
"detail" : {
|
||||||
"time": new Date(),
|
"time": new Date(),
|
||||||
@ -562,9 +500,6 @@ function makeEntry(channel, x) {
|
|||||||
episode: x.program.episode,
|
episode: x.program.episode,
|
||||||
title: x.program.title,
|
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') {
|
if (typeof(title)==='undefined') {
|
||||||
|
|||||||
@ -7,20 +7,9 @@ function equalItems(a, b) {
|
|||||||
if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) {
|
if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
console.log("no idea how to compare this: " + JSON.stringify(a) );
|
||||||
(a.type === "loading") || (a.type === "interlude")
|
console.log(" with this: " + JSON.stringify(b) );
|
||||||
|| (b.type === "loading") || (b.type === "interlude")
|
return true;
|
||||||
) {
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,14 +19,15 @@ function wereThereTooManyAttempts(sessionId, lineupItem) {
|
|||||||
let t1 = (new Date()).getTime();
|
let t1 = (new Date()).getTime();
|
||||||
|
|
||||||
let previous = cache[sessionId];
|
let previous = cache[sessionId];
|
||||||
let result = false;
|
|
||||||
|
|
||||||
if (typeof(previous) === 'undefined') {
|
if (typeof(previous) === 'undefined') {
|
||||||
previous = cache[sessionId] = {
|
previous = cache[sessionId] = {
|
||||||
t0: t1 - constants.TOO_FREQUENT * 5,
|
t0: t1 - constants.TOO_FREQUENT * 5,
|
||||||
lineupItem: null,
|
lineupItem: null,
|
||||||
};
|
};
|
||||||
} else if (t1 - previous.t0 < constants.TOO_FREQUENT) {
|
}
|
||||||
|
|
||||||
|
let result = false;
|
||||||
|
if (t1 - previous.t0 < constants.TOO_FREQUENT) {
|
||||||
//certainly too frequent
|
//certainly too frequent
|
||||||
result = equalItems( previous.lineupItem, lineupItem );
|
result = equalItems( previous.lineupItem, lineupItem );
|
||||||
}
|
}
|
||||||
@ -61,4 +51,4 @@ function wereThereTooManyAttempts(sessionId, lineupItem) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = wereThereTooManyAttempts;
|
module.exports = wereThereTooManyAttempts;
|
||||||
263
src/video.js
263
src/video.js
@ -8,17 +8,11 @@ const ProgramPlayer = require('./program-player');
|
|||||||
const channelCache = require('./channel-cache')
|
const channelCache = require('./channel-cache')
|
||||||
const wereThereTooManyAttempts = require('./throttler');
|
const wereThereTooManyAttempts = require('./throttler');
|
||||||
|
|
||||||
module.exports = { router: video, shutdown: shutdown }
|
module.exports = { router: video }
|
||||||
|
|
||||||
let StreamCount = 0;
|
let StreamCount = 0;
|
||||||
|
|
||||||
let stopPlayback = false;
|
function video( channelDB , fillerDB, db) {
|
||||||
|
|
||||||
async function shutdown() {
|
|
||||||
stopPlayback = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function video( channelService, fillerDB, db, programmingService, activeChannelService, programPlayTimeDB ) {
|
|
||||||
var router = express.Router()
|
var router = express.Router()
|
||||||
|
|
||||||
router.get('/setup', (req, res) => {
|
router.get('/setup', (req, res) => {
|
||||||
@ -51,26 +45,19 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
// Continuously stream video to client. Leverage ffmpeg concat for piecing together videos
|
// Continuously stream video to client. Leverage ffmpeg concat for piecing together videos
|
||||||
let concat = async (req, res, audioOnly, step) => {
|
let concat = async (req, res, audioOnly) => {
|
||||||
if ( typeof(step) === 'undefined') {
|
|
||||||
step = 0;
|
|
||||||
}
|
|
||||||
if (stopPlayback) {
|
|
||||||
res.status(503).send("Server is shutting down.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if channel queried is valid
|
// Check if channel queried is valid
|
||||||
if (typeof req.query.channel === 'undefined') {
|
if (typeof req.query.channel === 'undefined') {
|
||||||
res.status(500).send("No Channel Specified")
|
res.status(500).send("No Channel Specified")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let number = parseInt(req.query.channel, 10);
|
let number = parseInt(req.query.channel, 10);
|
||||||
let channel = await channelService.getChannel(number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
if (channel == null) {
|
if (channel.length === 0) {
|
||||||
res.status(500).send("Channel doesn't exist")
|
res.status(500).send("Channel doesn't exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
channel = channel[0]
|
||||||
|
|
||||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||||
|
|
||||||
@ -81,11 +68,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step == 0) {
|
res.writeHead(200, {
|
||||||
res.writeHead(200, {
|
'Content-Type': 'video/mp2t'
|
||||||
'Content-Type': 'video/mp2t'
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`)
|
console.log(`\r\nStream starting. Channel: ${channel.number} (${channel.name})`)
|
||||||
|
|
||||||
@ -112,7 +97,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
return;
|
return;
|
||||||
})
|
})
|
||||||
|
|
||||||
//ffmpeg.on('close', stop)
|
ffmpeg.on('close', stop)
|
||||||
|
|
||||||
res.on('close', () => { // on HTTP close, kill ffmpeg
|
res.on('close', () => { // on HTTP close, kill ffmpeg
|
||||||
console.log(`\r\nStream ended. Channel: ${channel.number} (${channel.name})`);
|
console.log(`\r\nStream ended. Channel: ${channel.number} (${channel.name})`);
|
||||||
@ -120,13 +105,13 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
})
|
})
|
||||||
|
|
||||||
ffmpeg.on('end', () => {
|
ffmpeg.on('end', () => {
|
||||||
console.log("Queue exhausted so we are appending the channel stream again to the http output.")
|
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.")
|
||||||
concat(req, res, audioOnly, step+1);
|
stop();
|
||||||
})
|
})
|
||||||
|
|
||||||
let channelNum = parseInt(req.query.channel, 10)
|
let channelNum = parseInt(req.query.channel, 10)
|
||||||
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}&stepNumber={step}`);
|
let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}&audioOnly=${audioOnly}`);
|
||||||
ff.pipe(res, { end: false} );
|
ff.pipe(res );
|
||||||
};
|
};
|
||||||
router.get('/video', async(req, res) => {
|
router.get('/video', async(req, res) => {
|
||||||
return await concat(req, res, false);
|
return await concat(req, res, false);
|
||||||
@ -137,11 +122,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
|
|
||||||
// Stream individual video to ffmpeg concat above. This is used by the server, NOT the client
|
// Stream individual video to ffmpeg concat above. This is used by the server, NOT the client
|
||||||
let streamFunction = async (req, res, t0, allowSkip) => {
|
let streamFunction = async (req, res, t0, allowSkip) => {
|
||||||
if (stopPlayback) {
|
|
||||||
res.status(503).send("Server is shutting down.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if channel queried is valid
|
// Check if channel queried is valid
|
||||||
res.on("error", (e) => {
|
res.on("error", (e) => {
|
||||||
console.error("There was an unexpected error in stream.", e);
|
console.error("There was an unexpected error in stream.", e);
|
||||||
@ -156,9 +136,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
let session = parseInt(req.query.session);
|
let session = parseInt(req.query.session);
|
||||||
let m3u8 = (req.query.m3u8 === '1');
|
let m3u8 = (req.query.m3u8 === '1');
|
||||||
let number = parseInt(req.query.channel);
|
let number = parseInt(req.query.channel);
|
||||||
let channel = await channelService.getChannel( number);
|
let channel = await channelCache.getChannelConfig(channelDB, number);
|
||||||
|
|
||||||
if (channel == null) {
|
if (channel.length === 0) {
|
||||||
res.status(404).send("Channel doesn't exist")
|
res.status(404).send("Channel doesn't exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -171,8 +151,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
if ( (typeof req.query.first !== 'undefined') && (req.query.first=='1') ) {
|
if ( (typeof req.query.first !== 'undefined') && (req.query.first=='1') ) {
|
||||||
isFirst = true;
|
isFirst = true;
|
||||||
}
|
}
|
||||||
|
channel = channel[0]
|
||||||
let isBetween = ( (typeof req.query.between !== 'undefined') && (req.query.between=='1') );
|
|
||||||
|
|
||||||
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
|
||||||
|
|
||||||
@ -183,62 +162,34 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ffmpegSettings.disablePreludes === true) {
|
|
||||||
//disable the preludes
|
|
||||||
isBetween = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get video lineup (array of video urls with calculated start times and durations.)
|
// Get video lineup (array of video urls with calculated start times and durations.)
|
||||||
|
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
|
||||||
let prog = null;
|
let prog = null;
|
||||||
let brandChannel = channel;
|
let brandChannel = channel;
|
||||||
let redirectChannels = [];
|
let redirectChannels = [];
|
||||||
let upperBounds = [];
|
let upperBounds = [];
|
||||||
|
|
||||||
const GAP_DURATION = constants.GAP_DURATION;
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
lineupItem = {
|
lineupItem = {
|
||||||
type: 'loading',
|
type: 'loading',
|
||||||
title: "Loading Screen",
|
streamDuration: 40,
|
||||||
noRealTime: true,
|
duration: 40,
|
||||||
streamDuration: GAP_DURATION,
|
|
||||||
duration: GAP_DURATION,
|
|
||||||
redirectChannels: [channel],
|
|
||||||
start: 0,
|
start: 0,
|
||||||
};
|
};
|
||||||
} else if (isBetween) {
|
} else if (lineupItem == null) {
|
||||||
lineupItem = {
|
prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, channel);
|
||||||
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];
|
|
||||||
} else {
|
|
||||||
prog = programmingService.getCurrentProgramAndTimeElapsed(t0, channel);
|
|
||||||
activeChannelService.peekChannel(t0, channel.number);
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
redirectChannels.push( helperFuncs.generateChannelContext(brandChannel) );
|
redirectChannels.push( brandChannel );
|
||||||
upperBounds.push( prog.program.duration - prog.timeElapsed );
|
upperBounds.push( prog.program.duration - prog.timeElapsed );
|
||||||
|
|
||||||
if ( !(prog.program.isOffline) || (prog.program.type != 'redirect') ) {
|
if ( !(prog.program.isOffline) || (prog.program.type != 'redirect') ) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
channelCache.recordPlayback(programPlayTimeDB,
|
channelCache.recordPlayback( brandChannel.number, t0, {
|
||||||
brandChannel.number, t0, {
|
|
||||||
/*type: 'offline',*/
|
/*type: 'offline',*/
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
err: Error("Recursive channel redirect found"),
|
err: Error("Recursive channel redirect found"),
|
||||||
@ -249,9 +200,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
|
|
||||||
|
|
||||||
let newChannelNumber= prog.program.channel;
|
let newChannelNumber= prog.program.channel;
|
||||||
let newChannel = await channelService.getChannel(newChannelNumber);
|
let newChannel = await channelCache.getChannelConfig(channelDB, newChannelNumber);
|
||||||
|
|
||||||
if (newChannel == null) {
|
if (newChannel.length == 0) {
|
||||||
let err = Error("Invalid redirect to a channel that doesn't exist");
|
let err = Error("Invalid redirect to a channel that doesn't exist");
|
||||||
console.error("Invalid redirect to channel that doesn't exist.", err);
|
console.error("Invalid redirect to channel that doesn't exist.", err);
|
||||||
prog = {
|
prog = {
|
||||||
@ -264,14 +215,14 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
newChannel = newChannel[0];
|
||||||
brandChannel = newChannel;
|
brandChannel = newChannel;
|
||||||
lineupItem = channelCache.getCurrentLineupItem( newChannel.number, t0);
|
lineupItem = channelCache.getCurrentLineupItem( newChannel.number, t0);
|
||||||
if (lineupItem != null) {
|
if (lineupItem != null) {
|
||||||
lineupItem = JSON.parse( JSON.stringify(lineupItem)) ;
|
lineupItem = JSON.parse( JSON.stringify(lineupItem)) ;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
prog = programmingService.getCurrentProgramAndTimeElapsed(t0, newChannel);
|
prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, newChannel);
|
||||||
activeChannelService.peekChannel(t0, newChannel.number);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,20 +255,11 @@ 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."
|
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 fillers = await fillerDB.getFillersFromChannel(brandChannel);
|
||||||
try {
|
let lineup = helperFuncs.createLineup(prog, brandChannel, fillers, isFirst)
|
||||||
let lineup = helperFuncs.createLineup(programPlayTimeDB, prog, brandChannel, fillers, isFirst)
|
lineupItem = lineup.shift();
|
||||||
lineupItem = lineup.shift();
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Error when attempting to pick video: " +err.stack);
|
|
||||||
lineupItem = {
|
|
||||||
isOffline: true,
|
|
||||||
err: err,
|
|
||||||
duration : 60000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !isBetween && !isLoading && (lineupItem != null) ) {
|
if ( !isLoading && (lineupItem != null) ) {
|
||||||
let upperBound = 1000000000;
|
let upperBound = 1000000000;
|
||||||
let beginningOffset = 0;
|
let beginningOffset = 0;
|
||||||
if (typeof(lineupItem.beginningOffset) !== 'undefined') {
|
if (typeof(lineupItem.beginningOffset) !== 'undefined') {
|
||||||
@ -326,8 +268,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
//adjust upper bounds and record playbacks
|
//adjust upper bounds and record playbacks
|
||||||
for (let i = redirectChannels.length-1; i >= 0; i--) {
|
for (let i = redirectChannels.length-1; i >= 0; i--) {
|
||||||
lineupItem = JSON.parse( JSON.stringify(lineupItem ));
|
lineupItem = JSON.parse( JSON.stringify(lineupItem ));
|
||||||
lineupItem.redirectChannels = redirectChannels;
|
|
||||||
lineupItem.upperBounds = upperBounds;
|
|
||||||
let u = upperBounds[i] + beginningOffset;
|
let u = upperBounds[i] + beginningOffset;
|
||||||
if (typeof(u) !== 'undefined') {
|
if (typeof(u) !== 'undefined') {
|
||||||
let u2 = upperBound;
|
let u2 = upperBound;
|
||||||
@ -337,13 +277,10 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
lineupItem.streamDuration = Math.min(u2, u);
|
lineupItem.streamDuration = Math.min(u2, u);
|
||||||
upperBound = lineupItem.streamDuration;
|
upperBound = lineupItem.streamDuration;
|
||||||
}
|
}
|
||||||
channelCache.recordPlayback( programPlayTimeDB, redirectChannels[i].number, t0, lineupItem );
|
channelCache.recordPlayback( redirectChannels[i].number, t0, lineupItem );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let t2 = (new Date()).getTime();
|
|
||||||
console.log( `Decision Latency: (${t2-t0})ms` );
|
|
||||||
|
|
||||||
|
|
||||||
console.log("=========================================================");
|
console.log("=========================================================");
|
||||||
console.log("! Start playback");
|
console.log("! Start playback");
|
||||||
@ -359,8 +296,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
}
|
}
|
||||||
console.log("=========================================================");
|
console.log("=========================================================");
|
||||||
|
|
||||||
if (! isLoading && ! isBetween) {
|
if (! isLoading) {
|
||||||
channelCache.recordPlayback(programPlayTimeDB, channel.number, t0, lineupItem);
|
channelCache.recordPlayback(channel.number, t0, lineupItem);
|
||||||
}
|
}
|
||||||
if (wereThereTooManyAttempts(session, 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.");
|
console.error("There are too many attempts to play the same item in a short period of time, playing the error stream instead.");
|
||||||
@ -398,14 +335,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
'Content-Type': 'video/mp2t'
|
'Content-Type': 'video/mp2t'
|
||||||
});
|
});
|
||||||
|
|
||||||
shieldActiveChannels(redirectChannels, t0, constants.START_CHANNEL_GRACE_PERIOD);
|
|
||||||
|
|
||||||
let t1;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
playerObj = await player.play(res);
|
playerObj = await player.play(res);
|
||||||
t1 = (new Date()).getTime();
|
|
||||||
console.log( `Player Latency: (${t1-t0})ms` );
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Error when attempting to play video: " +err.stack);
|
console.log("Error when attempting to play video: " +err.stack);
|
||||||
try {
|
try {
|
||||||
@ -417,59 +348,7 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! isLoading) {
|
|
||||||
//setup end event to mark the channel as not playing anymore
|
|
||||||
let t0 = new Date().getTime();
|
|
||||||
let b = 0;
|
|
||||||
let stopDetected = false;
|
|
||||||
if (typeof(lineupItem.beginningOffset) !== 'undefined') {
|
|
||||||
b = lineupItem.beginningOffset;
|
|
||||||
t0 -= b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to do it for every single redirected channel...
|
|
||||||
|
|
||||||
for (let i = redirectChannels.length-1; i >= 0; i--) {
|
|
||||||
activeChannelService.registerChannelActive(t0, redirectChannels[i].number);
|
|
||||||
}
|
|
||||||
let listener = (data) => {
|
|
||||||
if (data.ignoreOnDemand) {
|
|
||||||
console.log("Ignore channel update because it is from on-demand service");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let shouldStop = false;
|
|
||||||
try {
|
|
||||||
for (let i = 0; i < redirectChannels.length; i++) {
|
|
||||||
if (redirectChannels[i].number == data.channelNumber) {
|
|
||||||
shouldStop = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shouldStop) {
|
|
||||||
console.log("Playing channel has received an update.");
|
|
||||||
shieldActiveChannels( redirectChannels, t0, constants.CHANNEL_STOP_SHIELD )
|
|
||||||
setTimeout(stop, 100);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.err("Unexpected error when processing channel change during playback", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
channelService.on("channel-update", listener);
|
|
||||||
|
|
||||||
let oldStop = stop;
|
|
||||||
stop = () => {
|
|
||||||
channelService.removeListener("channel-update", listener);
|
|
||||||
if (!stopDetected) {
|
|
||||||
stopDetected = true;
|
|
||||||
let t1 = new Date().getTime();
|
|
||||||
t1 = Math.max( t0 + 1, t1 - constants.FORGETFULNESS_BUFFER - b );
|
|
||||||
for (let i = redirectChannels.length-1; i >= 0; i--) {
|
|
||||||
activeChannelService.registerChannelStopped(t1, redirectChannels[i].number);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oldStop();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let stream = playerObj;
|
let stream = playerObj;
|
||||||
|
|
||||||
|
|
||||||
@ -478,13 +357,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
|
|
||||||
|
|
||||||
stream.on("end", () => {
|
stream.on("end", () => {
|
||||||
let t2 = (new Date()).getTime();
|
|
||||||
console.log("Played video for: " + (t2 - t1) + " ms");
|
|
||||||
stop();
|
stop();
|
||||||
});
|
});
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
let t2 = (new Date()).getTime();
|
|
||||||
console.log("Played video for: " + (t2 - t1) + " ms");
|
|
||||||
console.log("Client Closed");
|
console.log("Client Closed");
|
||||||
stop();
|
stop();
|
||||||
});
|
});
|
||||||
@ -497,12 +372,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
|
|
||||||
|
|
||||||
router.get('/m3u8', async (req, res) => {
|
router.get('/m3u8', async (req, res) => {
|
||||||
if (stopPlayback) {
|
|
||||||
res.status(503).send("Server is shutting down.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let sessionId = StreamCount++;
|
let sessionId = StreamCount++;
|
||||||
|
|
||||||
//res.type('application/vnd.apple.mpegurl')
|
//res.type('application/vnd.apple.mpegurl')
|
||||||
@ -515,8 +384,8 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
}
|
}
|
||||||
|
|
||||||
let channelNum = parseInt(req.query.channel, 10)
|
let channelNum = parseInt(req.query.channel, 10)
|
||||||
let channel = await channelService.getChannel(channelNum );
|
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
|
||||||
if (channel == null) {
|
if (channel.length === 0) {
|
||||||
res.status(500).send("Channel doesn't exist")
|
res.status(500).send("Channel doesn't exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -551,12 +420,6 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
res.send(data)
|
res.send(data)
|
||||||
})
|
})
|
||||||
router.get('/playlist', async (req, res) => {
|
router.get('/playlist', async (req, res) => {
|
||||||
if (stopPlayback) {
|
|
||||||
res.status(503).send("Server is shutting down.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
res.type('text')
|
res.type('text')
|
||||||
|
|
||||||
// Check if channel queried is valid
|
// Check if channel queried is valid
|
||||||
@ -565,13 +428,9 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let stepNumber = parseInt(req.query.stepNumber, 10)
|
|
||||||
if (isNaN(stepNumber)) {
|
|
||||||
stepNumber = 0;
|
|
||||||
}
|
|
||||||
let channelNum = parseInt(req.query.channel, 10)
|
let channelNum = parseInt(req.query.channel, 10)
|
||||||
let channel = await channelService.getChannel(channelNum );
|
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
|
||||||
if (channel == null) {
|
if (channel.length === 0) {
|
||||||
res.status(500).send("Channel doesn't exist")
|
res.status(500).send("Channel doesn't exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -587,59 +446,29 @@ function video( channelService, fillerDB, db, programmingService, activeChannelS
|
|||||||
let sessionId = StreamCount++;
|
let sessionId = StreamCount++;
|
||||||
let audioOnly = ("true" == req.query.audioOnly);
|
let audioOnly = ("true" == req.query.audioOnly);
|
||||||
|
|
||||||
let transcodingEnabled = (ffmpegSettings.enableFFMPEGTranscoding === true)
|
if (
|
||||||
|
(ffmpegSettings.enableFFMPEGTranscoding === true)
|
||||||
&& (ffmpegSettings.normalizeVideoCodec === true)
|
&& (ffmpegSettings.normalizeVideoCodec === true)
|
||||||
&& (ffmpegSettings.normalizeAudioCodec === true)
|
&& (ffmpegSettings.normalizeAudioCodec === true)
|
||||||
&& (ffmpegSettings.normalizeResolution === 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) */
|
&& (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
|
//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=0&session=${sessionId}&audioOnly=${audioOnly}'\n`;
|
||||||
}
|
}
|
||||||
let remaining = maxStreamsToPlayInARow;
|
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}&audioOnly=${audioOnly}'\n`
|
||||||
if (stepNumber == 0) {
|
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
|
||||||
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`
|
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)
|
res.send(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
let shieldActiveChannels = (channelList, t0, timeout) => {
|
|
||||||
// because of channel redirects, it's possible that multiple channels
|
|
||||||
// are being played at once. Mark all of them as being played
|
|
||||||
// this is a grave period of 30
|
|
||||||
//mark all channels being played as active:
|
|
||||||
for (let i = channelList.length-1; i >= 0; i--) {
|
|
||||||
activeChannelService.registerChannelActive(t0, channelList[i].number);
|
|
||||||
}
|
|
||||||
setTimeout( () => {
|
|
||||||
for (let i = channelList.length-1; i >= 0; i--) {
|
|
||||||
activeChannelService.registerChannelStopped(t0, channelList[i].number);
|
|
||||||
}
|
|
||||||
}, timeout );
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let mediaPlayer = async(channelNum, path, req, res) => {
|
let mediaPlayer = async(channelNum, path, req, res) => {
|
||||||
let channel = await channelService.getChannel(channelNum );
|
let channel = await channelCache.getChannelConfig(channelDB, channelNum );
|
||||||
if (channel === null) {
|
if (channel.length === 0) {
|
||||||
res.status(404).send("Channel not found.");
|
res.status(404).send("Channel not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,6 @@ async function _writeProgramme(channel, program, xw, xmlSettings, cacheImageServ
|
|||||||
xw.writeRaw('\n <previously-shown/>')
|
xw.writeRaw('\n <previously-shown/>')
|
||||||
|
|
||||||
//sub-title
|
//sub-title
|
||||||
// TODO: Add support for track data (artist, album) here
|
|
||||||
if ( typeof(program.sub) !== 'undefined') {
|
if ( typeof(program.sub) !== 'undefined') {
|
||||||
xw.startElement('sub-title')
|
xw.startElement('sub-title')
|
||||||
xw.writeAttribute('lang', 'en')
|
xw.writeAttribute('lang', 'en')
|
||||||
|
|||||||
27
web/app.js
27
web/app.js
@ -4,33 +4,8 @@ require('./ext/lazyload')(angular)
|
|||||||
require('./ext/dragdrop')
|
require('./ext/dragdrop')
|
||||||
require('./ext/angularjs-scroll-glue')
|
require('./ext/angularjs-scroll-glue')
|
||||||
require('angular-vs-repeat');
|
require('angular-vs-repeat');
|
||||||
require('angular-sanitize');
|
|
||||||
const i18next = require('i18next');
|
|
||||||
const i18nextHttpBackend = require('i18next-http-backend');
|
|
||||||
window.i18next = i18next;
|
|
||||||
|
|
||||||
window.i18next.use(i18nextHttpBackend);
|
var app = angular.module('myApp', ['ngRoute', 'vs-repeat', 'angularLazyImg', 'dndLists', 'luegg.directives'])
|
||||||
|
|
||||||
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('plex', require('./services/plex'))
|
||||||
app.service('dizquetv', require('./services/dizquetv'))
|
app.service('dizquetv', require('./services/dizquetv'))
|
||||||
|
|||||||
@ -8,10 +8,6 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
|||||||
$scope.shows = [ { id: '?', pending: true} ]
|
$scope.shows = [ { id: '?', pending: true} ]
|
||||||
$timeout();
|
$timeout();
|
||||||
let shows = await dizquetv.getAllShowsInfo();
|
let shows = await dizquetv.getAllShowsInfo();
|
||||||
shows.sort( (a,b) => {
|
|
||||||
return a.name > b.name;
|
|
||||||
} );
|
|
||||||
|
|
||||||
$scope.shows = shows;
|
$scope.shows = shows;
|
||||||
$timeout();
|
$timeout();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,13 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
|||||||
$scope.fillers = [ { id: '?', pending: true} ]
|
$scope.fillers = [ { id: '?', pending: true} ]
|
||||||
$timeout();
|
$timeout();
|
||||||
let fillers = await dizquetv.getAllFillersInfo();
|
let fillers = await dizquetv.getAllFillersInfo();
|
||||||
fillers.sort( (a,b) => {
|
|
||||||
return a.name > b.name;
|
|
||||||
} );
|
|
||||||
$scope.fillers = fillers;
|
$scope.fillers = fillers;
|
||||||
$timeout();
|
$timeout();
|
||||||
}
|
}
|
||||||
$scope.refreshFiller();
|
$scope.refreshFiller();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let feedToFillerConfig = () => {};
|
let feedToFillerConfig = () => {};
|
||||||
let feedToDeleteFiller = feedToFillerConfig;
|
let feedToDeleteFiller = feedToFillerConfig;
|
||||||
|
|
||||||
|
|||||||
@ -83,6 +83,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
|||||||
$scope.t1 = (new Date()).getTime();
|
$scope.t1 = (new Date()).getTime();
|
||||||
$scope.t1 = ($scope.t1 - $scope.t1 % MINUTE );
|
$scope.t1 = ($scope.t1 - $scope.t1 % MINUTE );
|
||||||
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
|
$scope.t0 = $scope.t1 - $scope.before + $scope.offset;
|
||||||
|
$scope.title = "TV Guide";
|
||||||
$scope.times = [];
|
$scope.times = [];
|
||||||
|
|
||||||
$scope.updateJustNow();
|
$scope.updateJustNow();
|
||||||
@ -312,7 +313,7 @@ module.exports = function ($scope, $timeout, dizquetv) {
|
|||||||
ch.programs.push( {
|
ch.programs.push( {
|
||||||
duration: addDuration(b - a),
|
duration: addDuration(b - a),
|
||||||
altTitle: altTitle,
|
altTitle: altTitle,
|
||||||
showTitle: program.title, // movie title, episode title or track title
|
showTitle: program.title,
|
||||||
subTitle: subTitle,
|
subTitle: subTitle,
|
||||||
episodeTitle : episodeTitle,
|
episodeTitle : episodeTitle,
|
||||||
start: hasStart,
|
start: hasStart,
|
||||||
|
|||||||
@ -11,7 +11,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
|
|
||||||
post: function (scope, $element, attrs) {
|
post: function (scope, element, attrs) {
|
||||||
scope.screenW = 1920;
|
scope.screenW = 1920;
|
||||||
scope.screenh = 1080;
|
scope.screenh = 1080;
|
||||||
|
|
||||||
@ -49,7 +49,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.episodeMemory = {
|
scope.episodeMemory = {
|
||||||
saved : false,
|
saved : false,
|
||||||
};
|
};
|
||||||
scope.fixedOnDemand = false;
|
|
||||||
if (typeof scope.channel === 'undefined' || scope.channel == null) {
|
if (typeof scope.channel === 'undefined' || scope.channel == null) {
|
||||||
scope.channel = {}
|
scope.channel = {}
|
||||||
scope.channel.programs = []
|
scope.channel.programs = []
|
||||||
@ -87,10 +86,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.channel.transcoding = {
|
scope.channel.transcoding = {
|
||||||
targetResolution: "",
|
targetResolution: "",
|
||||||
}
|
}
|
||||||
scope.channel.onDemand = {
|
|
||||||
isOnDemand : false,
|
|
||||||
modulo: 1,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
scope.beforeEditChannelNumber = scope.channel.number
|
scope.beforeEditChannelNumber = scope.channel.number
|
||||||
|
|
||||||
@ -147,16 +142,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.channel.transcoding.targetResolution = "";
|
scope.channel.transcoding.targetResolution = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(scope.channel.onDemand) === 'undefined') {
|
|
||||||
scope.channel.onDemand = {};
|
|
||||||
}
|
|
||||||
if (typeof(scope.channel.onDemand.isOnDemand) !== 'boolean') {
|
|
||||||
scope.channel.onDemand.isOnDemand = false;
|
|
||||||
}
|
|
||||||
if (typeof(scope.channel.onDemand.modulo) !== 'number') {
|
|
||||||
scope.channel.onDemand.modulo = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
adjustStartTimeToCurrentProgram();
|
adjustStartTimeToCurrentProgram();
|
||||||
updateChannelDuration();
|
updateChannelDuration();
|
||||||
@ -178,26 +163,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
let t = Date.now();
|
let t = Date.now();
|
||||||
let originalStart = scope.channel.startTime.getTime();
|
let originalStart = scope.channel.startTime.getTime();
|
||||||
let n = scope.channel.programs.length;
|
let n = scope.channel.programs.length;
|
||||||
|
|
||||||
if (
|
|
||||||
(scope.channel.onDemand.isOnDemand === true)
|
|
||||||
&&
|
|
||||||
(scope.channel.onDemand.paused === true)
|
|
||||||
&&
|
|
||||||
! scope.fixedOnDemand
|
|
||||||
) {
|
|
||||||
//this should only happen once per channel
|
|
||||||
scope.fixedOnDemand = true;
|
|
||||||
originalStart = new Date().getTime();
|
|
||||||
originalStart -= scope.channel.onDemand.playedOffset;
|
|
||||||
let m = scope.channel.onDemand.firstProgramModulo;
|
|
||||||
let n = originalStart % scope.channel.onDemand.modulo;
|
|
||||||
if (n < m) {
|
|
||||||
originalStart += (m - n);
|
|
||||||
} else if (n > m) {
|
|
||||||
originalStart -= (n - m) - scope.channel.onDemand.modulo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//scope.channel.totalDuration might not have been initialized
|
//scope.channel.totalDuration might not have been initialized
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
@ -255,7 +220,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
{ name: "Flex", id: "flex" },
|
{ name: "Flex", id: "flex" },
|
||||||
{ name: "EPG", id: "epg" },
|
{ name: "EPG", id: "epg" },
|
||||||
{ name: "FFmpeg", id: "ffmpeg" },
|
{ name: "FFmpeg", id: "ffmpeg" },
|
||||||
{ name: "On-demand", id: "ondemand" },
|
|
||||||
];
|
];
|
||||||
scope.setTab = (tab) => {
|
scope.setTab = (tab) => {
|
||||||
scope.tab = tab;
|
scope.tab = tab;
|
||||||
@ -326,10 +290,9 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
isOffline: true
|
isOffline: true
|
||||||
}
|
}
|
||||||
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
|
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
|
||||||
scope._selectedOffline = null
|
scope._selectedOffline = null
|
||||||
scope._addingOffline = null;
|
scope._addingOffline = null;
|
||||||
scrollToLast();
|
|
||||||
updateChannelDuration()
|
updateChannelDuration()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1078,37 +1041,11 @@ 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++) {
|
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
|
||||||
delete selectedPrograms[i].commercials;
|
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()
|
updateChannelDuration()
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
@ -1120,9 +1057,7 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
}
|
}
|
||||||
scope.finishRedirect = (program) => {
|
scope.finishRedirect = (program) => {
|
||||||
if (scope.selectedProgram == -1) {
|
if (scope.selectedProgram == -1) {
|
||||||
scope.channel.programs.splice(scope.channel.programs.length, 0, program);
|
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
|
||||||
scrollToLast();
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
scope.channel.programs[ scope.selectedProgram ] = program;
|
scope.channel.programs[ scope.selectedProgram ] = program;
|
||||||
}
|
}
|
||||||
@ -1494,10 +1429,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.videoRateDefault = "(Use global setting)";
|
scope.videoRateDefault = "(Use global setting)";
|
||||||
scope.videoBufSizeDefault = "(Use global setting)";
|
scope.videoBufSizeDefault = "(Use global setting)";
|
||||||
|
|
||||||
scope.randomizeBlockShuffle = false;
|
|
||||||
|
|
||||||
scope.advancedTools = (localStorage.getItem("channel-programming-advanced-tools" ) === "show");
|
|
||||||
|
|
||||||
let refreshScreenResolution = async () => {
|
let refreshScreenResolution = async () => {
|
||||||
|
|
||||||
|
|
||||||
@ -1686,21 +1617,13 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
|
|
||||||
|
|
||||||
scope.onTimeSlotsDone = (slotsResult) => {
|
scope.onTimeSlotsDone = (slotsResult) => {
|
||||||
if (slotsResult === null) {
|
scope.channel.scheduleBackup = slotsResult.schedule;
|
||||||
delete scope.channel.scheduleBackup;
|
readSlotsResult(slotsResult);
|
||||||
} else {
|
|
||||||
scope.channel.scheduleBackup = slotsResult.schedule;
|
|
||||||
readSlotsResult(slotsResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.onRandomSlotsDone = (slotsResult) => {
|
scope.onRandomSlotsDone = (slotsResult) => {
|
||||||
if (slotsResult === null) {
|
scope.channel.randomScheduleBackup = slotsResult.schedule;
|
||||||
delete scope.channel.randomScheduleBackup;
|
readSlotsResult(slotsResult);
|
||||||
} else {
|
|
||||||
scope.channel.randomScheduleBackup = slotsResult.schedule;
|
|
||||||
readSlotsResult(slotsResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1713,73 +1636,6 @@ module.exports = function ($timeout, $location, dizquetv, resolutionOptions, get
|
|||||||
scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup );
|
scope.randomSlots.startDialog(progs, scope.maxSize, scope.channel.randomScheduleBackup );
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.rerollRandomSlots = () => {
|
|
||||||
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
|
|
||||||
scope.randomSlots.startDialog(
|
|
||||||
progs, scope.maxSize, scope.channel.randomScheduleBackup,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
scope.hasNoRandomSlots = () => {
|
|
||||||
return (
|
|
||||||
(typeof(scope.channel.randomScheduleBackup) === 'undefined' )
|
|
||||||
||
|
|
||||||
(scope.channel.randomScheduleBackup == null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.rerollTimeSlots = () => {
|
|
||||||
let progs = commonProgramTools.removeDuplicates( scope.channel.programs );
|
|
||||||
scope.timeSlots.startDialog(
|
|
||||||
progs, scope.maxSize, scope.channel.scheduleBackup,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
scope.hasNoTimeSlots = () => {
|
|
||||||
return (
|
|
||||||
(typeof(scope.channel.scheduleBackup) === 'undefined' )
|
|
||||||
||
|
|
||||||
(scope.channel.scheduleBackup == null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
scope.toggleAdvanced = () => {
|
|
||||||
scope.advancedTools = ! scope.advancedTools;
|
|
||||||
localStorage.setItem("channel-programming-advanced-tools" , scope.advancedTools ? "show" : "hide");
|
|
||||||
}
|
|
||||||
scope.hasAdvancedTools = () => {
|
|
||||||
return scope.advancedTools;
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.toolWide = () => {
|
|
||||||
if ( scope.hasAdvancedTools()) {
|
|
||||||
return {
|
|
||||||
"col-xl-6": true,
|
|
||||||
"col-md-12" : true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
"col-xl-12": true,
|
|
||||||
"col-lg-12" : true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.toolThin = () => {
|
|
||||||
if ( scope.hasAdvancedTools()) {
|
|
||||||
return {
|
|
||||||
"col-xl-3": true,
|
|
||||||
"col-lg-6" : true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
"col-xl-6": true,
|
|
||||||
"col-lg-6" : true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scope.logoOnChange = (event) => {
|
scope.logoOnChange = (event) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', event.target.files[0]);
|
formData.append('image', event.target.files[0]);
|
||||||
|
|||||||
@ -11,13 +11,8 @@ module.exports = function (dizquetv, resolutionOptions) {
|
|||||||
scope.settings = settings
|
scope.settings = settings
|
||||||
})
|
})
|
||||||
scope.updateSettings = (settings) => {
|
scope.updateSettings = (settings) => {
|
||||||
delete scope.settingsError;
|
|
||||||
dizquetv.updateFfmpegSettings(settings).then((_settings) => {
|
dizquetv.updateFfmpegSettings(settings).then((_settings) => {
|
||||||
scope.settings = _settings
|
scope.settings = _settings
|
||||||
}).catch( (err) => {
|
|
||||||
if ( typeof(err.data) === "string") {
|
|
||||||
scope.settingsError = err.data;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
scope.resetSettings = (settings) => {
|
scope.resetSettings = (settings) => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
module.exports = function ($timeout, commonProgramTools, getShowData) {
|
module.exports = function ($timeout) {
|
||||||
return {
|
return {
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
templateUrl: 'templates/filler-config.html',
|
templateUrl: 'templates/filler-config.html',
|
||||||
@ -92,26 +92,13 @@ module.exports = function ($timeout, commonProgramTools, getShowData) {
|
|||||||
id: scope.id,
|
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 = () => {
|
scope.showList = () => {
|
||||||
return ! scope.showPlexLibrary;
|
return ! scope.showPlexLibrary;
|
||||||
}
|
}
|
||||||
scope.sortFillersByLength = () => {
|
scope.sortFillers = () => {
|
||||||
scope.content.sort( (a,b) => { return a.duration - b.duration } );
|
scope.content.sort( (a,b) => { return a.duration - b.duration } );
|
||||||
refreshContentIndexes();
|
refreshContentIndexes();
|
||||||
}
|
}
|
||||||
scope.sortFillersCorrectly = () => {
|
|
||||||
scope.content = commonProgramTools.sortShows(scope.content);
|
|
||||||
refreshContentIndexes();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.fillerRemoveAllFiller = () => {
|
scope.fillerRemoveAllFiller = () => {
|
||||||
scope.content = [];
|
scope.content = [];
|
||||||
refreshContentIndexes();
|
refreshContentIndexes();
|
||||||
|
|||||||
@ -6,7 +6,6 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
|||||||
scope: {
|
scope: {
|
||||||
onFinish: "=onFinish",
|
onFinish: "=onFinish",
|
||||||
height: "=height",
|
height: "=height",
|
||||||
positionChoice: "=positionChoice",
|
|
||||||
visible: "=visible",
|
visible: "=visible",
|
||||||
limit: "=limit",
|
limit: "=limit",
|
||||||
},
|
},
|
||||||
@ -15,7 +14,6 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
|||||||
if ( typeof(scope.limit) == 'undefined') {
|
if ( typeof(scope.limit) == 'undefined') {
|
||||||
scope.limit = 1000000000;
|
scope.limit = 1000000000;
|
||||||
}
|
}
|
||||||
scope.insertPoint = "end";
|
|
||||||
scope.customShows = [];
|
scope.customShows = [];
|
||||||
scope.origins = [];
|
scope.origins = [];
|
||||||
scope.currentOrigin = undefined;
|
scope.currentOrigin = undefined;
|
||||||
@ -39,7 +37,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
|||||||
updateCustomShows();
|
updateCustomShows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope._onFinish = (s, insertPoint) => {
|
scope._onFinish = (s) => {
|
||||||
if (s.length > scope.limit) {
|
if (s.length > scope.limit) {
|
||||||
if (scope.limit == 1) {
|
if (scope.limit == 1) {
|
||||||
scope.error = "Please select only one clip.";
|
scope.error = "Please select only one clip.";
|
||||||
@ -47,7 +45,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
|||||||
scope.error = `Please select at most ${scope.limit} clips.`;
|
scope.error = `Please select at most ${scope.limit} clips.`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scope.onFinish(s, insertPoint)
|
scope.onFinish(s)
|
||||||
scope.selection = []
|
scope.selection = []
|
||||||
scope.visible = false
|
scope.visible = false
|
||||||
}
|
}
|
||||||
@ -71,7 +69,7 @@ module.exports = function (plex, dizquetv, $timeout, commonProgramTools) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.selectLibrary = async (library) => {
|
scope.selectLibrary = async (library) => {
|
||||||
await scope.fillNestedIfNecessary(library, true);
|
await scope.fillNestedIfNecessary(library);
|
||||||
let p = library.nested.length;
|
let p = library.nested.length;
|
||||||
scope.pending += library.nested.length;
|
scope.pending += library.nested.length;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -177,22 +177,8 @@ module.exports = function ($timeout, dizquetv, getShowData) {
|
|||||||
{ id: "shuffle", description: "Shuffle" },
|
{ id: "shuffle", description: "Shuffle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let doWait = (millis) => {
|
let doIt = async() => {
|
||||||
return new Promise( (resolve) => {
|
|
||||||
$timeout( resolve, millis );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
let doIt = async(fromInstant) => {
|
|
||||||
let t0 = new Date().getTime();
|
|
||||||
let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule );
|
let res = await dizquetv.calculateRandomSlots(scope.programs, scope.schedule );
|
||||||
let t1 = new Date().getTime();
|
|
||||||
|
|
||||||
let w = Math.max(0, 250 - (t1 - t0) );
|
|
||||||
if (fromInstant && (w > 0) ) {
|
|
||||||
await doWait(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < scope.schedule.slots.length; i++) {
|
for (let i = 0; i < scope.schedule.slots.length; i++) {
|
||||||
delete scope.schedule.slots[i].weightPercentage;
|
delete scope.schedule.slots[i].weightPercentage;
|
||||||
}
|
}
|
||||||
@ -203,7 +189,7 @@ module.exports = function ($timeout, dizquetv, getShowData) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
let startDialog = (programs, limit, backup, instant) => {
|
let startDialog = (programs, limit, backup) => {
|
||||||
scope.limit = limit;
|
scope.limit = limit;
|
||||||
scope.programs = programs;
|
scope.programs = programs;
|
||||||
|
|
||||||
@ -227,15 +213,11 @@ module.exports = function ($timeout, dizquetv, getShowData) {
|
|||||||
id: "flex.",
|
id: "flex.",
|
||||||
description: "Flex",
|
description: "Flex",
|
||||||
} );
|
} );
|
||||||
scope.hadBackup = (typeof(backup) !== 'undefined');
|
if (typeof(backup) !== 'undefined') {
|
||||||
if (scope.hadBackup) {
|
|
||||||
loadBackup(backup);
|
loadBackup(backup);
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.visible = true;
|
scope.visible = true;
|
||||||
if (instant) {
|
|
||||||
scope.finished(false, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -243,18 +225,13 @@ module.exports = function ($timeout, dizquetv, getShowData) {
|
|||||||
startDialog: startDialog,
|
startDialog: startDialog,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
scope.finished = async (cancel, fromInstant) => {
|
scope.finished = async (cancel) => {
|
||||||
scope.error = null;
|
scope.error = null;
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
if ( scope.schedule.slots.length === 0) {
|
|
||||||
scope.onDone(null);
|
|
||||||
scope.visible = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
scope.loading = true;
|
scope.loading = true;
|
||||||
$timeout();
|
$timeout();
|
||||||
scope.onDone( await doIt(fromInstant) );
|
scope.onDone( await doIt() );
|
||||||
scope.visible = false;
|
scope.visible = false;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error("Unable to generate channel lineup", err);
|
console.error("Unable to generate channel lineup", err);
|
||||||
@ -290,20 +267,6 @@ module.exports = function ($timeout, dizquetv, getShowData) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.hideCreateLineup = () => {
|
|
||||||
return (
|
|
||||||
scope.disableCreateLineup()
|
|
||||||
&& (scope.schedule.slots.length == 0)
|
|
||||||
&& scope.hadBackup
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.showResetSlots = () => {
|
|
||||||
return scope.hideCreateLineup();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
scope.canShowSlot = (slot) => {
|
scope.canShowSlot = (slot) => {
|
||||||
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
|
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -203,23 +203,9 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
|
|||||||
{ id: "shuffle", description: "Shuffle" },
|
{ id: "shuffle", description: "Shuffle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
let doWait = (millis) => {
|
let doIt = async() => {
|
||||||
return new Promise( (resolve) => {
|
|
||||||
$timeout( resolve, millis );
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
let doIt = async(fromInstant) => {
|
|
||||||
scope.schedule.timeZoneOffset = (new Date()).getTimezoneOffset();
|
scope.schedule.timeZoneOffset = (new Date()).getTimezoneOffset();
|
||||||
let t0 = new Date().getTime();
|
|
||||||
let res = await dizquetv.calculateTimeSlots(scope.programs, scope.schedule );
|
let res = await dizquetv.calculateTimeSlots(scope.programs, scope.schedule );
|
||||||
let t1 = new Date().getTime();
|
|
||||||
|
|
||||||
let w = Math.max(0, 250 - (t1 - t0) );
|
|
||||||
if (fromInstant && (w > 0) ) {
|
|
||||||
await doWait(w);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.schedule = scope.schedule;
|
res.schedule = scope.schedule;
|
||||||
delete res.schedule.fake;
|
delete res.schedule.fake;
|
||||||
return res;
|
return res;
|
||||||
@ -228,7 +214,7 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
let startDialog = (programs, limit, backup, instant) => {
|
let startDialog = (programs, limit, backup) => {
|
||||||
scope.limit = limit;
|
scope.limit = limit;
|
||||||
scope.programs = programs;
|
scope.programs = programs;
|
||||||
|
|
||||||
@ -252,15 +238,11 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
|
|||||||
id: "flex.",
|
id: "flex.",
|
||||||
description: "Flex",
|
description: "Flex",
|
||||||
} );
|
} );
|
||||||
scope.hadBackup = (typeof(backup) !== 'undefined');
|
if (typeof(backup) !== 'undefined') {
|
||||||
if (scope.hadBackup) {
|
|
||||||
loadBackup(backup);
|
loadBackup(backup);
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.visible = true;
|
scope.visible = true;
|
||||||
if (instant) {
|
|
||||||
scope.finished(false, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -268,19 +250,13 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
|
|||||||
startDialog: startDialog,
|
startDialog: startDialog,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
scope.finished = async (cancel, fromInstant) => {
|
scope.finished = async (cancel) => {
|
||||||
scope.error = null;
|
scope.error = null;
|
||||||
if (!cancel) {
|
if (!cancel) {
|
||||||
if ( scope.schedule.slots.length === 0) {
|
|
||||||
scope.onDone(null);
|
|
||||||
scope.visible = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
scope.loading = true;
|
scope.loading = true;
|
||||||
$timeout();
|
$timeout();
|
||||||
scope.onDone( await doIt(fromInstant) );
|
scope.onDone( await doIt() );
|
||||||
scope.visible = false;
|
scope.visible = false;
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error("Unable to generate channel lineup", err);
|
console.error("Unable to generate channel lineup", err);
|
||||||
@ -316,18 +292,6 @@ module.exports = function ($timeout, dizquetv, getShowData ) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.hideCreateLineup = () => {
|
|
||||||
return (
|
|
||||||
scope.disableCreateLineup()
|
|
||||||
&& (scope.schedule.slots.length == 0)
|
|
||||||
&& scope.hadBackup
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.showResetSlots = () => {
|
|
||||||
return scope.hideCreateLineup();
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.canShowSlot = (slot) => {
|
scope.canShowSlot = (slot) => {
|
||||||
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
|
return (slot.showId != 'flex.') && !(slot.showId.startsWith('redirect.'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
<title>dizqueTV</title>
|
<title>dizqueTV</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/png" href="/favicon.svg" ></link>
|
<link rel="icon" type="image/png" href="/favicon.svg" ></link>
|
||||||
<link rel="stylesheet" href="bootstrap-4.4.1-dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="fontawesome-free-5.15.4-web/css/all.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css">
|
||||||
<link href="style.css" rel="stylesheet">
|
<link href="style.css" rel="stylesheet">
|
||||||
<link href="custom.css" rel="stylesheet">
|
<link href="custom.css" rel="stylesheet">
|
||||||
<script src="version.js"></script>
|
<script src="version.js"></script>
|
||||||
@ -33,12 +33,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</small>
|
</small>
|
||||||
</h1>
|
</h1>
|
||||||
<a href="#!/guide">{{'topMenu.guide' | i18next}}</a> -
|
<a href="#!/guide">Guide</a> - <a href="#!/channels">Channels</a> - <a href="#!/library">Library</a> - <a href="#!/player">Player</a> - <a href="#!/settings">Settings</a> - <a href="#!/version">Version</a>
|
||||||
<a href="#!/channels">{{'topMenu.channels' | i18next}}</a> -
|
|
||||||
<a href="#!/library">{{'topMenu.library' | i18next}}</a> -
|
|
||||||
<a href="#!/player">{{'topMenu.player' | i18next}}</a> -
|
|
||||||
<a href="#!/settings">{{'topMenu.settings' | i18next}}</a> -
|
|
||||||
<a href="#!/version">{{'topMenu.version' | i18next}}</a>
|
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
<span style="margin-right: 15px;">
|
<span style="margin-right: 15px;">
|
||||||
<a href="/api/xmltv.xml">XMLTV <span class="far fa-file-code"></span></a>
|
<a href="/api/xmltv.xml">XMLTV <span class="far fa-file-code"></span></a>
|
||||||
|
|||||||
@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"topMenu": {
|
|
||||||
"guide": "Guide",
|
|
||||||
"channels": "Channels",
|
|
||||||
"library": "Library",
|
|
||||||
"player": "Player",
|
|
||||||
"settings": "Settings",
|
|
||||||
"version": "Version"
|
|
||||||
},
|
|
||||||
"guide": {
|
|
||||||
"title": "Tv Guide",
|
|
||||||
"attempt_to_play_channel": "Attempt to play channel: {{title}} in local media player"
|
|
||||||
},
|
|
||||||
"settings_server": {
|
|
||||||
"title": "Plex Settings",
|
|
||||||
"servers": "Plex Servers",
|
|
||||||
"sign_server": "Sign In/Add Servers",
|
|
||||||
"add_server": "Add a Plex Server",
|
|
||||||
"minutes_to_sign_plex": "You have 2 minutes to sign into your Plex Account.",
|
|
||||||
"name": "Name",
|
|
||||||
"uri": "URI",
|
|
||||||
"ui_route": "UI Route",
|
|
||||||
"backend_route": "Backend Route",
|
|
||||||
"ok": "ok",
|
|
||||||
"error": "error",
|
|
||||||
"ui_bad": "If a Plex server configuration has problems with the UI route, the channel editor won't be able to access its content.",
|
|
||||||
"backend_bad": "If a Plex server configuration has problems with the backend route, dizqueTV won't be able to play its content.",
|
|
||||||
"plex_transcoder_settings": "Plex Transcoder Settings",
|
|
||||||
"update": "Update",
|
|
||||||
"reset_options": "Reset Options",
|
|
||||||
"debug_logging": "Debug logging",
|
|
||||||
"paths": "Paths",
|
|
||||||
"send_status_plex": "Send play status to Plex",
|
|
||||||
"send_status_plex_note": "Note: This affects the \"on deck\" for your plex account.",
|
|
||||||
"no_plex_path": "If stream changes video codec, audio codec, or audio channels upon episode change, you will experience playback issues unless ffmpeg transcoding and normalization are also enabled.",
|
|
||||||
|
|
||||||
"video_options": "Video Options",
|
|
||||||
"supported_video_formats": "Supported Video Formats",
|
|
||||||
"max_playable_resolution": "Max Playable Resolution",
|
|
||||||
"max_transcode_resolution": "Max Transcode Resolution",
|
|
||||||
"audio_options": "Audio Options",
|
|
||||||
"supported_audio_formats": "Supported Audio Formats",
|
|
||||||
"supported_audio_formats_note": "Comma separated list. Some possible values are 'ac3,aac,mp3'.",
|
|
||||||
"max_audio_channels": "Maximum Audio Channels",
|
|
||||||
"max_audio_channels_note": "Note: 7.1 audio and on some clients, 6.1, is known to cause playback issues.",
|
|
||||||
"audio_boost": "Audio Boost",
|
|
||||||
"audio_boost_note": "Note: Only applies when downmixing to stereo.",
|
|
||||||
"miscellaneous_options": "Miscellaneous Options",
|
|
||||||
"max_direct_stream_bitrate": "Max Direct Stream Bitrate (Kbps)",
|
|
||||||
"max_transcode_bitrate": "Max Transcode Bitrate (Kbps)",
|
|
||||||
"max_transcode_bitrate_note": "Plex will decide to transcode or direct play based on these settings and if Plex transcodes, it will try to match the transcode bitrate.",
|
|
||||||
"direct_stream_media_buffer": "Direct Stream Media Buffer Size",
|
|
||||||
"transcode_media_buffer": "Transcode Media Buffer Size",
|
|
||||||
"stream_protocol": "Stream Protocol",
|
|
||||||
"force_direct_play": "Force Direct Play",
|
|
||||||
"subtitle_options": "Subtitle Options",
|
|
||||||
"subtitle_size": "Subtitle Size",
|
|
||||||
"enable_subtitle": "Enable Subtitles (Requires Transcoding)",
|
|
||||||
"path_replacements": "Path Replacements",
|
|
||||||
"original_plex_path": "Original Plex path to replace:",
|
|
||||||
"replace_plex_path": "Replace Plex path with:"
|
|
||||||
|
|
||||||
},
|
|
||||||
"settings_xmltv": {
|
|
||||||
"title": "XMLTV Settings",
|
|
||||||
"update": "Update",
|
|
||||||
"reset_options": "Reset Options",
|
|
||||||
"output_path": "Output Path",
|
|
||||||
"output_path_note": "You can edit this location in file xmltv-settings.json.",
|
|
||||||
"epg_hours": "EPG Hours",
|
|
||||||
"epg_hours_note": "How many hours of programming to include in the xmltv file.",
|
|
||||||
"refresh_timer": "Refresh Timer (hours)",
|
|
||||||
"refresh_timer_note": "How often should the xmltv file be updated.",
|
|
||||||
"image_cache": "Image Cache",
|
|
||||||
"image_cache_note": "If enabled the pictures used for Movie and TV Show posters will be cached in dizqueTV's .dizqueTV folder and will be delivered by dizqueTV's server instead of requiring calls to Plex. Note that using fixed xmltv location in Plex (as opposed to url) will not work correctly in this case."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -390,8 +390,4 @@ div.programming-programs div.list-group-item {
|
|||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
margin-bottom: 0.25em;
|
margin-bottom: 0.25em;
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item .library-item-hover:hover {
|
|
||||||
background: #D0D0FF
|
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@
|
|||||||
<input id="channelEndTime" class="form-control form-control-sm col-md-auto" type="datetime-local" ng-model="endTime" ng-disabled="true" aria-describedby="endTimeHelp"></input>
|
<input id="channelEndTime" class="form-control form-control-sm col-md-auto" type="datetime-local" ng-model="endTime" ng-disabled="true" aria-describedby="endTimeHelp"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class='col-md-auto'>
|
<div class='col-md-auto'>
|
||||||
<small class="text-muted form-text" id='endTimeHelp'>Programming will restart from the beginning. </small><small ng-show='channel.onDemand.isOnDemand' class="text-muted form-text" id='endTimeHelp'>For on-demand channels, the times in the schedule are tentative. </small>
|
<small class="text-muted form-text" id='endTimeHelp'>Programming will restart from the beginning.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -170,7 +170,6 @@
|
|||||||
ng-init="setUpWatcher()"
|
ng-init="setUpWatcher()"
|
||||||
ng-if="true"
|
ng-if="true"
|
||||||
ng-style="{'max-height':programmingHeight()}"
|
ng-style="{'max-height':programmingHeight()}"
|
||||||
id="channelConfigProgramList"
|
|
||||||
>
|
>
|
||||||
<div ng-repeat="x in channel.programs track by x.$index"
|
<div ng-repeat="x in channel.programs track by x.$index"
|
||||||
ng-click="selectProgram(x.$index)"
|
ng-click="selectProgram(x.$index)"
|
||||||
@ -198,29 +197,23 @@
|
|||||||
<small class='text-info'>There are no programs in the channel, use the <i class='fas fa-plus'></i> button to add programs from your media library or use the Tools to add Flex time or a Channel Redirect</small>
|
<small class='text-info'>There are no programs in the channel, use the <i class='fas fa-plus'></i> button to add programs from your media library or use the Tools to add Flex time or a Channel Redirect</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class='{
|
<div class='col-md-4 col-sm-12 col-xl-6 col-lg-5 programming-pane tools-pane' ng-show="showShuffleOptions"
|
||||||
"col-md-4" : true,
|
|
||||||
"col-sm-12" : true,
|
|
||||||
"col-xl-6" : hasAdvancedTools(),
|
|
||||||
"col-lg-5" : hasAdvancedTools(),
|
|
||||||
"col-xl-4" : !hasAdvancedTools(),
|
|
||||||
"col-lg-3" : !hasAdvancedTools()
|
|
||||||
}' class='programming-pane tools-pane' ng-show="showShuffleOptions"
|
|
||||||
ng-style="{'max-height':programmingHeight()}"
|
ng-style="{'max-height':programmingHeight()}"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-md-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<input type="number" class="form-control form-control-sm" placeholder="Desired number of consecutive TV shows." min="1" max="10" ng-model="blockCount" style="width:5em">
|
<input type="number" class="form-control form-control-sm" placeholder="Desired number of consecutive TV shows." min="1" max="10" ng-model="blockCount" style="width:5em">
|
||||||
</input>
|
</input>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<select class="custom-select" ng-model="randomizeBlockShuffle">
|
<div class="input-group-text" style="padding: 0;">
|
||||||
<option ng-value="false" label="Fixed" />
|
<label class="small" for="randomizeBlockShuffle" style="margin-bottom: 2px;"> Randomize </label>
|
||||||
<option ng-value="true" label="Random" />
|
<input id="randomizeBlockShuffle" type="checkbox" ng-model="randomizeBlockShuffle"></input>
|
||||||
</select>
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="blockShuffle(blockCount, randomizeBlockShuffle)">
|
||||||
<i class='fa fa-random' title='Block Shuffle' ></i> Block Shuffle
|
<i class='fa fa-random' title='Block Shuffle' ></i> Block Shuffle
|
||||||
@ -230,7 +223,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()" aria-describedby="randomShuffleHelp" title='Random Shuffle'>
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="randomShuffle()" aria-describedby="randomShuffleHelp" title='Random Shuffle'>
|
||||||
<i class='fa fa-random'></i> Random Shuffle
|
<i class='fa fa-random'></i> Random Shuffle
|
||||||
@ -241,7 +234,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
|
<div class='col-xl-3 col-lg-6' style="padding: 5px;" ng-show="hasPrograms()" >
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()" title='Cyclic Shuffle' >
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="cyclicShuffle()" title='Cyclic Shuffle' >
|
||||||
<i class='fa fa-random'></i> Cyclic Shuffle
|
<i class='fa fa-random'></i> Cyclic Shuffle
|
||||||
@ -250,7 +243,7 @@
|
|||||||
<p ng-show='showHelp.check'>Like Random Shuffle, but tries to preserve the sequence of episodes for each TV show. If a TV show has multiple instances of its episodes, they are also cycled appropriately.</p>
|
<p ng-show='showHelp.check'>Like Random Shuffle, but tries to preserve the sequence of episodes for each TV show. If a TV show has multiple instances of its episodes, they are also cycled appropriately.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="replicaCount" style="width:5em">
|
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="replicaCount" style="width:5em">
|
||||||
@ -262,7 +255,7 @@
|
|||||||
<p ng-show='showHelp.check'>Makes multiple copies of the schedule and plays them in sequence. Normally this isn't necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.</p>
|
<p ng-show='showHelp.check'>Makes multiple copies of the schedule and plays them in sequence. Normally this isn't necessary, because dizqueTV will always play the schedule back from the beginning when it finishes. But creating replicas is a useful intermediary step sometimes before applying other transformations. Note that because very large channels can be problematic, the number of replicas will be limited to avoid creating really large channels.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="randomReplicaCount" style="width:5em">
|
<input type="number" class="form-control form-control-sm" placeholder="Repeats" min="1" max="{{maxReplicas()}}" ng-model="randomReplicaCount" style="width:5em">
|
||||||
@ -274,7 +267,7 @@
|
|||||||
<p ng-show='showHelp.check'>Like "Replicate", it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.</p>
|
<p ng-show='showHelp.check'>Like "Replicate", it will make multiple copies of the programming. In addition it will shuffle the programs, but it will make sure not to have too small a distance between two identical programs.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
|
||||||
<div class='input-group'>
|
<div class='input-group'>
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()" title='Sort TV Shows' >
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortShows()" title='Sort TV Shows' >
|
||||||
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
|
<i class='fa fa-sort-alpha-down'></i> Sort TV Shows
|
||||||
@ -285,7 +278,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()" >
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()" title='Sort Release Dates' >
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()" title='Sort Release Dates' >
|
||||||
<i class='fa fa-sort-numeric-down'></i> Sort Release Dates
|
<i class='fa fa-sort-numeric-down'></i> Sort Release Dates
|
||||||
@ -294,7 +287,7 @@
|
|||||||
<p ng-show='showHelp.check'>Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.</p>
|
<p ng-show='showHelp.check'>Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()" >
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()" title='Balance Shows' >
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()" title='Balance Shows' >
|
||||||
<i class='fa fa-balance-scale'></i> Balance Shows
|
<i class='fa fa-balance-scale'></i> Balance Shows
|
||||||
@ -303,7 +296,7 @@
|
|||||||
<p ng-show='showHelp.check'>Will replicate some TV shows or delete duplicates of other TV shows in an effort to make it so the total durations of all episodes of each episode are as similar as possible. It's usually impossible to make the shows perfectly balanced without creating a really high number of duplicates, but it will try to get close. Movies are treated as a single show.</p>
|
<p ng-show='showHelp.check'>Will replicate some TV shows or delete duplicates of other TV shows in an effort to make it so the total durations of all episodes of each episode are as similar as possible. It's usually impossible to make the shows perfectly balanced without creating a really high number of duplicates, but it will try to get close. Movies are treated as a single show.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()" >
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()" >
|
||||||
<div class='input-group'>
|
<div class='input-group'>
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()" title='Tweak Weights...'>
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="startFrequencyTweak()" title='Tweak Weights...'>
|
||||||
<i class='fa fa-balance-scale'></i> Tweak Weights...
|
<i class='fa fa-balance-scale'></i> Tweak Weights...
|
||||||
@ -321,7 +314,7 @@
|
|||||||
<p ng-show='showHelp.check'>Programs a Flex time slot. Normally you'd use pad times, restrict times or add breaks to add a large quantity of Flex times at once, but this exists for more specific cases.</p>
|
<p ng-show='showHelp.check'>Programs a Flex time slot. Normally you'd use pad times, restrict times or add breaks to add a large quantity of Flex times at once, but this exists for more specific cases.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<select class="custom-select" ng-model="nightStart"
|
<select class="custom-select" ng-model="nightStart"
|
||||||
@ -337,7 +330,7 @@
|
|||||||
<p ng-show='showHelp.check'>The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.</p>
|
<p ng-show='showHelp.check'>The channel's regular programming between the specified hours. Flex time will fill up the remaining hours.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<select class="custom-select" ng-model="paddingOption"
|
<select class="custom-select" ng-model="paddingOption"
|
||||||
@ -351,7 +344,7 @@
|
|||||||
<p ng-show='showHelp.check'>Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones. This button might be disabled if the channel is already too large.</p>
|
<p ng-show='showHelp.check'>Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones. This button might be disabled if the channel is already too large.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<select class="custom-select" style="width:5em" ng-model="breakAfter"
|
<select class="custom-select" style="width:5em" ng-model="breakAfter"
|
||||||
@ -368,7 +361,7 @@
|
|||||||
<p ng-show='showHelp.check'>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes. This button might be disabled if the channel is already too large.</p>
|
<p ng-show='showHelp.check'>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes. This button might be disabled if the channel is already too large.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<select class="custom-select" ng-model="rerunStart"
|
<select class="custom-select" ng-model="rerunStart"
|
||||||
@ -388,7 +381,7 @@
|
|||||||
<p ng-show='showHelp.check'>Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon. This button might be disabled if the channel is already too large.</p>
|
<p ng-show='showHelp.check'>Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon. This button might be disabled if the channel is already too large.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group" >
|
<div class="input-group" >
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<button class="btn btn-sm btn-secondary form-control form-control-sm" type="button" ng-click="savePositions()">
|
<button class="btn btn-sm btn-secondary form-control form-control-sm" type="button" ng-click="savePositions()">
|
||||||
@ -414,7 +407,7 @@
|
|||||||
<p ng-show='showHelp.check'>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
|
<p ng-show='showHelp.check'>Adds a channel redirect. During this period of time, the channel will redirect to another channel.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<div class='loader' ng-hide='channelsDownloaded'></div>
|
<div class='loader' ng-hide='channelsDownloaded'></div>
|
||||||
@ -432,7 +425,7 @@
|
|||||||
<p ng-show='showHelp.check'>Will redirect to another channel while between the selected hours.</p>
|
<p ng-show='showHelp.check'>Will redirect to another channel while between the selected hours.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolWide()" style="padding: 5px;" ng-show="hasPrograms()">
|
<div class="col-xl-6 col-lg-12" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<button class='btn btn-sm btn-warning form-control' ng-click="slideAllPrograms(-slide.value)"
|
<button class='btn btn-sm btn-warning form-control' ng-click="slideAllPrograms(-slide.value)"
|
||||||
@ -456,51 +449,27 @@
|
|||||||
<p ng-show='showHelp.check'>Slides the whole schedule. The "Fast-Forward" button will advance the stream by the specified amount of time. The "Rewind" button does the opposite.</p>
|
<p ng-show='showHelp.check'>Slides the whole schedule. The "Fast-Forward" button will advance the stream by the specified amount of time. The "Rewind" button does the opposite.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class="input-group" >
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
|
||||||
<button class="btn btn-sm btn-warning form-control"
|
|
||||||
type="button"
|
|
||||||
ng-click="rerollTimeSlots()"
|
|
||||||
ng-disabled="hasNoTimeSlots()"
|
|
||||||
title = "Regenerate time slots..."
|
|
||||||
>
|
|
||||||
<i class='fas fa-redo'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class='btn btn-sm btn-warning form-control' ng-click="onTimeSlotsButtonClick()"
|
<button class='btn btn-sm btn-warning form-control' ng-click="onTimeSlotsButtonClick()"
|
||||||
title="Time Slots..."
|
title="Time Slots..."
|
||||||
>
|
>
|
||||||
<i class='fas fa-blender'></i> Time Slots...
|
<i class='fas fa-blender'></i> Time Slots...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p ng-show='showHelp.check'>This allows to schedule specific shows to run at specific time slots of the day or a week. It's recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.</p>
|
<p ng-show='showHelp.check'>This allows to schedul specific shows to run at specific time slots of the day or a week. It's recommended you first populate the channel with the episodes from the shows you want to play and/or other content like movies and redirects.</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-class="toolThin()" style="padding: 5px;" ng-show="hasPrograms()">
|
<div class="col-xl-3 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
|
<div class="input-group">
|
||||||
|
|
||||||
<div class="input-group" >
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<button class="btn btn-sm btn-warning form-control"
|
|
||||||
type="button"
|
|
||||||
ng-click="rerollRandomSlots()"
|
|
||||||
ng-disabled="hasNoRandomSlots()"
|
|
||||||
title = "Regenerate random slots..."
|
|
||||||
>
|
|
||||||
<i class='fas fa-redo'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class='btn btn-sm btn-warning form-control' ng-click="onRandomSlotsButtonClick()"
|
<button class='btn btn-sm btn-warning form-control' ng-click="onRandomSlotsButtonClick()"
|
||||||
title="Random Slots..."
|
title="Random Slots..."
|
||||||
>
|
>
|
||||||
<i class='fas fa-flask'></i> Random Slots...
|
<i class='fas fa-flask'></i> Random Slots...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p ng-show='showHelp.check'>This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block. Once a channel has been configured with random slots, the reload button can re-evaluate them again, with the saved settings.</p>
|
<p ng-show='showHelp.check'>This is similar to Time Slots, but instead of time sections, you pick a probability to play each tv show and the length of the block.</p>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -524,7 +493,7 @@
|
|||||||
<p ng-show='showHelp.check'>Removes any Flex periods from the schedule.</p>
|
<p ng-show='showHelp.check'>Removes any Flex periods from the schedule.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms() && hasAdvancedTools()">
|
<div class="col-md-auto" style="padding: 5px;" ng-show="hasPrograms()">
|
||||||
<div class='input-group'>
|
<div class='input-group'>
|
||||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()" title='Remove Specials' >
|
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSpecials()" title='Remove Specials' >
|
||||||
<i class='fa fa-trash-alt'></i> Specials
|
<i class='fa fa-trash-alt'></i> Specials
|
||||||
@ -550,24 +519,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<p ng-show='showHelp.check'>Wipes out the schedule so that you can start over.</p>
|
<p ng-show='showHelp.check'>Wipes out the schedule so that you can start over.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<div class="col-xl-6 col-lg-6" style="padding: 5px;" ng-show="hasPrograms()">
|
|
||||||
|
|
||||||
|
|
||||||
<div class="input-group" >
|
|
||||||
|
|
||||||
<button class='btn btn-sm btn-outline-secondary form-control' ng-click="toggleAdvanced()"
|
|
||||||
title="Toggle extra tools..."
|
|
||||||
>
|
|
||||||
<i class='fas fa-tools'></i> {{ hasAdvancedTools() ? "Less" : "More"}} Tools...
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p ng-show='showHelp.check'>Use this button to show or hide a bunch of additional tools that might be useful.</p>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -887,38 +838,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
============= TAB: ON-DEMAND =========================
|
|
||||||
-->
|
|
||||||
|
|
||||||
<div class="modal-body" ng-if="tab == 'ondemand'">
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="onDemand" aria-describedby="onDemandHelp" ng-model='channel.onDemand.isOnDemand'>
|
|
||||||
<label class="form-check-label" for="onDemand">On-Demand</label>
|
|
||||||
|
|
||||||
<span class='text-muted' id="stealthHelp">(The channel's programming will be paused when it is not being played. No programs will appear in the TV-guide while the channel is paused.)</span>
|
|
||||||
</div>
|
|
||||||
<br></br>
|
|
||||||
|
|
||||||
<div class='form-group' ng-show='channel.onDemand.isOnDemand'>
|
|
||||||
<label class='form-label' for="segmentLength" >Segment Length:</label>
|
|
||||||
|
|
||||||
<select class="form-control custom-select" id="segmentLength" ng-model="channel.onDemand.modulo" convert-to-number >
|
|
||||||
<option ng-value="1">Instant</option>
|
|
||||||
<option ng-value="300000">5 minutes</option>
|
|
||||||
<option ng-value="600000">10 minutes</option>
|
|
||||||
<option ng-value="900000">15 minutes</option>
|
|
||||||
<option ng-value="1800000">30 minutes</option>
|
|
||||||
<option ng-value="6000000">1 hour</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
|
|
||||||
<small id='guideFlexHelp' class="text-muted" for='guideFlex'>Channel will be divided in segments. For example, if you use padding or time slots in your channel so that everything starts at 0:00 or 0:30 , you want a 30 minutes-segment. Use no segment if you want the channel to play exactly where you left it. Flex time will be added if necessary for padding.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<span class="pull-right text-danger" ng-show="error.any"> <i class='fa fa-exclamation-triangle'></i> There were errors. Please review the form.</span>
|
<span class="pull-right text-danger" ng-show="error.any"> <i class='fa fa-exclamation-triangle'></i> There were errors. Please review the form.</span>
|
||||||
<span class="pull-right text-info" ng-show='! hasPrograms() && (tab != "programming")'> <i class='fas fa-info-circle'></i> Use the "Programming" tab to add programs to the channel.</span>
|
<span class="pull-right text-info" ng-show='! hasPrograms() && (tab != "programming")'> <i class='fas fa-info-circle'></i> Use the "Programming" tab to add programs to the channel.</span>
|
||||||
@ -939,7 +858,7 @@
|
|||||||
<frequency-tweak programs="_programFrequencies" message="_frequencyMessage" modified="_frequencyModified" on-done="tweakFrequencies"></frequency-tweak>
|
<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>
|
<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>
|
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
|
||||||
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" position-choice=true on-finish="importPrograms"></plex-library>
|
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||||
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></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>
|
<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>
|
<time-slots-schedule-editor linker="registerTimeSlots" on-done="onTimeSlotsDone"></time-slots-schedule-editor>
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
<div>
|
<div>
|
||||||
<small class="text-danger" nf-show="settingsError">
|
|
||||||
{{ settingsError }}
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<h5>FFMPEG Settings
|
<h5>FFMPEG Settings
|
||||||
|
|
||||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||||
@ -12,48 +8,9 @@
|
|||||||
Reset Options
|
Reset Options
|
||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
|
<h6>FFMPEG Executable Path (eg: C:\ffmpeg\bin\ffmpeg.exe || /usr/bin/ffmpeg)</h6>
|
||||||
<hr></hr>
|
<input type="text" class="form-control form-control-sm" ria-describedby="ffmpegHelp" ng-model="settings.ffmpegPath"></input>
|
||||||
<h6>FFMPEG Executable Path</h6>
|
<small id="ffmpegHelp" class="form-text text-muted">FFMPEG version 4.2+ required. Check by opening the version tab</small>
|
||||||
<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>
|
<hr></hr>
|
||||||
<h6>Miscellaneous Options</h6>
|
<h6>Miscellaneous Options</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -239,18 +196,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -37,23 +37,16 @@
|
|||||||
<div ng-show="showTools">
|
<div ng-show="showTools">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="input-group col-md-3" style="padding: 5px;">
|
<div class="input-group col-md-3" style="padding: 5px;">
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillersCorrectly()">
|
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillers()">
|
||||||
<i class='fa fa-sort-alpha-down'></i> Sort Clips
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group col-md-3" style="padding: 5px;">
|
|
||||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillersByLength()">
|
|
||||||
<i class='fa fa-sort-amount-down-alt'></i> Sort Lengths
|
<i class='fa fa-sort-amount-down-alt'></i> Sort Lengths
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-md-3" style="padding: 5px;">
|
<div class="input-group col-md-3" style="padding: 5px;">
|
||||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">
|
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">
|
||||||
<i class='fa fa-trash-alt'></i> Remove Duplicates
|
<i class='fa fa-trash-alt'></i> Remove Duplicates
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-md-3" style="padding: 5px;">
|
<div class="input-group col-md-6" style="padding: 5px;">
|
||||||
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">
|
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">
|
||||||
<i class='fa fa-trash-alt'></i> Remove All Filler
|
<i class='fa fa-trash-alt'></i> Remove All Filler
|
||||||
</button>
|
</button>
|
||||||
@ -86,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ng-style="programSquareStyle(x, false)" ></div>
|
<div ng-style="programSquareStyle(x, false)" ></div>
|
||||||
<div class="title" >
|
<div class="title" >
|
||||||
{{ getText(x) }}
|
{{x.title}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-pull-right">
|
<div class="flex-pull-right">
|
||||||
<button class="btn btn-sm btn-link" ng-click="contentSplice(x.$index,1)">
|
<button class="btn btn-sm btn-link" ng-click="contentSplice(x.$index,1)">
|
||||||
|
|||||||
@ -28,59 +28,46 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<select class="form-control form-control-sm custom-select" ng-model="currentOrigin"
|
||||||
<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>
|
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>
|
<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">
|
<li class="list-group-item" ng-repeat="a in libraries">
|
||||||
<div class="flex-container library-item-hover {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
|
<div class="flex-container {{ displayImages ? 'w_images' : 'wo_images' }}" ng-click="getNested(a, true);">
|
||||||
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
<span class="fa {{ a.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
||||||
<img ng-if="displayImages" lazy-img="{{a.icon}}" ></img>
|
<img ng-if="displayImages" lazy-img="{{a.icon}}" ></img>
|
||||||
<span>{{ displayTitle(a) }}</span><!-- Library -->
|
<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 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-circle btn"></span>
|
<span class="fa fa-plus btn"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul ng-if="a.collapse" class="list-group">
|
<ul ng-if="a.collapse" class="list-group">
|
||||||
<li class="list-group-item {{ b.type !== 'movie' ? 'list-group-item-secondary' : 'list-group-item-video' }}"
|
<li class="list-group-item {{ b.type !== 'movie' ? 'list-group-item-secondary' : 'list-group-item-video' }}"
|
||||||
ng-repeat="b in a.nested">
|
ng-repeat="b in a.nested">
|
||||||
<div class="flex-container library-item-hover"
|
<div class="flex-container"
|
||||||
ng-click="b.type !== 'movie' ? getNested(b) : selectItem(b, true)">
|
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="tab"></span>
|
||||||
<span ng-if="b.type !== 'movie'" class="fa {{ b.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} 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>
|
<img ng-if="displayImages" lazy-img="{{ b.type === 'episode' ? b.episodeIcon : b.icon }}" ></img>
|
||||||
<span class="flex-grow-1">{{ displayTitle(b) }}</span>
|
{{ displayTitle(b) }}
|
||||||
<span ng-if="b.type === 'movie'" class="">
|
<span ng-if="b.type === 'movie'" class="flex-pull-right">
|
||||||
{{b.durationStr}}
|
{{b.durationStr}}
|
||||||
</span>
|
</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 ng-if="b.type === 'playlist'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectPlaylist(b);">
|
||||||
<span class="fa fa-plus-circle btn"></span>
|
<span class="fa fa-plus btn"></span>
|
||||||
</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 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-circle btn"></span>
|
<span class="fa fa-plus btn"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul ng-if="b.collapse" class="list-group">
|
<ul ng-if="b.collapse" class="list-group">
|
||||||
<li ng-repeat="c in b.nested"
|
<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' }}">
|
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 library-item-hover"
|
<div class="flex-container"
|
||||||
ng-click="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track' ? getNested(c) : selectItem(c, true)">
|
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'"
|
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||||
class="tab"></span>
|
class="tab"></span>
|
||||||
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||||
@ -88,30 +75,23 @@
|
|||||||
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
<span ng-if="c.type !== 'movie' && c.type !== 'episode' && c.type !== 'track'"
|
||||||
class="fa {{ c.collapse ? 'fa-chevron-down' : 'fa-chevron-right' }} tab"></span>
|
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>
|
<img ng-if="displayImages" lazy-img="{{c.type === 'episode' ? c.episodeIcon : c.icon }}" ></img>
|
||||||
<span class="flex-grow-1">{{ displayTitle(c) }}</span>
|
{{ displayTitle(c) }}
|
||||||
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track' "
|
<span ng-if="c.type === 'movie' || c.type === 'episode' || c.type === 'track' "
|
||||||
class="">
|
class="flex-pull-right">
|
||||||
{{c.durationStr}}
|
{{c.durationStr}}
|
||||||
</span>
|
</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 ng-if="c.type === 'season' || c.type === 'album'" class="flex-pull-right" ng-click="$event.stopPropagation(); selectSeason(c);">
|
||||||
<span class="fa fa-plus-circle btn"></span>
|
<span class="fa fa-plus btn"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ul ng-if="c.collapse" class="list-group">
|
<ul ng-if="c.collapse" class="list-group">
|
||||||
<li class="list-group-item list-group-item-video"
|
<li class="list-group-item list-group-item-video"
|
||||||
ng-repeat="d in c.nested">
|
ng-repeat="d in c.nested">
|
||||||
<div class="flex-container library-item-hover" ng-click="selectItem(d, true)">
|
<div class="flex-container" ng-click="selectItem(d, true)">
|
||||||
|
<span class="fa fa-plus-circle tab"></span>
|
||||||
<img ng-if="displayImages" lazy-img="{{d.episodeIcon}}" ></img>
|
<img ng-if="displayImages" lazy-img="{{d.episodeIcon}}" ></img>
|
||||||
<span class="flex-grow-1">{{ displayTitle(d) }}</span>
|
{{ displayTitle(d) }}
|
||||||
<span class="">{{d.durationStr}}</span>
|
<span class="flex-pull-right">{{d.durationStr}}</span>
|
||||||
<span class="flex-pull-right">
|
|
||||||
<span class="fa fa-plus-circle btn"></span>
|
|
||||||
</span>
|
|
||||||
<!-- Episode -->
|
<!-- Episode -->
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -131,7 +111,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<div class="loader" ng-if="pending > 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
|
<div class="loader" ng-if="pending > 0" ></div> <h6 style='display:inline-block'>Selected Items</h6>
|
||||||
@ -150,21 +129,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='text-danger'>{{error}}</div>
|
<div class='text-danger'>{{error}}</div>
|
||||||
|
<div class="modal-footer">
|
||||||
<div class="modal-footer flex">
|
<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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,28 +1,28 @@
|
|||||||
<div>
|
<div>
|
||||||
<h5>{{'settings_server.title' | i18next}}</h5>
|
<h5>Plex Settings</h5>
|
||||||
<h6>{{'settings_server.servers' | i18next}}
|
<h6>Plex Servers
|
||||||
<button class="pull-right btn btn-sm btn-success" style="margin-bottom:10px;" ng-disabled="isProcessing" ng-click="addPlexServer()">
|
<button class="pull-right btn btn-sm btn-success" style="margin-bottom:10px;" ng-disabled="isProcessing" ng-click="addPlexServer()">
|
||||||
{{'settings_server.sign_server' | i18next}}
|
Sign In/Add Servers
|
||||||
</button>
|
</button>
|
||||||
</h6>
|
</h6>
|
||||||
<div ng-if="isProcessing">
|
<div ng-if="isProcessing">
|
||||||
<br>
|
<br>
|
||||||
<h6>
|
<h6>
|
||||||
<span class="pull-right text-info">{{ isProcessing ? 'settings_server.minutes_to_sign_plex' : '' | i18next}}</span>
|
<span class="pull-right text-info">{{ isProcessing ? 'You have 2 minutes to sign into your Plex Account.' : ''}}</span>
|
||||||
</h6>
|
</h6>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{'settings_server.name' | i18next}}</th>
|
<th>Name</th>
|
||||||
<th>{{'settings_server.uri' | i18next}}</th>
|
<th>uri</th>
|
||||||
<th>{{'settings_server.ui_route' | i18next}}</th>
|
<th>UI Route</th>
|
||||||
<th>{{'settings_server.backend_route' | i18next}}</th>
|
<th>Backend Route</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="servers.length === 0">
|
<tr ng-if="servers.length === 0">
|
||||||
<td colspan="7">
|
<td colspan="7">
|
||||||
<p class="text-center text-danger">{{'settings_server.add_server' | i18next}}</p>
|
<p class="text-center text-danger">Add a Plex Server</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="serversPending">
|
<tr ng-if="serversPending">
|
||||||
@ -33,13 +33,13 @@
|
|||||||
<td><span class='text-secondary text-small'>{{ x.uri }}</span></td>
|
<td><span class='text-secondary text-small'>{{ x.uri }}</span></td>
|
||||||
<td>
|
<td>
|
||||||
<div class='loader' ng-if="x.uiStatus == 0"></div>
|
<div class='loader' ng-if="x.uiStatus == 0"></div>
|
||||||
<div class='text-success' ng-if="x.uiStatus == 1"><i class='fa fa-check'></i>{{'settings_server.ok' | i18next}}</div>
|
<div class='text-success' ng-if="x.uiStatus == 1"><i class='fa fa-check'></i>ok</div>
|
||||||
<div class='text-danger' ng-if="x.uiStatus == -1"><i class='fa fa-exclamation-triangle'></i>{{'settings_server.error' | i18next}}</div>
|
<div class='text-danger' ng-if="x.uiStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class='loader' ng-if="x.backendStatus == 0"></div>
|
<div class='loader' ng-if="x.backendStatus == 0"></div>
|
||||||
<div class='text-success' ng-if="x.backendStatus == 1"><i class='fa fa-check'></i>{{'settings_server.ok' | i18next}}</div>
|
<div class='text-success' ng-if="x.backendStatus == 1"><i class='fa fa-check'></i>ok</div>
|
||||||
<div class='text-danger' ng-if="x.backendStatus == -1"><i class='fa fa-exclamation-triangle'></i>{{'settings_server.error' | i18next}}</div>
|
<div class='text-danger' ng-if="x.backendStatus == -1"><i class='fa fa-exclamation-triangle'></i>error</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-secondary" ng-click="editPlexServer(x)">
|
<button class="btn btn-sm btn-outline-secondary" ng-click="editPlexServer(x)">
|
||||||
@ -53,25 +53,25 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="isAnyUIBad()">
|
<tr ng-if="isAnyUIBad()">
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<p class="text-center text-danger small">{{'settings_server.ui_bad' | i18next}}</p>
|
<p class="text-center text-danger small">If a Plex server configuration has problems with the UI route, the channel editor won't be able to access its content.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="isAnyBackendBad()">
|
<tr ng-if="isAnyBackendBad()">
|
||||||
<td colspan="5">
|
<td colspan="5">
|
||||||
<p class="text-center text-danger small">{{'settings_server.server_bad' | i18next}}</p>
|
<p class="text-center text-danger small">If a Plex server configuration has problems with the backend route, dizqueTV won't be able to play its content.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<h6>{{'settings_server.plex_transcoder_settings' | i18next}}
|
<h6>Plex Transcoder Settings
|
||||||
|
|
||||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||||
{{'settings_server.update' | i18next}}
|
Update
|
||||||
</button>
|
</button>
|
||||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||||
{{'settings_server.reset_options' | i18next}}
|
Reset Options
|
||||||
</button>
|
</button>
|
||||||
</h6>
|
</h6>
|
||||||
<hr>
|
<hr>
|
||||||
@ -79,10 +79,10 @@
|
|||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input id="debugLogging" type="checkbox" ng-model="settings.debugLogging"></input>
|
<input id="debugLogging" type="checkbox" ng-model="settings.debugLogging"></input>
|
||||||
<label for="debugLogging">{{'settings_server.debug_logging' | i18next}}</label>
|
<label for="debugLogging">Debug logging</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.paths' | i18next}}</label>
|
<label>Paths</label>
|
||||||
<select ng-model="settings.streamPath"
|
<select ng-model="settings.streamPath"
|
||||||
ng-options="o.id as o.description for o in pathOptions" ></select>
|
ng-options="o.id as o.description for o in pathOptions" ></select>
|
||||||
</div>
|
</div>
|
||||||
@ -90,110 +90,110 @@
|
|||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input id="updatePlayStatus" type="checkbox" ng-model="settings.updatePlayStatus" ria-describedby="updatePlayStatusHelp"></input>
|
<input id="updatePlayStatus" type="checkbox" ng-model="settings.updatePlayStatus" ria-describedby="updatePlayStatusHelp"></input>
|
||||||
<label for="updatePlayStatus">{{'settings_server.send_status_plex' | i18next}}</label>
|
<label for="updatePlayStatus">Send play status to Plex</label>
|
||||||
<small id="updatePlayStatusHelp" class="form-text text-muted">{{'settings_server.send_status_plex_note' | i18next}}</small>
|
<small id="updatePlayStatusHelp" class="form-text text-muted">Note: This affects the "on deck" for your plex account.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" ng-hide="hideIfNotPlexPath()">
|
<div class="row" ng-hide="hideIfNotPlexPath()">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<p class="text-center text-info small">{{'settings_server.no_plex_path' | i18next}}</p>
|
<p class="text-center text-info small">If stream changes video codec, audio codec, or audio channels upon episode change, you will experience playback issues unless ffmpeg transcoding and normalization are also enabled.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" ng-hide="hideIfNotPlexPath()">
|
<div class="row" ng-hide="hideIfNotPlexPath()">
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h6 style="font-weight: bold">{{'settings_server.video_options' | i18next}}</h6>
|
<h6 style="font-weight: bold">Video Options</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.supported_video_formats' | i18next}}</label>
|
<label>Supported Video Formats</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.videoCodecs" ria-describedby="videoCodecsHelp"></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.videoCodecs" ria-describedby="videoCodecsHelp"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.max_playable_resolution' | i18next}}</label>
|
<label>Max Playable Resolution</label>
|
||||||
<select ng-model="settings.maxPlayableResolution"
|
<select ng-model="settings.maxPlayableResolution"
|
||||||
ng-options="o.id as o.description for o in resolutionOptions" ></select>
|
ng-options="o.id as o.description for o in resolutionOptions" ></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.max_transcode_resolution' | i18next}}</label>
|
<label>Max Transcode Resolution</label>
|
||||||
<select ng-model="settings.maxTranscodeResolution"
|
<select ng-model="settings.maxTranscodeResolution"
|
||||||
ng-options="o.id as o.description for o in resolutionOptions "></select>
|
ng-options="o.id as o.description for o in resolutionOptions "></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h6 style="font-weight: bold">{{'settings_server.audio_options' | i18next}}</h6>
|
<h6 style="font-weight: bold">Audio Options</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.supported_audio_formats' | i18next}}</label>
|
<label>Supported Audio Formats</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioCodecs" ria-describedby="audioCodecsHelp" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.audioCodecs" ria-describedby="audioCodecsHelp" ></input>
|
||||||
<small id="audioCodecsHelp" class="form-text text-muted">{{'settings_server.supported_audio_formats_note' | i18next}}</small>
|
<small id="audioCodecsHelp" class="form-text text-muted">Comma separated list. Some possible values are 'ac3,aac,mp3'.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.max_audio_channels' | i18next}}</label>
|
<label>Maximum Audio Channels</label>
|
||||||
<select ng-model="settings.maxAudioChannels"
|
<select ng-model="settings.maxAudioChannels"
|
||||||
ng-options="o.id as o.description for o in maxAudioChannelsOptions" ria-describedby="maxAudioChannelsHelp"></select>
|
ng-options="o.id as o.description for o in maxAudioChannelsOptions" ria-describedby="maxAudioChannelsHelp"></select>
|
||||||
<small id="maxAudioChannelsHelp" class="form-text text-muted">{{'settings_server.max_audio_channels_note' | i18next}}</small>
|
<small id="maxAudioChannelsHelp" class="form-text text-muted">Note: 7.1 audio and on some clients, 6.1, is known to cause playback issues.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.audio_boost' | i18next}}</label>
|
<label>Audio Boost</label>
|
||||||
<select ng-model="settings.audioBoost"
|
<select ng-model="settings.audioBoost"
|
||||||
ng-options="o.id as o.description for o in audioBoostOptions" ria-describedby="audioBoostHelp"></select>
|
ng-options="o.id as o.description for o in audioBoostOptions" ria-describedby="audioBoostHelp"></select>
|
||||||
<small id="audioBoostHelp" class="form-text text-muted">{{'settings_server.audio_boost_note' | i18next}}</small>
|
<small id="audioBoostHelp" class="form-text text-muted">Note: Only applies when downmixing to stereo.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" ng-hide="hideIfNotPlexPath()">
|
<div class="row" ng-hide="hideIfNotPlexPath()">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h6 style="font-weight: bold">{{'settings_server.miscellaneous_options' | i18next}}</h6>
|
<h6 style="font-weight: bold">Miscellaneous Options</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.max_direct_stream_bitrate' | i18next}}</label>
|
<label>Max Direct Stream Bitrate (Kbps)</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.directStreamBitrate" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.directStreamBitrate" ></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.max_transcode_bitrate' | i18next}}</label>
|
<label>Max Transcode Bitrate (Kbps)</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeBitrate" aria-described-by="transcodebrhelp" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeBitrate" aria-described-by="transcodebrhelp" ></input>
|
||||||
<small id="transcodebrhelp" class='text-muted form-text'>{{'settings_server.max_transcode_bitrate_note' | i18next}}</small>
|
<small id="transcodebrhelp" class='text-muted form-text'>Plex will decide to transcode or direct play based on these settings and if Plex transcodes, it will try to match the transcode bitrate.</small>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.direct_stream_media_buffer' | i18next}}</label>
|
<label>Direct Stream Media Buffer Size</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.mediaBufferSize" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.mediaBufferSize" ></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.transcode_media_buffer' | i18next}}</label>
|
<label>Transcode Media Buffer Size</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeMediaBufferSize" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeMediaBufferSize" ></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.stream_protocol' | i18next}}</label>
|
<label>Stream Protocol</label>
|
||||||
<select ng-model="settings.streamProtocol"
|
<select ng-model="settings.streamProtocol"
|
||||||
ng-options="o.id as o.description for o in streamProtocols" ></select>
|
ng-options="o.id as o.description for o in streamProtocols" ></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input id="forceDirectPlay" type="checkbox" ng-model="settings.forceDirectPlay" ></input>
|
<input id="forceDirectPlay" type="checkbox" ng-model="settings.forceDirectPlay" ></input>
|
||||||
<label for="forceDirectPlay">{{'settings_server.force_direct_play' | i18next}}</label>
|
<label for="forceDirectPlay">Force Direct Play</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h6 style="font-weight: bold">{{'settings_server.subtitle_options' | i18next}}</h6>
|
<h6 style="font-weight: bold">Subtitle Options</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.subtitle_size' | i18next}}</label>
|
<label>Subtitle Size</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.subtitleSize" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.subtitleSize" ></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input class="form-check-input" id="enableSubtitles" type="checkbox" ng-model="settings.enableSubtitles" ng-disabled="shouldDisableSubtitles()" ></input>
|
<input class="form-check-input" id="enableSubtitles" type="checkbox" ng-model="settings.enableSubtitles" ng-disabled="shouldDisableSubtitles()" ></input>
|
||||||
<label class="form-check-label" for="enableSubtitles">{{'settings_server.enable_subtitle' | i18next}}</label>
|
<label class="form-check-label" for="enableSubtitles">Enable Subtitles (Requires Transcoding)</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" ng-hide="hideIfNotDirectPath()">
|
<div class="row" ng-hide="hideIfNotDirectPath()">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<h6 style="font-weight: bold">{{'settings_server.path_replacements' | i18next}}</h6>
|
<h6 style="font-weight: bold">Path Replacements</h6>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.original_plex_path' | i18next}}</label>
|
<label>Original Plex path to replace:</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplace" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplace" ></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{{'settings_server.replace_plex_path' | i18next}}</label>
|
<label>Replace Plex path with:</label>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplaceWith" ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplaceWith" ></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -65,7 +65,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='form-group col-md-7' ng-if="schedule.randomDistribution == 'weighted'" >
|
<div class='form-group col-md-7' ng-if="schedule.randomDistribution == 'weighted'" >
|
||||||
<label ng-if="$index==0" for="weightRange{{$index}}">Weight</label>
|
<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=60000
|
<input class='form-control-range custom-range' id="weightRange{{$index}}" type='range' ng-model='slot.weight' min=1 max=600
|
||||||
data-toggle="tooltip" data-placement="bottom" title="Slots with more weight will be picked more frequently."
|
data-toggle="tooltip" data-placement="bottom" title="Slots with more weight will be picked more frequently."
|
||||||
ng-change="refreshSlots()"
|
ng-change="refreshSlots()"
|
||||||
>
|
>
|
||||||
@ -177,8 +177,7 @@
|
|||||||
<div class="modal-footer" ng-show='!loading'>
|
<div class="modal-footer" ng-show='!loading'>
|
||||||
<div class='text-danger small'>{{error}}</div>
|
<div class='text-danger small'>{{error}}</div>
|
||||||
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
|
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
|
||||||
<button ng-show='! hideCreateLineup()' ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
|
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
|
||||||
<button ng-show='showResetSlots()' type="button" class="btn btn-sm btn-danger" ng-click="finished(false);">Reset Slots</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -148,8 +148,7 @@
|
|||||||
<div class="modal-footer" ng-show='!loading'>
|
<div class="modal-footer" ng-show='!loading'>
|
||||||
<div class='text-danger small'>{{error}}</div>
|
<div class='text-danger small'>{{error}}</div>
|
||||||
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
|
<button type="button" class="btn btn-sm btn-link" ng-click="finished(true)">Cancel</button>
|
||||||
<button ng-show='! hideCreateLineup()' ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
|
<button ng-disabled='disableCreateLineup()' type="button" class="btn btn-sm btn-primary" ng-click="finished(false);">Create Lineup</button>
|
||||||
<button ng-show='showResetSlots()' type="button" class="btn btn-sm btn-danger" ng-click="finished(false);">Reset Slots</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
<div>
|
<div>
|
||||||
<h5>{{'settings_xmltv.title' | i18next}}
|
<h5>XMLTV Settings
|
||||||
|
|
||||||
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
<button class="pull-right btn btn-sm btn-success" style="margin-left: 5px;" ng-click="updateSettings(settings)">
|
||||||
{{'settings_xmltv.update' | i18next}}
|
Update
|
||||||
</button>
|
</button>
|
||||||
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
<button class="pull-right btn btn-sm btn-warning" ng-click="resetSettings(settings)">
|
||||||
{{'settings_xmltv.reset_options' | i18next}}
|
Reset Options
|
||||||
</button>
|
</button>
|
||||||
</h5>
|
</h5>
|
||||||
<h6>{{'settings_xmltv.output_path' | i18next}}</h6>
|
<h6>Output Path</h6>
|
||||||
<input type="text" class="form-control form-control-sm" ng-model="settings.file" aria-describedby="pathhelp" readonly ></input>
|
<input type="text" class="form-control form-control-sm" ng-model="settings.file" aria-describedby="pathhelp" readonly ></input>
|
||||||
<small id="pathhelp" class="form-text text-muted">{{'settings_xmltv.output_path_note' | i18next}}</small>
|
<small id="pathhelp" class="form-text text-muted">You can edit this location in file xmltv-settings.json.</small>
|
||||||
<br></br>
|
<br></br>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<label>{{'settings_xmltv.epg_hours' | i18next}}</label>
|
<label>EPG Hours</label>
|
||||||
<input type="number" class="form-control form-control-sm" ng-model="settings.cache" aria-describedby="cachehelp"></input>
|
<input type="number" class="form-control form-control-sm" ng-model="settings.cache" aria-describedby="cachehelp"></input>
|
||||||
<small id="cachehelp" class="form-text text-muted">{{'settings_xmltv.epg_hours_note' | i18next}}</small>
|
<small id="cachehelp" class="form-text text-muted">How many hours of programming to include in the xmltv file.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<label>{{'settings_xmltv.refresh_timer' | i18next}}</label>
|
<label>Refresh Timer (hours)</label>
|
||||||
<input type="number" class="form-control form-control-sm" ng-model="settings.refresh" aria-describedby="timerhelp"></input>
|
<input type="number" class="form-control form-control-sm" ng-model="settings.refresh" aria-describedby="timerhelp"></input>
|
||||||
<small id="timerhelp" class="form-text text-muted">{{'settings_xmltv.refresh_timer_note' | i18next}}</small>
|
<small id="timerhelp" class="form-text text-muted">How often should the xmltv file be updated.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br ></br>
|
<br ></br>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="imageCache" aria-describedby="imageCacheHelp" ng-model='settings.enableImageCache'>
|
<input type="checkbox" class="form-check-input" id="imageCache" aria-describedby="imageCacheHelp" ng-model='settings.enableImageCache'>
|
||||||
<label class="form-check-label" for="stealth">{{'settings_xmltv.image_cache' | i18next}}</label>
|
<label class="form-check-label" for="stealth">Image Cache</label>
|
||||||
|
|
||||||
<div class='text-muted' id="imageCacheHelp">{{'settings_xmltv.image_cache_note' | i18next}}</div>
|
<div class='text-muted' id="imageCacheHelp">If enabled the pictures used for Movie and TV Show posters will be cached in dizqueTV's .dizqueTV folder and will be delivered by dizqueTV's server instead of requiring calls to Plex. Note that using fixed xmltv location in Plex (as opposed to url) will not work correctly in this case.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<div class='container-fluid'>
|
<div class='container-fluid'>
|
||||||
|
|
||||||
<h5>
|
<h5>
|
||||||
{{'guide.title' | i18next}}
|
{{title}}
|
||||||
</h5>
|
</h5>
|
||||||
<div style='padding:0; position:relative'>
|
<div style='padding:0; position:relative'>
|
||||||
<table class="table tvguide" style="{'column-width': colspanPercent + '%' }">
|
<table class="table tvguide" style="{'column-width': colspanPercent + '%' }">
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<tr ng-mouseover="channels[channelNumber].mouse=true" ng-mouseleave="channels[channelNumber].mouse=false" ng-repeat="channelNumber in channelNumbers track by $index" ng-Class="{'even' : ($index % 2==0), 'odd' : ($index % 2==1) }" >
|
<tr ng-mouseover="channels[channelNumber].mouse=true" ng-mouseleave="channels[channelNumber].mouse=false" ng-repeat="channelNumber in channelNumbers track by $index" ng-Class="{'even' : ($index % 2==0), 'odd' : ($index % 2==1) }" >
|
||||||
<td title='{{channels[channelNumber].altTitle}}' class='even channel-number' colspan="{{channelNumberColspan}}" >
|
<td title='{{channels[channelNumber].altTitle}}' class='even channel-number' colspan="{{channelNumberColspan}}" >
|
||||||
<div>
|
<div>
|
||||||
<a role="button" href='/media-player/{{channelNumber}}.m3u' title="{{'guide.attempt_to_play_channel' | i18next: {title: channels[channelNumber].altTitle} }}" class='btn btn-sm btn-outline-primary play-channel' ng-show='channels[channelNumber].mouse'>
|
<a role="button" href='/media-player/{{channelNumber}}.m3u' title="Attempt to play channel: '{{channels[channelNumber].altTitle}}' in local media player" class='btn btn-sm btn-outline-primary play-channel' ng-show='channels[channelNumber].mouse'>
|
||||||
<span class='fa fa-play'></span>
|
<span class='fa fa-play'></span>
|
||||||
</a>
|
</a>
|
||||||
<span ng-hide='channels[channelNumber].mouse' >
|
<span ng-hide='channels[channelNumber].mouse' >
|
||||||
|
|||||||
@ -30,15 +30,6 @@ module.exports = function (getShowData) {
|
|||||||
})
|
})
|
||||||
newProgs = newProgs.concat(shows[keys[i]])
|
newProgs = newProgs.concat(shows[keys[i]])
|
||||||
}
|
}
|
||||||
movies.sort( (a,b) => {
|
|
||||||
if (a.title === b.title) {
|
|
||||||
return 0;
|
|
||||||
} else if (a.title < b.title) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
} );
|
|
||||||
return newProgs.concat(movies);
|
return newProgs.concat(movies);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +59,7 @@ module.exports = function (getShowData) {
|
|||||||
let data = getShowData(progs[i]);
|
let data = getShowData(progs[i]);
|
||||||
if (data.hasShow) {
|
if (data.hasShow) {
|
||||||
let key = data.showId + "|" + data.order;
|
let key = data.showId + "|" + data.order;
|
||||||
if (typeof(tmpProgs[key]) === 'undefined') {
|
tmpProgs[key] = progs[i];
|
||||||
tmpProgs[key] = progs[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,7 +283,7 @@ module.exports = function ($http, $window, $interval) {
|
|||||||
console.error(msg , err);
|
console.error(msg , err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (includeCollections === true) {
|
if ( (includeCollections === true) && (res.viewGroup !== "artist" ) ) {
|
||||||
let k = res.librarySectionID;
|
let k = res.librarySectionID;
|
||||||
|
|
||||||
k = `/library/sections/${k}/collections`;
|
k = `/library/sections/${k}/collections`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user